Declares the span that encloses a session's macrostep spans, for a durable driver this package knows nothing about.
Why it exists
A statifier.macrostep span is the root of its own trace, stitched to
its neighbours with links (ADR-0003 decision 8). The one exception is
the family's own durable stepper: with
OpentelemetryStatifier.Persistence attached, a macrostep driven
inside a statifier_persistence.run.step span nests inside it, so a
durable run reads as one tree (ADR-0004 decision 4).
That nesting was never really about statifier_persistence. It is
about a durable stepper: a host with its own storage, its own lock
and its own step span wants the same tree, and had no way to ask for
it. This module is the way to ask. It generalises ADR-0004 decision 4
from the family's durable stepper to any durable stepper, and nothing
else about the bridge changes: a host that never calls register/2
gets byte-identical behaviour, and the family's own persistence path
is untouched whether this API is used or not.
What it is not
It is not a way to pick up the process's ambient OTel context. The bridge still never reads it, and still never attaches anything to it (ADR-0003 decision 8). A declaration is an explicit hand-in: the host names the span it means, and that is precisely what makes it safe where reading the ambient context would not be. A host with an unrelated HTTP request span open does not silently capture every macrostep into it; a host that wants its step span to be the parent says so.
It is also not a way to write onto the declared span. The bridge parents spans under it and nothing more - no attributes, no span events, and no ending it. The span's lifetime stays entirely the host's.
Using it
The scoped form is the one to reach for: it withdraws the declaration on the way out, including when the block raises.
require OpenTelemetry.Tracer
OpenTelemetry.Tracer.with_span "my_app.workflow.step" do
OpentelemetryStatifier.Parent.within(
OpenTelemetry.Tracer.current_span_ctx(),
fn ->
# every statifier.macrostep span this process drives in here
# nests inside my_app.workflow.step
MyApp.DurableStepper.drive(session_id, event)
end
)
endUse register/2 and unregister/1 directly when the enclosing span
outlives one function call - a GenServer that opens a step span in
one callback and closes it in another, say:
{:ok, registration} = OpentelemetryStatifier.Parent.register(span_ctx)
# ...
:ok = OpentelemetryStatifier.Parent.unregister(registration)Which spans nest
A declaration is keyed on the process whose spans nest under it,
which defaults to self() - the shape a driver that steps the session
synchronously in its own process wants. A driver that steps a session
in another process passes pid: session_pid.
Declarations compose with the bridge's own sibling spans and with each
other: the innermost declaration or open sibling span at the moment a
macrostep starts is its parent, so a host that runs both its own
stepper and statifier_persistence gets the tree it would expect.
When the registrant dies
A registration is withdrawn by the process that made it. If that
process dies without withdrawing - a driver crashing mid-step - the
declaration is abandoned: nobody is left to end the declared span, so
nesting under it would attach live spans to a trace that will never
close. Macrostep spans fall back to rooting their own traces from the
moment the registrant dies, and OpentelemetryStatifier.SpanTable's
sweep deletes the row without ending the declared span, because this
bridge did not open it.
A register/2 that cannot record the declaration returns
{:error, reason} rather than raising: :invalid_span for anything
that is not a valid, non-zero span context (:undefined from a
current_span_ctx/0 with no span open is the common one),
:registrant_not_alive for a :pid that is already gone, and
:no_span_table when the bridge was never set up. Telemetry never
breaks the host, so within/3 runs its block either way and simply
does not nest.
Summary
Types
Why a declaration was refused. Every one of them is an ordinary answer the caller can act on, not an exception.
An opaque handle to one live declaration. Hand it to unregister/1;
nothing else reads it.
Functions
Declares span_ctx as the enclosing span for the macrostep spans a
process drives from now until unregister/1.
Withdraws the declaration register/2 returned.
Runs fun with span_ctx declared as the enclosing span, and
withdraws the declaration afterwards - including when fun raises,
throws, or exits.
Types
@type error() :: :invalid_span | :registrant_not_alive | :no_span_table | {:unknown_options, [atom()]}
Why a declaration was refused. Every one of them is an ordinary answer the caller can act on, not an exception.
@opaque registration()
An opaque handle to one live declaration. Hand it to unregister/1;
nothing else reads it.
Functions
@spec register( OpenTelemetry.span_ctx(), keyword() ) :: {:ok, registration()} | {:error, error()}
Declares span_ctx as the enclosing span for the macrostep spans a
process drives from now until unregister/1.
Options:
:pid- the process whose spans nest underspan_ctx. Defaults toself(), which is what a driver stepping the session in its own process wants.:table- the span table, for a host that passed:tabletoOpentelemetryStatifier.setup/1. Defaults toOpentelemetryStatifier.SpanTable.
Returns {:ok, registration}, or {:error, reason} - see the
moduledoc's list; the caller is never left guessing whether the
declaration took.
Examples
iex> OpentelemetryStatifier.Parent.register(:undefined)
{:error, :invalid_span}
@spec unregister(registration()) :: :ok
Withdraws the declaration register/2 returned.
Idempotent, and safe from any process: withdrawing a declaration that
is already gone - swept after its registrant died, or withdrawn once
already by an after block that ran twice - is :ok. The declared
span is never ended here; the host that opened it owns its lifetime.
@spec within(OpenTelemetry.span_ctx(), (-> result), keyword()) :: result when result: var
Runs fun with span_ctx declared as the enclosing span, and
withdraws the declaration afterwards - including when fun raises,
throws, or exits.
Returns whatever fun returns. A declaration that could not be
recorded is not an error here: instrumentation does not decide
whether a host's work runs, so fun is called either way and its
macrostep spans simply root their own traces. Call register/2
directly when you want to see the reason.
Takes the same options as register/2.