Skip to content

Commit 95adc61

Browse files
committed
docs(examples): per-feature example suite — middleware, resilience, discovery, tracing, zrpc (14 new).
Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
1 parent e3c3eb8 commit 95adc61

41 files changed

Lines changed: 1853 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/00-metrics/main.mbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/// Prometheus exposition a `/metrics` scrape would read back.
1919
///
2020
/// moon run --target native examples/00-metrics
21+
#coverage.skip
2122
async fn main {
2223
let app = @moonapi.App::new()
2324
let api = @moonzero.Group::new(app, "/api/v1")

examples/01-config/main.mbt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
///|
2+
/// Load a `ServiceConf` three ways — the `new()` defaults, a partial JSON config
3+
/// filled from those defaults, and the YAML `etc/*.yaml` format go-zero ships —
4+
/// then read the `LogLevel` ordering and the raw `yaml_parse` output.
5+
///
6+
/// moon run examples/01-config
7+
#coverage.skip
8+
fn main {
9+
try {
10+
let def = @moonzero.ServiceConf::new()
11+
println("defaults: " + describe(def))
12+
13+
// A partial JSON config: only the fields present override; the rest fall back
14+
// to the same defaults `new()` uses (go-zero's `,optional`/`,default=`).
15+
let json_src =
16+
#|{ "name": "greet", "port": 9000, "log_level": "error" }
17+
let from_json = @moonzero.ServiceConf::from_json(json_src)
18+
println("from_json: " + describe(from_json))
19+
20+
// The YAML config format go-zero actually writes, decoded through the same
21+
// lenient field reader so it agrees with JSON field-for-field.
22+
let yaml_src = "name: greet\nhost: 127.0.0.1\nport: 9000\ntimeout_ms: 1500\nlog_level: debug\n"
23+
let from_yaml = @moonzero.ServiceConf::from_yaml(yaml_src)
24+
println("from_yaml: " + describe(from_yaml))
25+
26+
// LogLevel derives Compare in verbosity order, so thresholds test directly.
27+
let debug = @moonzero.LogLevel::parse("debug")
28+
let error = @moonzero.LogLevel::parse("error")
29+
println(
30+
"log levels: " +
31+
debug.to_string() +
32+
" < " +
33+
error.to_string() +
34+
" = " +
35+
(debug < error).to_string(),
36+
)
37+
println(
38+
"unknown level falls back to: " +
39+
@moonzero.LogLevel::parse("verbose").to_string(),
40+
)
41+
42+
// The self-built YAML parser lowered to the same Json the JSON loader reads.
43+
let doc = @moonzero.yaml_parse(
44+
"name: greet\nnested:\n port: 9000\n tags:\n - a\n - b\n",
45+
)
46+
println("yaml_parse -> " + doc.stringify())
47+
} catch {
48+
ConfigError(msg) => println("config error: " + msg)
49+
}
50+
}
51+
52+
///|
53+
fn describe(conf : @moonzero.ServiceConf) -> String {
54+
conf.name +
55+
" " +
56+
conf.host +
57+
":" +
58+
conf.port.to_string() +
59+
" timeout=" +
60+
conf.timeout_ms.to_string() +
61+
"ms level=" +
62+
conf.log_level.to_string()
63+
}

examples/01-config/moon.pkg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {
2+
"Lfan-ke/moonzero",
3+
}
4+
5+
pkgtype(kind: "executable")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Generated using `moon info`, DON'T EDIT IT
2+
package "Lfan-ke/moonzero/examples/01-config"
3+
4+
// Values
5+
6+
// Errors
7+
8+
// Types and methods
9+
10+
// Type aliases
11+
12+
// Traits

examples/02-middleware/main.mbt

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
///|
2+
/// Assemble the base middleware onion — CORS, request-id, tracing, structured
3+
/// logging, request logging, and recovery — over a two-route app, then drive a
4+
/// normal request (watching the injected response headers and the JSON access log)
5+
/// and one that raises (watching recovery answer 500 instead of crashing).
6+
///
7+
/// moon run --target native examples/02-middleware
8+
#coverage.skip
9+
async fn main {
10+
let app = @moonapi.App::new()
11+
let api = @moonzero.Group::new(app, "/api/v1")
12+
api.get("/ping", _ctx => @moonapi.text(200, "pong"))
13+
api.get("/boom", _ctx => raise Boom)
14+
15+
// An incrementing clock so the access log shows a real elapsed duration.
16+
let tick : Ref[Int64] = { val: 0L }
17+
let clock = @moonzero.Clock::new(() => {
18+
let now = tick.val
19+
tick.val = now + 5L
20+
now
21+
})
22+
let conf = @moonzero.ServiceConf::new(
23+
name="greet",
24+
host="127.0.0.1",
25+
port=8888,
26+
)
27+
let onion = @moonzero.Server::new(conf, app)
28+
.use_(@moonzero.cors(@moonzero.CorsConf::new()))
29+
.use_(@moonzero.request_id())
30+
.use_(@moonzero.tracing())
31+
.use_(@moonzero.structured_logging(clock))
32+
.use_(@moonzero.logging)
33+
.use_(@moonzero.recovery)
34+
println(onion.describe())
35+
let handler = onion.to_asgi()
36+
37+
// A normal request: 200, with the middleware's response headers stamped on.
38+
let (status, headers, body) = drive(handler, "GET", "/api/v1/ping", [])
39+
println("ping -> " + status.to_string() + " body=" + text(body))
40+
println(" x-request-id=" + hget(headers, "x-request-id"))
41+
println(" x-trace-id=" + hget(headers, "x-trace-id"))
42+
println(" traceparent=" + hget(headers, "traceparent"))
43+
println(
44+
" access-control-allow-origin=" +
45+
hget(headers, "access-control-allow-origin"),
46+
)
47+
48+
// A client-supplied request id is reused verbatim rather than minted.
49+
let (_, reused, _) = drive(handler, "GET", "/api/v1/ping", [
50+
("x-request-id", "heke1228"),
51+
])
52+
println("supplied request id reused: " + hget(reused, "x-request-id"))
53+
54+
// A handler that raises: recovery answers 500 instead of letting it escape.
55+
let (boom_status, _, boom_body) = drive(handler, "GET", "/api/v1/boom", [])
56+
println("boom -> " + boom_status.to_string() + " body=" + text(boom_body))
57+
58+
// The structured access-log record on its own, rendered as one JSON line.
59+
let entry = @moonzero.RequestLog::{
60+
http_method: "GET",
61+
path: "/api/v1/ping",
62+
status: 200,
63+
duration_ms: 5L,
64+
request_id: "req-1",
65+
client_ip: "127.0.0.1",
66+
user_agent: "lfanke",
67+
}
68+
println("RequestLog: " + entry.render())
69+
}
70+
71+
///|
72+
/// A local error a route raises to exercise recovery.
73+
suberror Boom
74+
75+
///|
76+
/// Feed one request through an assembled ASGI onion in-process, capturing the
77+
/// response status, the `HttpResponseStart` headers, and the body.
78+
async fn drive(
79+
handler : @moonasgi.AsgiApp,
80+
verb : String,
81+
path : String,
82+
headers : Array[(String, String)],
83+
) -> (Int, Array[(String, String)], Bytes) {
84+
let scope = @moonasgi.Scope::Http(
85+
@moonasgi.HttpScope::new(http_method=verb, path~, headers~),
86+
)
87+
let sent : Ref[Bool] = { val: false }
88+
let receive : @moonasgi.Receive = () => {
89+
if sent.val {
90+
@moonasgi.Event::HttpDisconnect
91+
} else {
92+
sent.val = true
93+
@moonasgi.Event::HttpRequest(body=b"", more_body=false)
94+
}
95+
}
96+
let status : Ref[Int] = { val: 0 }
97+
let out : Ref[Array[(String, String)]] = { val: [] }
98+
let body = Buffer()
99+
let sink : @moonasgi.Send = event => {
100+
match event {
101+
HttpResponseStart(status=s, headers=h, ..) => {
102+
status.val = s
103+
out.val = h
104+
}
105+
HttpResponseBody(body=b, ..) => body.write_bytes(b)
106+
_ => ()
107+
}
108+
}
109+
handler(scope, receive, sink)
110+
(status.val, out.val, body.to_bytes())
111+
}
112+
113+
///|
114+
fn hget(headers : Array[(String, String)], name : String) -> String {
115+
for pair in headers {
116+
if pair.0 == name {
117+
return pair.1
118+
}
119+
}
120+
"(none)"
121+
}
122+
123+
///|
124+
fn text(b : Bytes) -> String {
125+
let sb = StringBuilder::new()
126+
for i = 0; i < b.length(); i = i + 1 {
127+
sb.write_char(b[i].to_int().unsafe_to_char())
128+
}
129+
sb.to_string()
130+
}

examples/02-middleware/moon.pkg

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Drives the assembled ASGI onion in-process, awaiting the async app the
2+
// middleware wraps — native-only, like moonzero's other async edges.
3+
supported_targets = "native"
4+
5+
import {
6+
"Lfan-ke/moonzero",
7+
"Lfan-ke/moonapi",
8+
"Lfan-ke/moonasgi",
9+
"moonbitlang/async",
10+
}
11+
12+
pkgtype(kind: "executable")
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
///|
2+
/// Drive each resilience decision core over an explicit clock, so its whole state
3+
/// machine is visible without a running server: the token bucket, the circuit
4+
/// breaker, the request deadline, the max-conns permit pool, and the fixed-window
5+
/// period limiter.
6+
///
7+
/// moon run examples/03-resilience-core
8+
#coverage.skip
9+
fn main {
10+
// TokenBucket: a burst of `capacity` back-to-back, then empty until it refills.
11+
let bucket = @moonzero.TokenBucket::new(3.0, burst=3.0, now=0)
12+
let burst = [
13+
bucket.allow(0),
14+
bucket.allow(0),
15+
bucket.allow(0),
16+
bucket.allow(0),
17+
]
18+
println("token bucket burst-of-3 then empty: " + bools(burst))
19+
println(" available after 1s refill: " + bucket.available(1000L).to_string())
20+
println(" admits again once refilled: " + bucket.allow(1000L).to_string())
21+
22+
// Breaker: two consecutive failures trip it Open; after the cool-down it admits
23+
// one HalfOpen probe, and a probe success closes it again.
24+
let breaker = @moonzero.Breaker::new(
25+
max_failures=2,
26+
open_ms=1000L,
27+
half_open_max=1,
28+
)
29+
breaker.record_failure(0L)
30+
breaker.record_failure(0L)
31+
println("breaker after 2 failures: " + breaker.state().to_string())
32+
println(" admits while open (t=500): " + breaker.allow(500L).to_string())
33+
let probe = breaker.allow(1000L)
34+
println(
35+
" admits half-open probe (t=1000): " +
36+
probe.to_string() +
37+
", state=" +
38+
breaker.state().to_string(),
39+
)
40+
breaker.record_success()
41+
println(" after probe success: " + breaker.state().to_string())
42+
43+
// Deadline: a budget measured from a start instant; a non-positive budget never
44+
// expires (go-zero's disabled-timeout convention).
45+
let deadline = @moonzero.Deadline::start(500L, 0L)
46+
println(
47+
"deadline remaining at t=100: " +
48+
deadline.remaining(100L).to_string() +
49+
"ms, expired at t=600: " +
50+
deadline.expired(600L).to_string(),
51+
)
52+
println(
53+
" disabled deadline expires: " +
54+
@moonzero.Deadline::start(0L, 0L).expired(999999L).to_string(),
55+
)
56+
57+
// MaxConns: a permit pool that admits up to `max` in flight at once.
58+
let conns = @moonzero.MaxConns::new(2)
59+
let taken = [conns.try_acquire(), conns.try_acquire(), conns.try_acquire()]
60+
println(
61+
"max-conns(2) three acquires: " +
62+
bools(taken) +
63+
", in_flight=" +
64+
conns.in_flight().to_string(),
65+
)
66+
conns.release()
67+
println(
68+
" after one release, acquire again: " + conns.try_acquire().to_string(),
69+
)
70+
71+
// PeriodLimit: a fixed window of `quota` per period, per key.
72+
let period = @moonzero.PeriodLimit::new(period_secs=1, quota=3)
73+
let window = [
74+
period.take("k", 0L),
75+
period.take("k", 0L),
76+
period.take("k", 0L),
77+
period.take("k", 0L),
78+
]
79+
println("period-limit quota-3 window: " + join(window.map(period_name)))
80+
println(
81+
" next window (t=1s) resets: " + period_name(period.take("k", 1000L)),
82+
)
83+
}
84+
85+
///|
86+
fn period_name(r : @moonzero.PeriodResult) -> String {
87+
match r {
88+
PeriodAllowed => "allowed"
89+
PeriodHitQuota => "hit-quota"
90+
PeriodOverQuota => "over-quota"
91+
}
92+
}
93+
94+
///|
95+
fn bools(items : Array[Bool]) -> String {
96+
join(items.map(b => b.to_string()))
97+
}
98+
99+
///|
100+
fn join(items : Array[String]) -> String {
101+
let sb = StringBuilder::new()
102+
sb.write_char('[')
103+
for i = 0; i < items.length(); i = i + 1 {
104+
if i > 0 {
105+
sb.write_string(", ")
106+
}
107+
sb.write_string(items[i])
108+
}
109+
sb.write_char(']')
110+
sb.to_string()
111+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {
2+
"Lfan-ke/moonzero",
3+
}
4+
5+
pkgtype(kind: "executable")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Generated using `moon info`, DON'T EDIT IT
2+
package "Lfan-ke/moonzero/examples/03-resilience-core"
3+
4+
// Values
5+
6+
// Errors
7+
8+
// Types and methods
9+
10+
// Type aliases
11+
12+
// Traits

0 commit comments

Comments
 (0)