One request,
drawn to scale.
moontel is OpenTelemetry tracing for MoonBit. It builds the spans, decides which traces to keep, and reads and writes the two headers that carry a trace from one service to the next.
Six spans, measured in nanoseconds.
The library made this trace; the page only draws it. Times are handed in, because nothing in moontel reads a clock — so the same request comes out the same, here and in a replay.
Fifty-five characters carry the whole thing.
One header, four fields. Every hop keeps the trace id, replaces the span id, and passes the vendor state on untouched.
00-—-—-01
Written by this library, not typed here
Id is octets, not text. Sixteen for the trace and eight for the span, as W3C Trace Context lays them out; hexadecimal is a spelling produced at the wire and in a log, never stored. A parser that accepted upper and lower case would let two spellings of one trace id into the same backend, so it refuses the upper one.
One decision, for the whole trace.
A sampler decides once, at the root, and every later hop obeys. The decision reads the trace id — not a clock, not a counter — so the same trace gets the same answer in every process. Raise the rate and every trace the lower rate kept is still kept.
— requests · each a trace id this library sampled for real
Three things moontel will not invent for itself.
Where ids come from, what time it is, and where finished spans go. All three are handed in, which is why the same code serves a server, a batch job replaying last night's events, and a replay that needs the timeline to be exact.
Randomness
A function from a width to that many octets. Sixteen is a trace id, eight is a span id. A generator that returns three, or sixteen zeros, is refused rather than papered over.
fn tracer_to(
rand : (Int32) -> Bytes,
sink : &@trace.Sink,
) -> @trace.Tracer {
@trace.Tracer::new(
rand~,
sink~,
sampler=@trace.parent_based(
root=@trace.always,
),
)
}
The clock
Every call that needs a time is handed one, in nanoseconds: the start of a span, its end, the instant of an event. Nothing here reads a clock, so a replay of last night is not a replay with different timings.
fn span_around(
tracer : @trace.Tracer,
now : () -> Int64,
) -> Unit raise {
let span = tracer.start(
"GET /orders",
at=now(),
)
span.finish(at=now())
}
A sink
Where a finished span goes. A recorder for a replay, a queue for a service, nothing at all for a program that only wants trace ids for its logs.
struct Printer {}
impl @trace.Sink for Printer with fn accept(_, span) {
println("\{span.name} took \{span.duration()} ns")
}
The index.
Three packages and a facade. Import the package that owns the type you construct: a re-exported type arrives without its constructors. The complete reference, down to the last derived method, is on the API plate.