Three programs,
and what they print.
No excerpts and no pseudo-code: each listing below is the whole file
under src/examples, and each output is what that file
printed when it ran.
- 00
- a trace, in memory
- 01
- one hop of propagation
- 02
- a traced ASGI request
hello
src/examples/00-hello/main.mbt moon run src/examples/00-hello// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A tracer, a root span, a child span, some attributes and an event, and the two
/// spans as a sink receives them.
///
/// moon run src/examples/00-hello
#coverage.skip
fn main {
let recorder = @trace.Recorder::new()
let tracer = @trace.Tracer::new(
rand=random_bytes,
sink=recorder,
sampler=@trace.parent_based(root=@trace.always),
)
// Times are handed in: this library never reads a clock, so a batch job
// replaying yesterday's events and a server under load use the same code.
let started = 1_700_000_000_000_000_000L
let root = tracer.start("GET /orders/{id}", kind=@trace.Server, at=started, attributes=[
("http.request.method", @trace.Str("GET")),
]) catch {
_ => abort("the platform has no entropy source")
}
let child = tracer.start(
"SELECT orders",
parent=root.context(),
kind=@trace.Client,
at=started + 1_000_000L,
) catch {
_ => abort("the platform has no entropy source")
}
child.set_attribute("db.rows", @trace.Int(3L))
child.set_status(@trace.Ok)
child.finish(at=started + 2_000_000L)
root.add_event("cache.miss", at=started + 3_000_000L)
root.set_attribute("http.response.status_code", @trace.Int(200L))
root.finish(at=started + 4_000_000L)
for span in recorder.spans() {
println(
"\{span.name} \{span.context.to_traceparent()} \{span.duration()} ns",
)
for pair in span.attributes {
println(" \{pair.0} = \{pair.1}")
}
for event in span.events {
println(" event \{event.name} at \{event.at}")
}
}
}
///|
/// Eight or sixteen octets from the platform's entropy source. A caller this close
/// to the edge is the one that knows what to do when there is none; here that is to
/// stop rather than to trace with predictable ids.
fn random_bytes(width : Int32) -> Bytes {
@moonseed.bytes(width, @system.source) catch {
_ => abort("the platform has no entropy source")
}
}What it prints
SELECT orders 00-be3c4e65302ba7c1387a58b5144df67d-059be837e83c67a2-01 1000000 ns
db.rows = 3
GET /orders/{id} 00-be3c4e65302ba7c1387a58b5144df67d-cd73d0d2d74ffe97-01 4000000 ns
http.request.method = "GET"
http.response.status_code = 200
event cache.miss at 1700000000003000000
Two spans, child first because a sink sees them in the order they finished. The trace id is shared; each span has its own. The ids differ on every run, because the example draws them from the platform's entropy source rather than a script.
propagate
src/examples/01-propagate/main.mbt moon run src/examples/01-propagate// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The two headers a hop reads and the three it writes, over the same carrier: an
/// array of name and value pairs, which is what an ASGI scope and an HTTP client's
/// header list both are.
///
/// moon run src/examples/01-propagate
#coverage.skip
fn main {
let inbound = [
("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
("tracestate", "vendor=abc"),
("baggage", "userId=alice,note=caf%C3%A9"),
("accept", "application/json"),
]
println("inbound")
for header in inbound {
println(" \{header.0}: \{header.1}")
}
// What the hop understood: the trace to join, and the bag that came with it.
let extracted = @propagate.extract(inbound)
println("")
println(
match extracted.context {
Some(context) => "joined trace \{context.to_traceparent()}"
None => "no usable context, this hop starts a trace"
},
)
println("baggage: \{extracted.baggage.to_header()}")
// A span of this hop's own, under whatever context arrived.
let tracer = @trace.Tracer::new(
rand=random_bytes,
sink=@trace.Recorder::new(),
sampler=@trace.parent_based(root=@trace.always),
)
let span = match extracted.context {
Some(parent) =>
tracer.start("publish", parent~, kind=@trace.Client, at=1L) catch {
_ => abort("the platform has no entropy source")
}
None =>
tracer.start("publish", kind=@trace.Client, at=1L) catch {
_ => abort("the platform has no entropy source")
}
}
span.finish(at=2L)
// The next request carries this hop's span as its parent, and the bag unchanged.
let outgoing = @propagate.inject(inbound, {
context: Some(span.context()),
baggage: extracted.baggage,
})
println("")
println("outgoing")
for header in outgoing {
println(" \{header.0}: \{header.1}")
}
}
///|
fn random_bytes(width : Int32) -> Bytes {
@moonseed.bytes(width, @system.source) catch {
_ => abort("the platform has no entropy source")
}
}What it prints
inbound traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 tracestate: vendor=abc baggage: userId=alice,note=caf%C3%A9 accept: application/json joined trace 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 baggage: userId=alice,note=caf%C3%A9 outgoing accept: application/json traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-16e31247ca00e178-01 tracestate: vendor=abc baggage: userId=alice,note=caf%C3%A9
The trace id survives the hop and the span id does not: that is the
whole of propagation. Note the bag is percent-encoded on the wire and
decoded in memory, and that accept is carried through
untouched because it is not a header this library owns.
asgi
src/examples/02-asgi/main.mbt moon run src/examples/02-asgi// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A request through a traced application: the middleware reads the context the
/// request arrived with, opens a server span, and puts the context back on the
/// response.
///
/// moon run src/examples/02-asgi
///
/// The application is driven in process, the way a caller would drive it; a real
/// server binds the same `@spec.AsgiApp` to a socket instead.
#coverage.skip
async fn main {
@async.pause()
let recorder = @trace.Recorder::new()
let tracer = @trace.Tracer::new(
rand=random_bytes,
sink=recorder,
sampler=@trace.parent_based(root=@trace.always),
)
let layer = @asgi.Tracing::new(tracer~, now=clock())
let app : @spec.AsgiApp = async fn(_, receive, send) {
ignore(receive())
send(
@spec.HttpResponseStart(
status=200,
headers=[("content-type", "application/json")],
trailers=false,
),
)
send(@spec.HttpResponseBody(body=b"{\"ok\":true}", more_body=false))
}
let traced = layer.wrap(app)
let scope = @spec.Http(
@spec.HttpScope::new(
http_method="POST",
path="/orders",
headers=[
(
"traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
),
("baggage", "userId=alice"),
],
server=("shop.example.com", Some(8443)),
),
)
// A minimal server: one request in, whatever the application sends out.
let handled = Ref(false)
let receive : @spec.Receive = async fn() {
@async.pause()
if handled.val {
@spec.HttpDisconnect
} else {
handled.val = true
@spec.HttpRequest(body=b"", more_body=false)
}
}
let send : @spec.Send = async fn(event) {
@async.pause()
match event {
@spec.HttpResponseStart(status~, headers~, ..) => {
println("response \{status}")
for header in headers {
println(" \{header.0}: \{header.1}")
}
}
@spec.HttpResponseBody(body~, ..) =>
println("body \{body.length()} octets")
_ => ()
}
}
traced(scope, receive, send)
println("")
for span in recorder.spans() {
println("\{span.name} \{span.duration()} ns \{span.status}")
for pair in span.attributes {
println(" \{pair.0} = \{pair.1}")
}
}
}
///|
/// A clock that moves ten milliseconds per reading. A real service hands in its own
/// clock here; this one keeps the example's output the same on every run.
fn clock() -> () -> Int64 {
let now = Ref(1_700_000_000_000_000_000L)
() => {
now.val = now.val + 10_000_000L
now.val
}
}
///|
fn random_bytes(width : Int32) -> Bytes {
@moonseed.bytes(width, @system.source) catch {
_ => abort("the platform has no entropy source")
}
}What it prints
response 200 content-type: application/json traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-960516d9a24e7b34-01 baggage: userId=alice body 11 octets POST /orders 10000000 ns Unset http.request.method = "POST" url.path = "/orders" url.scheme = "http" network.protocol.version = "1.1" server.address = "shop.example.com" server.port = 8443 http.response.status_code = 200
The middleware read the context the request arrived with, opened a server span under it, and put the context back on the response. The clock is a script that advances ten milliseconds per reading, which is why the duration is a round number.
Run them yourself.
The three programs are packages in this repository, so a checkout can run them without installing anything else:
moon run src/examples/00-hello --target native
moon run src/examples/01-propagate --target native
moon run src/examples/02-asgi --target native
00-hello and 01-propagate build on every
target. 02-asgi awaits what the application sends, and the
async runtime has no wasm-gc build, so that target builds a second file
that says so rather than a program that only looks like the example.
The home page runs a fourth program: it compiles this library to JavaScript and draws what came out. That one is not meant to be run by hand: the page loads it as a module and draws its output.