# `OpentelemetryStatifier.Sibling`
[🔗](https://github.com/riddler/opentelemetry_statifier/blob/v0.10.0/lib/opentelemetry_statifier/sibling.ex#L1)

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 by the `:stop`
    or the `:exception` whose `span_ref` matches. Only
    `statifier_persistence` has them - its step seam and its batch
    migration seam (ADR-0004's 2026-09-28 Amendment); `sob-ADR-0006`
    decided against pairs on the grounds that Oban already owns every
    interval it could bracket. The `:stop` closes through
    `close_span/5`. The `:exception` does not:
    `OpentelemetryStatifier.Persistence.Handler` takes the span from the
    table and ends it itself, with an error status, so the `exception`
    span event it records lands between the take and the end.
  * **An interval span**, for a point-in-time event that nonetheless
    carries a `duration` measurement (`[:statifier_persistence,
    :adapter, :call]` and `[..., :execution, :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.

# `host`

```elixir
@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 execution 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.

# `caller_context_links`

```elixir
@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`

```elixir
@spec close_span(
  OpentelemetryStatifier.Config.t(),
  reference(),
  integer(),
  map(),
  OpenTelemetry.status() | nil
) :: :ok
```

Closes the paired span under `span_ref`, setting `attributes` first,
and `status` when one is given. A `span_ref` with no open span is
contract-legal (the sweep ends the orphans a dead process leaves) and
closes nothing.

# `interval_span`

```elixir
@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`

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

# `name`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
