Skip to content

std/tracing: OpenTelemetry-compatible observability (tracing, metrics, logs) - #13

Merged
code-by-sia merged 9 commits into
mainfrom
tracing
Aug 2, 2026
Merged

std/tracing: OpenTelemetry-compatible observability (tracing, metrics, logs)#13
code-by-sia merged 9 commits into
mainfrom
tracing

Conversation

@code-by-sia

Copy link
Copy Markdown
Owner

Adds an OpenTelemetry-compatible observability suite as pure Xi, alongside std/monitoring and off until enabled. No new C or runtime code: ids come from crypto.randomHex, timing from time.nowNanos, export over std/http, payloads via std/json, correlation through the injected Logger.

Every mechanic is a bindable interface, so a program can replace any of them (the Tracer itself, the exporter, sampler, id generator, clock, propagator, config, meter) with a bind.

What is included (all stages)

  • Tracing core — spans with a trace/span identity, automatic parent-child nesting via a current-span stack, attributes, events, status; buffered export; console and in-memory exporters.
  • W3C propagationW3CPropagator inject/extract of the traceparent header; startChild(remoteContext, ...) continues a trace across a process boundary. std/http gains requestWith(..., extraHeaders) (a behavior-preserving refactor) to carry the header outbound.
  • OTLP/HTTP exporter — POSTs OTLP/JSON to <endpoint>/v1/traces (the format a Collector, Jaeger or Tempo reads), following the OTLP conventions (kind/status ints, string-encoded 64-bit times, typed attribute values). Opt-in; endpoint/service name from a typed TracingConfig.
  • SamplingRatioSampler (stable per-trace, ratio from config) and AlwaysOffSampler, opt-in; the default AlwaysOnSampler stays auto-injected.
  • BridgesTracingMonitoring surfaces the span count in the std/monitoring report under tracing; WebTracer brackets an HTTP handler with a server span (continues an inbound trace, sets HTTP semantic-convention attributes).
  • LogsTraceLog stamps trace_id/span_id onto log lines while a span is active.
  • Metrics — a Meter with counter, up-down counter, gauge, and histogram instruments under std/monitoring/instruments.xi, surfaced in the monitoring report under metrics.

Design and staged rationale: docs/tracing.md.

Also in this PR

  • Compiler fix: if let ... else ran the else branch unconditionally (codegen emitted else; plus a stray block, so the else body always ran; most visible with compound-typed optionals). Fixed in compiler/impl/codegen/stmt.xi with a regression test. General fix, not tracing-specific.

Verification

  • 23 tests across 9 files (tracing, propagation, OTLP encoding, sampling, monitoring bridge, logs, metrics, pluggability, and the if-let regression) pass.
  • The self-hosting fixpoint holds (gen2 == gen3), and all 37 existing test files still pass (no regression from the compiler and std/http changes).

Adds the tracing signal of OpenTelemetry as pure Xi, alongside std/monitoring
and off until enabled. A Tracer records spans with a trace/span identity,
parent-child nesting via a current-span stack, attributes, events, and status;
finished spans are buffered and handed to a SpanExporter on flush.

Every mechanic is an interface, so a program replaces any of them with a bind:
the Tracer itself, the SpanExporter, the Sampler, the IdGenerator, or the
TraceClock. Defaults are a random id generator, a system clock, an always-on
sampler, and a console exporter; an in-memory exporter ships for tests.

Needs no new C or runtime code: ids come from crypto.randomHex, timing from
time.nowNanos, output through the injected Logger. Includes a runnable example
and tests for nesting, attributes/events, error recording, and pluggability.
Design and staged roadmap in docs/tracing.md.
genIf handled the else clause for a plain if but not for if let: the if-let
path emitted only 'if ((x).has_value) { ... }' and returned, so a trailing
else { ... } was parsed as a separate statement and lowered to 'else;' plus an
unconditional block. The else body then ran even when the optional was present
(most visible with compound-typed optionals). Mirror the plain-if else handling
in the if-let path, with the bound name in scope only in the then-branch.
Regression test in examples/language/iflet_else_test.xi.
A W3CPropagator injects a SpanContext into a traceparent header
(00-<trace>-<span>-<flags>) and extracts one back, returning none for an absent
or malformed header (wrong length, bad separators, non-hex or all-zero ids).
startChild(remoteContext, ...) continues a trace across a process boundary, so a
server span parents to the caller's span and shares its trace id.

std/http gains requestWith(..., extraHeaders), a behavior-preserving refactor
that shares the transport with request, so an outgoing call can carry the
injected traceparent. Tests cover round-trip, rejection, and cross-service
parenting.
OtlpHttpSpanExporter POSTs OTLP/JSON to <endpoint>/v1/traces, the wire format an
OpenTelemetry collector, Jaeger or Tempo reads. The encoder builds the
resourceSpans/scopeSpans/spans envelope and follows OTLP/JSON conventions: span
kind and status as ints, 64-bit times as strings, attribute values wrapped in a
typed { stringValue } envelope. Endpoint and service name come from a
TracingConfig (DefaultTracingConfig by default, or bind readConfig).

The exporter is opt-in (import std/tracing/exporter/otlp.xi and bind it), so a
plain app keeps the console exporter and no HTTP dependency. Encoder tests assert
the shape and conventions by navigating the Json, no live collector needed.
RatioSampler keeps roughly the configured fraction of traces, deciding from the
trace id's first 16 bits so the choice is stable across a whole trace and every
service agrees; the ratio comes from TracingConfig.sampleRatio. AlwaysOffSampler
keeps tracing structurally on while emitting nothing. Both are opt-in (import
std/tracing/sampling.xi and bind), so the default AlwaysOnSampler still
auto-injects. The pure ratioDecision is unit-tested for the full/none edges,
per-trace stability, and the threshold.
TracingMonitoring implements Monitoring, so importing it surfaces the
finished-span count and enabled flag in the std/monitoring report under
'tracing' (the same list-injected way WebMonitoring adds 'web'); neither module
depends on the other's internals.

WebTracer brackets an HTTP handler with a server span: begin continues an
inbound trace from the traceparent header (or starts a root) and tags the span
with http.request.method and url.path; finish records
http.response.status_code and the status. A handler injects WebTracer and calls
begin/finish around its work, since Xi web dispatch is where-guarded handlers
rather than a middleware chain.
Adds currentTraceId/currentSpanId to the Tracer (the innermost active span, or
"") and a TraceLog that wraps the injected Logger to append trace_id/span_id
to each line while a span is active. TraceLog is a distinct interface rather than
a Logger decorator, so it does not depend on itself; the pure correlate() is
tested, and the accessors are shown tracking span nesting.
Adds a Meter with the OpenTelemetry instrument types: counter, up-down counter,
gauge, and histogram (count/sum/min/max). It is the metrics signal, under
std/monitoring rather than std/tracing; MetricsMonitoring surfaces the numbers in
the monitoring report under 'metrics'. Instruments are kept in a flat list keyed
by (name, kind) because a Map cannot yet be class state. Tests cover each
instrument and the snapshot.
@code-by-sia
code-by-sia merged commit c80b6d1 into main Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant