moontel OpenTelemetry trace API · W3C Trace Context · MoonBit OpenTelemetry · MoonBit GitHub

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.

— ns — spans — levels — dropped
01 / 05

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.

Rest the pointer on a span.
02 / 05

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

version2 characters
trace id32 characters · one per trace
span id16 characters · one per hop
flags2 characters · bit 0 is sampled

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.

03 / 05

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.

— of — sampled

— requests · each a trace id this library sampled for real

04 / 05

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")
}
05 / 05

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.

trace — spans, samplers, the sink seam

Tracer::new(rand~, sink~, sampler~, limits?) → Tracer
Tracer::start(name, parent?, kind?, at~, attributes?, links?) → Span raise
Span::finish(at~) → Unit
Span::set_attribute(key, Value) → Unit
Span::set_attributes(Array[(String, Value)]) → Unit
Span::add_event(name, attributes?, at~) → Unit
Span::record_exception(message~, at~, type_?, stacktrace?, attributes?) → Unit
Span::set_status(Status) → Unit
Span::set_name(String) → Unit
Span::add_link(Link) → Unit
Span::context() → TraceContext
Span::is_recording, is_ended() → Bool
SpanDatacontext, parent, name, kind, start, end, attributes, events, links, status, dropped
SpanData::sampled, duration() → Bool, () → Int64
TraceContexttrace_id, span_id, flags, state
TraceContext::to_traceparent() → String
TraceContext::sampled, is_valid() → Bool
TraceState::parse(StringView) → TraceState raise
TraceState::to_header, get, insert, deleteread a member, add one closest to this hop, drop one
Samplershould_sample(SampleRequest) → Decision · description() → String
Sinkaccept(SpanData) → Unit
always, never, ratio, parent_basedthe four samplers
Recorder, Discardspans in memory · spans thrown away
Limitsattributes, events, links, attributes_per_event, attribute_value_length
ValueBool · Int · Double · Str · and the four typed arrays
Status, SpanKind, DecisionUnset / Ok / Error · Internal / Server / Client / Producer / Consumer
RefusedField · NotHex · Zero · TooMany · TooLong · Ids

propagate — the W3C headers

extract(Array[(String, String)]) → Extracted
inject(Array[(String, String)], Extracted) → Array[(String, String)]
parse(traceparent, tracestate?) → TraceContext raise
parse_traceparent, parse_tracestatethe fields, and the refusal
Baggagemembers · parse · to_header · get · len · add
Extracted, Member, Propertyone hop in and out · a bag entry · its metadata
traceparent_header, tracestate_header, baggage_headerthe three names, lowercase

asgi — the middleware

Tracing::new(tracer~, now~, name?, baggage?, ignore?) → Tracing
Tracing::wrap(AsgiApp) → AsgiApp, and Layer for stacking
method_and_path(http_method, path) → String