A service framework for MoonBit after go-zero: config-driven REST engine and middleware over moonapi, zRPC over moonrpc, discovery on etcd, consul and redis, and go-zero's breaker, shedder and limiters over moonward.
Dependencies
Moved on mooncakes from Lfan-ke/moonzero to moonbitstack/moonzero.
flowchart LR
conf["ServiceConf"] --> srv["**moonzero** Server"]
api["moonapi App"] --> srv
mw["middleware<br/>(logging, ...)"] --> srv
srv -->|"to_asgi()"| asgi(["moonasgi AsgiApp"])
asgi --> cat["mooncat serves it"]let app = @moonapi.App::new()
let api = @moonzero.Group::new(app, "/api/v1") // prefix a set of routes
api.get("/ping", _ctx => @moonapi.text(200, "pong"))
let conf = @moonzero.ServiceConf::new(
name="greet", host="127.0.0.1", port=12000, timeout_ms=3000, log_level=Info,
)
let server = @moonzero.Server::new(conf, app)
.use_(@moonzero.cors(@moonzero.CorsConf::new())) // Access-Control-* headers
.use_(@moonzero.request_id()) // x-request-id per request
.use_(@moonzero.recovery) // 500 instead of a panic
.use_(@moonzero.logging)
server.describe() // "greet listening on 127.0.0.1:12000"
@mooncat.serve(server.to_asgi(), host=conf.host, port=conf.port) // run it (native)| Package | What | go-zero |
|---|---|---|
| clock | the millisecond clock every timing decision is a function of | core/timex |
| breaker | the adaptive circuit breaker, over moonward's | core/breaker |
| shedder | the adaptive load shedder, over moonward's | core/load |
| limit | token bucket, period limit and connection limit, in memory over moonward's or on Redis | core/limit, core/syncx |
| logx | levelled, structured logging | core/logx |
| metric | counters and histograms in the Prometheus format | core/metric |
| trace | W3C trace context | core/trace |
| proc | graceful shutdown | core/proc |
| conf | configuration loading and the service, REST and RPC configuration | core/conf |
| registry | endpoints, registries and balancers | core/discov |
| redis, etcd, consul | the three stores' clients, and discovery on each | core/stores/redis, core/discov |
| zrpc | the zRPC server and client on moonrpc | zrpc |
| rest/handler | the middlewares | rest/handler |
| rest | the engine, the server and route groups | rest |
| discov | native file, socket and watch drivers for the registries | — |
let clock = @moonzero.Clock::new(() => now_ms()) // real time at the async edge
let server = @moonzero.Server::new(conf, app)
.use_(@moonzero.maxbytes(1 << 20)) // 413 over 1 MiB
.use_(@moonzero.rate_limit(@moonzero.TokenBucket::new(100.0), clock))// 429 when empty
.use_(@moonzero.breaker(@moonzero.Breaker::new(clock))) // 503 while shedding
.use_(@moonzero.timeout(3000L, clock)) // deadline
.use_(@moonzero.structured_logging(clock)) // one JSON line/req// JWT HS256, on mooncred and mooncrypt
let token = @moonzero.jwt_sign(
Map([("sub", Json::string("alice")), ("exp", Json::number(1893456000.0))]),
"topsecret",
)
let server = @moonzero.Server::new(conf, app)
.use_(@moonzero.auth("topsecret", clock)) // 401 unless a valid Bearer JWT
.use_(@moonzero.tracing()) // W3C traceparent in/out
.use_(@moonzero.metrics(m, clock)) // request counter + latency histogram
// The etc/*.yaml goctl writes, loaded and assembled the way go-zero's engine does
let conf = @moonzero.RestConf::from_yaml(etc_yaml) // PascalCase keys, MOONZERO_* env overrides
let server = @moonzero.RestEngine::new(conf).build(app) // the chain Middlewares asks for
// A real zRPC call over moonrpc's h2c transport — unary and streaming
let rpc = @moonzero.RpcServer::new(@moonzero.RpcServerConf::new(name="greeter", port=9090))
let g = rpc.group("hello.Greeter")
g.register("SayHello", req => handle(req)) // unary
g.register_server_streaming("Tail", req => chunks(req)) // one in, many out
g.register_client_streaming("Upload", msgs => summarize(msgs)) // many in, one out
g.register_bidi_streaming("Chat", () => @moonzero.BidiStreamHandler::{
on_message: m => [echo(m)], // a reply the instant each message arrives
on_end: () => [b"bye"], // a farewell after the client half-closes
})
let ch = @moonzero.RpcChannel::connect(rpc)
ch.call("/hello.Greeter/SayHello", request) // Ok(reply) | Err(status)
ch.call_server_streaming("/hello.Greeter/Tail", request) // Ok([msg, ...]) | Err(status)
ch.call_client_streaming("/hello.Greeter/Upload", [a, b]) // Ok(reply) | Err(status)
let call = ch.open_bidi("/hello.Greeter/Chat") // stream both ways
call.send(a) // -> the replies produced right then (interleaved)
call.close_send() // -> Ok([on_end replies...]) | Err(status)// Persisted, watchable registry (etcd v3-shaped): register, watch, snapshot
let reg = @moonzero.PersistentRegistry::new()
reg.watch(e => log(e)) // Put/Delete events in revision order
reg.register("greeter", @moonzero.Endpoint::new("10.0.0.1", 9090))
reg.register("greeter", @moonzero.Endpoint::new("10.0.0.2", 9090))
let saved = reg.snapshot() // persist to a file/etcd; restore reloads it
// Real registry I/O over a file (native): a publisher persists, a reader watches
@discov.persist_registry(path, reg) // write the snapshot to a real file
let reader = @discov.FileRegistry::load(path) // load it back
reader.reload() // re-read -> the Put/Delete diff since last load
reader.watch(dir, e => log(e)) // reload on every filesystem change
// Load-balanced client: resolve an instance, then call it over h2c
let ch = @moonzero.LoadBalancedChannel::new(reg.resolver(), cluster, "greeter")
ch.call("/hello.Greeter/SayHello", request) // dials 10.0.0.1, then .2, cycling
// Metrics read out for a /metrics scrape after serving
let m = @moonzero.ServerMetrics::new()
m.requests().value("GET /ping 200") // request count for that label
m.latency().mean() // mean request latency, mstype RpcHandler = (Bytes) -> Bytesfn canonical_key(key : String) -> Stringfn consul_check_id(service_id : String) -> Stringfn consul_register_body(id : String, name : String, address : String, port : Int, ttl_secs : Int) -> Bytesfn etcd_prefix_end(prefix : Bytes) -> Bytesfn etcd_service_prefix(prefix : String, service : String) -> Stringlet exposition_content_type : Stringfn generate_span_id(seed : Int64) -> Stringfn generate_trace_id(seed : Int64) -> Stringfn jwt_authorized(token : String?, secret : String, now_secs : Int64) -> Boollet message_key : Stringlet period_script : Stringlet port : Intfn redis_service_pattern(prefix : String, service : String) -> Stringfn redis_service_prefix(prefix : String, service : String) -> Stringlet rpc_port : Intfn set_log_writer(writer : (String) -> Unit) -> Unitlet stamp_key : Stringlet token_script : StringInstall
Download zipA service framework for MoonBit after go-zero: config-driven REST engine and middleware over moonapi, zRPC over moonrpc, discovery on etcd, consul and redis, and go-zero's breaker, shedder and limiters over moonward.
Dependencies