moontel Examples · three programs, three outputs Examples GitHub
Examples

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
00

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.

01

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.

02

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.

03

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.