moonzero is the integration layer of the moon* suite — the role go-zero plays for Go. It assembles a moonapi application from config, wraps it in middleware, and produces a runnable moonasgi AsgiApp that a server (mooncat) runs. It depends only on moonapi + moonasgi, so it stays backend-agnostic.
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=8888, 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:8888"
@mooncat.serve(server.to_asgi(), host=conf.host, port=conf.port) // run it (native)Verified across all backends (wasm, wasm-gc, js, native) in CI, 0 warnings under --deny-warn.
Beyond the base onion (logging, recovery, CORS, request-id), moonzero ships go-zero's resilience set, each modelled as a pure decision core over an injected Clock so its timing is exactly testable:
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 open
.use_(@moonzero.timeout(3000L, clock)) // deadline
.use_(@moonzero.structured_logging(clock)) // one JSON line/reqrate_limit— aTokenBucket: admits a burst, refills continuously, answers429when empty.breaker— aBreakerclosed/open/half-open state machine: trips after K consecutive failures, fails fast with503, then admits half-open probes to test recovery.timeout— aDeadlineenforced on the response path. Preemptively aborting a hung handler needs racing it against a timer (@async.any) under the native runtime; that race is wired at the async server edge, the deadline core is portable and tested.maxbytes— rejects a request whose declaredContent-Lengthexceeds the limit with413.structured_logging— aRequestLogrendered as one JSON line per request (method, path, status, duration, request-id, client-ip, user-agent).
// JWT HS256 — self-built SHA-256/HMAC (verified against NIST/RFC vectors)
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
// YAML config — the etc/*.yaml format go-zero ships, same lenient defaults as JSON
let conf = @moonzero.ServiceConf::from_yaml("name: greet\nport: 9000\nlog_level: error\n")
// 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)jwt_sign/jwt_verify— compact HS256 tokens on a self-built SHA-256 + HMAC-SHA256, signatures compared in constant time,exp/nbfenforced, and thealg:nonedowngrade refused. Interop-verified against the canonical jwt.io token.auth— the middleware that requiresAuthorization: Bearer <jwt>and answers401for an absent, malformed, tampered, or expired token.ServiceConf::from_yaml— a minimal-subset YAML parser (block maps, nesting, sequences, typed scalars, comments) feeding the same field reader as the JSON loader, so both formats agree field-for-field.RpcServer/RpcGroup— config-driven zRPC groups that registermoonrpcMethodhandlers by gRPC path.RpcChannel— a client over the h2c transport:to_h2turns the registered handlers into amoonrpcH2Server, and thecallfamily runs real exchanges through it — HPACK-coded HEADERS, length-prefixed DATA frames, and thegrpc-statustrailer read back off the reply. Unary (call), server-streaming (call_server_streaming, one request then every framed reply in order), client-streaming (call_client_streaming, each request as its own DATA frame then one reply after half-close), and bidirectional streaming all round-trip through the same engine. A call to an unregistered path comes backUNIMPLEMENTED, the trailers-only response a gRPC server sends for an unknown method.- Bidi streaming —
open_bidiopens a stream that stays open both ways: eachBidiCall::sendwrites one request message and returns the replies the server produced right then (an echo handler answers each message as it arrives), andclose_sendhalf-closes, runs the server'son_end, and reports the finalgrpc-status.call_bidi_streamingdrives a whole exchange in one shot, returning the interleaved replies followed by theon_endmessages. The channel's HPACK decoder is advanced across every reply block, so its dynamic table stays in lockstep with the engine's encoder for the life of the call. ShutdownCoordinator— graceful shutdown for a serving zRPC server:dispatch_gracefulcounts each call as in-flight for its duration,initiate_shutdownmakes new calls come backUnavailable(a stopped listener) while in-flight ones run to completion, andis_drainedreports when the last one has finished so the process may exit.
// 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, msInMemoryRegistry— a service registry shaped like go-zero's etcddiscovstore: aservice -> instance -> endpointmap with a monotonic store revision a watcher can compare against.RoundRobinandpick_firstbalancers select an endpoint from a resolved set.PersistentRegistry— the persisted, watchable registry: adds a livewatch(Put/Delete events in revision order), anevents_sincecatch-up from any revision, andsnapshot/restorethat round-trip the whole keyspace through an etcd v3RangeResponse-shaped JSON document without losing a revision — the bytes a file- or etcd-backed deployment persists and reloads.LoadBalancedChannel— a load-balanced zRPC client over aResolveinterface: it resolves a service, picks a live instance with the balancer, dials it through anRpcCluster, and makes the call, so instances registering or leaving between calls take effect on the next one. Any store exposingresolver()backs it; an etcd- or consul-backed store drops in unchanged. Unary, server-streaming, and bidi calls all go through the resolve-then-balance path.FileRegistry— real registry I/O in the nativediscovsub-package (← go-zero'sdiscovpublisher/subscriber, over the filesystem instead of etcd's network):persist_registrywrites the snapshot to a real file throughmoonbitlang/async's fs,FileRegistry::loadreads it back and exposes aresolver(),reloadre-reads and returns thePut/Deletediff since the last load, andwatch/watch_onceblock on a real filesystem watcher and reload on every change. Reader and publisher share only the file, exactly as an etcd subscriber and publisher share only the keyspace, so the balancer and load-balanced channel above drive it unchanged.ServerMetrics— aCounterVecof per-method/route/status request tallies and a cumulative latencyHistogram(Prometheuslebuckets, sum, count), driven by themetricsmiddleware that times each request on the clock.tracing— trace-id propagation: continue an inbound W3Ctraceparentor start a new trace, mint a child span, and stamptraceparent+x-trace-idonto the response.
Typed config (JSON + YAML with defaults, timeout + log level) + service assembly + the base middleware onion (logging, recovery, CORS, request-id) + the resilience set (timeout, rate-limit, breaker, maxbytes, structured logging) + route groups + JWT auth + zRPC groups with real unary, server/client-streaming, and bidirectional h2c round-trips + graceful shutdown draining in-flight calls + a persisted, watchable registry with round-robin/pick-first balancers and a load-balanced client + real file-backed registry I/O with a live filesystem watcher + request metrics + trace-id propagation are here. Next: an etcd/consul network client behind the same Resolve interface, and moonctl-driven scaffolding of a full moonzero service from a spec.
Apache-2.0.