OpentelemetryStatifier.Sibling (OpentelemetryStatifier v0.5.0)

Copy Markdown View Source

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 :start and closed on the :stop whose span_ref matches. Only statifier_persistence's step seam has one; sob-ADR-0006 decided 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 duration measurement ([:statifier_persistence, :adapter, :call] and [..., :run, :lock]). Its start is back-calculated as now - 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_oban contract.

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. A list of hosts is tried in order and the first open span wins.

Types

host()

@type host() :: {:process, pid()} | {:session, String.t() | nil} | :detached

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.

A list of hosts is an ordered fallback: the first member with an open span wins, and a list every member of which misses misses exactly as a single host would. It is what a sibling family whose correlation key is not always a session id needs. statifier_oban's scope is the session id under the family's own stepper and a host's durable run id under a durable driver, so its scheduling seam asks for the session first and for the calling process second - the durable driver's step span is open right there, and the pid check the session shape carries is what makes the second ask safe.

Functions

caller_context_links(arg1)

@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.

close_span(config, span_ref, end_time, attributes)

@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.

interval_span(config, name, duration, attributes)

@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.

mapping(prefix, correlation_key)

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.

name(event)

@spec name([atom()]) :: String.t()

The span (or span-event) name for an event: its segments joined with dots, so span-name cardinality is exactly one per event name.

open_span(config, name, span_ref, pid, start_time, attributes)

@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.

parent_ctx(table, 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.

point(config, name, host, attributes, links)

@spec point(OpentelemetryStatifier.Config.t(), String.t(), host() | [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. A list of hosts is tried in order and the first open span wins.