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.
The current span
Section titled “The current span”current is the span this fiber is inside, and it is the propagation the
paragraph above promises. around installs a span for the duration of a
body and puts back the enclosing one on every path out, including a raise
and a cancellation; a handler’s start reads it and takes the trace id, the
sampling decision and the parent from it.
A fiber inherits its spawner’s current span, copied at the moment the child is created, so a request that fans out into four fibers is one trace rather than five. A copy rather than a share: what a child opens is the child’s, and a slot both could write would leave the spawner holding a span it never entered.
Why ambient at all, when the rest of this language is about saying what you
use: an effect operation runs where the effect is performed, not where the
handler was installed, so start cannot see the around above it on the
stack and cannot require a capability of its own. Threading the span through
every signature was the alternative, and it is the one that makes a library
untraceable unless every function in it agrees to carry a parameter it has
no use for.
This did not work until it did. For as long as this paragraph has been
here, Span::parent was written 0 at every call site in the repository:
a nested around began a second trace, and the OTLP exporter carried a
comment saying so. The claim was the design and not the behavior.
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 · current · 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 = { pub name: String, pub 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 = { pub trace_high: Int, pub trace_low: Int, pub span: Int, pub 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”pub sampled: BoolWhether 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 = { pub context: Context, pub parent: Int, pub name: String,};A span in progress.
parent
Section titled “parent”pub parent: IntThe 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 Contexttrace_id
Section titled “trace_id”pub fn trace_id(self) -> StringThe trace id, as thirty-two lower-case hex digits.
The form every collector indexes and every log line carries, and the reason it is here rather than assembled by each caller: a trace id written as two decimal integers cannot be pasted into the search box of anything.
span_id
Section titled “span_id”pub fn span_id(self) -> StringThe span id, as sixteen lower-case hex digits.
pub 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 canceled leaves the
finalizer to close it, and it closes as Failed because that is what
happened.
raises 'er is the other half: it carries the body’s own failures out,
and a body that does no fallible work instantiates the row empty and needs
no ! at the call. The row has nothing to do with cancellation — a body is
canceled at its loops, blocking calls and calls whatever its row, and the
span closes as Failed on the way out.
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.
current
Section titled “current”pub fn current() -> Option<Context>The span this fiber is inside, or None at the top.
A handler’s start is expected to call this, and it is the whole of
what a handler has to do to produce a trace rather than a pile of unrelated
one-span traces: take the trace id and the parent from what comes back, or
begin a fresh trace when nothing does.
start: fn (name, _attributes) => match current() { Option::Some(inside) => { context: { trace_high: inside.trace_high, trace_low: inside.trace_low, span: random.int(), sampled: inside.sampled }, parent: inside.span, name: name, }, Option::None => { ... a new trace ... },}Why an operation has to ask rather than be told
Section titled “Why an operation has to ask rather than be told”An effect operation runs where the effect is performed, not where the
handler was installed, so it cannot see the around above it on the stack
and cannot require a capability of its own. The enclosing span therefore has
to be somewhere ambient, and the only thing that survives what a span has to
survive — a spawn, a steal, a wake and a cancellation — is the fiber. This
reads a slot the runtime keeps per fiber, which around sets for the
duration of a body and puts back on every path out.
What it does across a spawn
Section titled “What it does across a spawn”A fiber inherits its spawner’s current span at the moment it is created, so work that fans out stays one trace. It inherits a copy: a span the child opens afterwards is the child’s, and does not become the parent’s when the child returns.
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.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource