OpenTelemetry-aligned trace/span model for Standard ML, with W3C
traceparent codec and OTLP/JSON export. No I/O, no wall clock, no
threads -- a Tracer is a pure accumulator {clock, spans, seed}
threaded through startSpan/endSpan, and identifiers are derived
deterministically from the vendored sml-prng SplitMix64.
Part of the sjqtentacles monorepo of SML libraries. It builds on
sml-prng (vendored) for
identifier generation and
sml-json (vendored, which
itself vendors sml-parsec) for OTLP/JSON export.
- TraceId (16 bytes) / SpanId (8 bytes):
generate : seed -> id * seed,toHex,fromHex. Hex output is lowercase;fromHexaccepts any case. - Span:
{traceId, spanId, parentSpanId, name, kind, attributes, events, status, startTime, endTime option}.start,finish,addAttr,addEvent,setStatus. Timestamps (startTime,endTime, eventtime) and the tracer clock areIntInf.int, so a real Unix nanosecond count (~1.7e18) is stored and exported losslessly. - Tracer: pure accumulator.
startSpanpushes a span (inheriting the current traceId/parent when the stack is non-empty, starting a fresh trace otherwise);endSpanpops and records it. - TraceContext: W3C
traceparentcodec.encode : traceId * spanId * flags -> stringproduces00-<32 hex>-<16 hex>-<2 hex>;decodevalidates version, lengths, hex, and rejects all-zero IDs. - OtlpExport:
toJson : span list -> Json.jsonproduces the OTLP/JSONResourceSpansstructure with aservice.nameresource, a single scope, and one entry per span carryingtraceId,spanId,parentSpanId,name,kind,startTimeUnixNano,endTimeUnixNano,attributes,events, andstatus.
Complete and tested. The model is the pure "tracer + logical clock" shape; wiring it to real I/O (OTLP/HTTP export, wall-clock timestamps) is the caller's job.
sml-prng(vendored atlib/github.com/sjqtentacles/sml-prng/)sml-json(vendored atlib/github.com/sjqtentacles/sml-json/, which vendorssml-parsec)- Standard ML Basis only -- no FFI, no threads.
Pure Standard ML. Verified on MLton and Poly/ML, with identical, deterministic output across both.
Both compilers use a fixed-width default int (MLton 32-bit, Poly/ML
63-bit) -- neither is arbitrary precision. Timestamps and integer JSON
numbers therefore use IntInf.int (arbitrary precision on both), so a
Unix nanosecond timestamp serializes to the same exact digits on each and
never overflows MLton's 32-bit int.
(* Seed the tracer for a deterministic identifier sequence. *)
val tr0 = Trace.init 0w123
(* Start a root span; a fresh TraceId is derived from the seed. *)
val (tr1, rootSid) =
Trace.startSpan (tr0, { name = "GET /", kind = Trace.Server, parent = NONE })
(* A child span inherits the root's traceId and parentSpanId. *)
val (tr2, _) =
Trace.startSpan (tr1, { name = "db.query", kind = Trace.Client, parent = NONE })
val (tr3, _) = Trace.endSpan (tr2, Trace.Ok) (* pop child *)
val (tr4, _) = Trace.endSpan (tr3, Trace.Ok) (* pop root *)
(* W3C traceparent for the root. *)
val tp = Trace.TraceContext.encode
(#traceId (valOf (Trace.currentSpan tr1)),
rootSid, 0w1)
(* "00-<32 hex>-<16 hex>-01" *)
(* OTLP/JSON export of the finished spans. *)
val otlp = Trace.OtlpExport.toJson (Trace.finishedSpans tr4)make test # build + run the suite under MLton (default)
make test-poly # run the suite under Poly/ML
make all-tests # run under both
make clean- TraceId: hex round-trip (generated and known vectors); 32-char lowercase
hex;
fromHexrejects bad length and bad chars. - SpanId: hex round-trip; 16-char hex; known vector.
- TraceContext:
encodematches the W3C example (00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01);decoderecovers all fields; round-trip with flags=0; rejects all-zero traceId/spanId and bad length. - Span lifecycle:
startproduces an unfinished span;addAttr/addEventaccumulate;finishsetsendTimeand promotesUnsettoOk. - Parent-child nesting: a child started under a root inherits the root's
traceIdand hasparentSpanId = root.spanId;endSpanpops the stack;finishedSpansreturns spans oldest-start-first. - OTLP JSON: one
resourceSpansentry with aservice.nameresource and a single scope; two spans; the root hasname = "root"; the child hasparentSpanId. - Large integers: a realistic Unix nanosecond timestamp (~1.7e18, past
MLton's 2^31
intceiling) used asstartTime/endTime, plus a large integer attribute, serialize to their exact decimal digits with no overflow or truncation on either compiler. - Determinism: same seed -> same
TraceId/SpanIdsequence; same-seedTracerproduces the same spanId sequence.
smlpkg add github.com/sjqtentacles/sml-trace
smlpkg syncThen reference the library basis from your own .mlb:
lib/github.com/sjqtentacles/sml-trace/sml-trace.mlb
For Poly/ML, use the sources listed in sources.mlb in order (the
vendored sml-prng, then sml-json + sml-parsec, then trace.sig
and trace.sml).
MIT. See LICENSE.