std::trace
Spans, and the context that has to survive a fiber changing worker.
A service that cannot be traced is not a candidate, and docs/positioning.md
claims Khora should be one wherever a team compares Go, backend TypeScript
or application Rust. Nothing in std emitted so much as a log line before
this module.
Why this is std’s and the exporter is not
Section titled “Why this is std’s and the exporter is not”The observability design note argues it in full and the short version is
ecosystem.md’s rule: what belongs here is the layer everyone would
otherwise re-derive subtly wrong, and for tracing that is two things.
The vocabulary, because two packages that disagree about what a span is cannot compose into one trace. And propagation — a span’s parent surviving a spawn, a steal, a wake and a cancellation — which is not a library concern at all: it is a property of the scheduler, and it is exactly where every other ecosystem’s tracing leaks.
What is not here: OTLP, Datadog’s protocol, Prometheus, batching, retry,
and every sampling policy beyond a head sampler. All of it is a vendor’s
release cadence attached to a network protocol, which is the same argument
that keeps Postgres out of std.
The model is OpenTelemetry’s, and that is a one-way decision
Section titled “The model is OpenTelemetry’s, and that is a one-way decision”Datadog spans are a service, a resource and flat string tags; OpenTelemetry spans carry typed attributes, events and a status. Rendering OTel into Datadog loses information and the reverse cannot be done at all — which is why Datadog ships an OTel exporter and not the other way round. So the model is the superset and the narrower vendor drops what it cannot hold.
What makes this cheap in Khora specifically
Section titled “What makes this cheap in Khora specifically”A capability is an interception point. Instrumenting one is another handler:
fn traced(inner: FsRead, tracer: Tracer) -> FsRead { handler for FsRead { read: fn path => around(tracer, "fs.read", fn () => inner.read(path)), .. }}Every existing caller of FsRead is instrumented and none of them
changed.
That is what OpenTelemetry achieves with bytecode agents, and here it falls
out of capability-passing because that is what a capability is.
API at a glance
Section titled “API at a glance”Types: Value · Attribute · Status · Context · Span
Effects: Tracer
Trait implementations: Eq for Context · Show for Context
Functions: around · around_result · text · number · flag
pub type Value = | Text(String) | Number(Int) | Decimal(Float) | Flag(Bool);What an attribute can hold.
The smallest thing that matches OpenTelemetry’s AnyValue without
inheriting its variance. A vendor that only has strings renders these; one
that has types keeps them.
Attribute
Section titled “Attribute”pub type Attribute = { name: String, value: Value,};A named value on a span.
Status
Section titled “Status”pub type Status = | Ok | Failed(String);How a span ended.
| OkNothing went wrong, which is the common case and carries nothing.
Failed
Section titled “Failed”| Failed(String)Something did, and this is what it was.
A string rather than a typed error, because a span crosses into a protocol that has no idea what Khora’s rows are. The typed error stays where it was raised; this is its rendering.
Context
Section titled “Context”pub type Context = { trace_high: Int, trace_low: Int, span: Int, sampled: Bool,};A trace and a span within it, in the shape the wire format uses.
Two Ints for the trace id because W3C’s is 128 bits and Khora’s
integer is 64. Naming the halves is better than a string that has to be
parsed at every hop.
sampled
Section titled “sampled”sampled: Bool
Whether this trace is being recorded. Decided once, at the root, and carried unchanged — a trace that is half sampled is not a trace.
pub type Span = { context: Context, parent: Int, name: String,};A span in progress.
parent
Section titled “parent”parent: Int
The span this one is inside, or zero at the root.
Effects
Section titled “Effects”Tracer
Section titled “Tracer”pub effect Tracer { start: (String, List<Attribute>) -> Span, finish: (Span, Status) -> (), event: (Span, String, List<Attribute>) -> (),}Starting, ending and annotating spans.
Monomorphic on purpose. A scoped around would be generic in the
body’s result, and an effect field that is itself polymorphic is a rank-2
type — a different feature from ordinary generics, and one
observability.md flagged as an open question. It is not needed: around
below is an ordinary generic function that takes the tracer as an
argument, which gets the scoping without the type system change.
start: (String, List
Begins a span inside whatever is current.
finish
Section titled “finish”finish: (Span, Status) ->()
Ends it. Idempotent is not promised; end a span once.
event: (Span, String, List
Something that happened during the span, with no duration of its own.
Methods
Section titled “Methods”Tracer
Section titled “Tracer”impl Tracerpub fn none() -> TracerA tracer that records nothing, as cheaply as possible.
The default, and it has to be nearly free. If tracing costs when it is off it gets turned off, and then it does not exist. This allocates one span value with a zero context and does nothing else — no attributes are read, no strings are built, and with Perceus the list the caller passed is released at the call.
Context
Section titled “Context”impl Contextpub fn none() -> ContextA context that names no trace.
is_valid
Section titled “is_valid”pub fn is_valid(self) -> BoolWhether this context refers to a trace at all.
W3C says an all-zero trace id or span id is invalid, which is what makes zero usable as “none” without a separate flag.
to_traceparent
Section titled “to_traceparent”pub fn to_traceparent(self) -> StringThe traceparent header for this context.
00-{32 hex}-{16 hex}-{2 hex}, which is version zero of the W3C trace
context — a stable, tiny, vendor-neutral spec, which is why it is in
std when the exporters are not.
of_traceparent
Section titled “of_traceparent”pub fn of_traceparent(header: String) -> Option<Context>The context a traceparent header names, or None if it does not.
Refuses anything it does not fully understand, including a version it has not heard of. A header half-read is a trace joined to the wrong parent, which is worse than a trace that starts fresh.
Trait implementations
Section titled “Trait implementations”Eq for Context
Section titled “Eq for Context”impl Eq for Contextfn eq(self, other: Context) -> BoolShow for Context
Section titled “Show for Context”impl Show for Contextfn show(self) -> StringFunctions
Section titled “Functions”around
Section titled “around”pub fn around<A, 'ef, 'er>(tracer: Tracer, name: String, body: () -> A with 'ef raises 'er) -> A with 'ef raises 'erRuns body inside a span, and ends the span whichever way it leaves.
The scoped form, as a plain function rather than a field on the effect, so that no rank-2 type is needed. Takes the tracer explicitly because that is what makes it ordinary.
Whichever way it leaves
Section titled “Whichever way it leaves”Three ways out, and the two that matter are the ones a let and a call
cannot see. Written as start, body(), finish in a row, a span would
be finished exactly when nothing goes wrong — and a span closed only on the
happy path is worse than no span, because a trace with a dangling parent is
read as still running.
So the finish is registered with a region before the body starts, the same
way std::db::transaction registers its rollback, and for the same reason:
a region’s finalizers run on every path out because every path out already
releases the binding holding the region. A body that returns marks the span
Ok and settles it; a body that raises or is cancelled leaves the
finalizer to close it, and it closes as Failed because that is what
happened.
raises 'er is the other half. The effect-runtime design note §6: a
cancellation point is a ! in a function that can raise, so a body with no
row has none, nothing inside the span can be interrupted, and no frame here
could carry the interruption if it were. A body that does no fallible work
instantiates the row empty and needs no ! at the call.
around_result
Section titled “around_result”pub fn around_result<A, E: Show, 'ef, 'er>(tracer: Tracer, name: String, body: () -> Result<A, E> with 'ef raises 'er) -> Result<A, E> with 'ef raises 'erThe same, for a body that reports its failure as a value.
The difference from around is what reaches the span’s status: this reads
the Result and puts the error’s own text on it, where around can only
say that the body did not return. Reach for this when the failure has
something to say, which is most of the time.
pub fn text(name: String, value: String) -> AttributeA Text attribute, which is most of them.
number
Section titled “number”pub fn number(name: String, value: Int) -> AttributeA Number attribute.
pub fn flag(name: String, value: Bool) -> AttributeA Flag attribute.
Khora 0.1.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource