The span mechanics the two sibling families share, so
OpentelemetryStatifier.Persistence.Handler and
OpentelemetryStatifier.Oban.Handler decide only what their own
contracts decide - which event is an interval, which is a point, and
where a point belongs - and nothing about how a span is opened.
Three shapes cover both contracts (ADR-0004):
- A paired span, opened on a
:startand closed on the:stopwhosespan_refmatches. Onlystatifier_persistence's step seam has one;sob-ADR-0006decided against pairs on the grounds that Oban already owns every interval it could bracket. - An interval span, for a point-in-time event that nonetheless
carries a
durationmeasurement ([:statifier_persistence, :adapter, :call]and[..., :run, :lock]). Its start is back-calculated asnow - duration, the contrib family's usual shape, because the event reports an interval that has already closed. - A point, for everything else. It lands as a span event on the
bridge span open around it when there is one, and becomes its own
zero-duration span when there is not - a timer firing on an Oban
worker days later has nothing of this bridge's open around it, and
dropping it would lose the single most useful fact in the
statifier_obancontract.
Nesting is through this bridge's own table, never the process's
ambient OTel context. ADR-0003 decision 8's rule that the bridge
neither reads nor writes the ambient context is unchanged; what
ADR-0004 adds is that a span this bridge itself has open in this
process parents the spans that follow it. That is what produces the
topology statifier_persistence's docs/telemetry.md describes -
adapter call inside step, durable macrostep inside step - without the
bridge ever touching a host's context.
A span a host declares through
OpentelemetryStatifier.Parent.register/2 parents the same way and for
the same reason: it was handed in explicitly rather than read off the
process, which is what keeps ADR-0003 decision 8 intact while letting a
durable driver this package has never heard of get the topology the
family's own stepper gets.
Summary
Types
Where a point event looks for a span to land on: the calling process's innermost open sibling span, or a logical session's innermost open macrostep span - the latter only when that session's macrostep is open in this very process, so a delivery-seam event on an Oban worker never writes onto a span another process holds for the same scope.
Functions
The link a caller_context yields, or [].
Closes the paired span under span_ref, setting attributes first.
A span_ref with no open span is contract-legal (the sweep ends the
orphans a dead process leaves) and closes nothing.
Records an already-closed interval of duration native time units as
its own span named name, nested in whatever sibling span is open in
the calling process.
The attribute mapping for one sibling family: prefix as the
namespace, and correlation_key (session_id here, scope there)
aliased onto the shared statifier.session_id - the one attribute
that joins a step, a timer and a macrostep across all three families.
The span (or span-event) name for an event: its segments joined with dots, so span-name cardinality is exactly one per event name.
Opens a paired span named name under span_ref, starting at
start_time, nested in whatever sibling span is open in pid.
The context a span opened in pid right now should start from: the
innermost span this bridge has recorded as enclosing that process, or
an empty context when it has none.
Records a point-in-time event named name: a span event on the bridge
span host names when one is open, and its own zero-duration span
linked to links when none is.
Types
Where a point event looks for a span to land on: the calling process's innermost open sibling span, or a logical session's innermost open macrostep span - the latter only when that session's macrostep is open in this very process, so a delivery-seam event on an Oban worker never writes onto a span another process holds for the same scope.
:detached asks for neither: it is the shape for an event this bridge
knows is running somewhere it has nothing open - inside an Oban job,
days after the macrostep that armed it - where the span it becomes is
a root linked to the arming trace rather than a child of anything.
Functions
@spec caller_context_links(term()) :: [OpenTelemetry.link()]
The link a caller_context yields, or [].
st-ADR-0063 makes the slot an opaque host term and both sibling
contracts carry it without reading it; the OTel-shaped reading of it
lives here and nowhere else. A host that stamped the W3C text form -
%{"traceparent" => "00-<trace>-<span>-<flags>"}, the form
statifier_oban's docs/telemetry.md tells hosts to write because the
row outlives the node - links to the trace that armed the timer. Any
other shape links to nothing: nil is the ordinary detached case, and
a term this bridge cannot read is not an error either. The link is
never a parent - parenthood would hold the arming trace open for the
length of the delay.
@spec close_span(OpentelemetryStatifier.Config.t(), reference(), integer(), map()) :: :ok
Closes the paired span under span_ref, setting attributes first.
A span_ref with no open span is contract-legal (the sweep ends the
orphans a dead process leaves) and closes nothing.
@spec interval_span(OpentelemetryStatifier.Config.t(), String.t(), integer(), map()) :: :ok
Records an already-closed interval of duration native time units as
its own span named name, nested in whatever sibling span is open in
the calling process.
@spec mapping(String.t(), atom()) :: OpentelemetryStatifier.Attributes.mapping()
The attribute mapping for one sibling family: prefix as the
namespace, and correlation_key (session_id here, scope there)
aliased onto the shared statifier.session_id - the one attribute
that joins a step, a timer and a macrostep across all three families.
monotonic_time and system_time are dropped: they are the clock
plumbing every family's events carry so a bridge can place a span, and
the span's own start and end timestamps are where they land. duration
is kept, because the sibling contracts publish it as a number a host
reads without any tracing at all.
The span (or span-event) name for an event: its segments joined with dots, so span-name cardinality is exactly one per event name.
@spec open_span( OpentelemetryStatifier.Config.t(), String.t(), reference(), pid(), integer(), map() ) :: :ok
Opens a paired span named name under span_ref, starting at
start_time, nested in whatever sibling span is open in pid.
@spec parent_ctx(atom(), pid()) :: OpenTelemetry.Ctx.t()
The context a span opened in pid right now should start from: the
innermost span this bridge has recorded as enclosing that process, or
an empty context when it has none.
This is the whole of ADR-0004's nesting mechanism, and it is why the
bridge still never reads the process's ambient OTel context. Two kinds
of span can parent another here, and neither is "whatever happens to be
ambient": one this bridge itself opened in this process from a sibling
package's :start event, and one a host declared through
OpentelemetryStatifier.Parent.register/2, which is the sanctioned
door a foreign durable driver uses to get the same topology
(ADR-0004 decision 4's 2026-09-02 Notes).
OpentelemetryStatifier.SpanTable.fetch_enclosing_ctx/2 picks the
innermost of the two.
@spec point(OpentelemetryStatifier.Config.t(), String.t(), host(), map(), [ OpenTelemetry.link() ]) :: :ok
Records a point-in-time event named name: a span event on the bridge
span host names when one is open, and its own zero-duration span
linked to links when none is.