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

Owns the ETS table that holds open macrostep spans and, per session,
the last span context ots-j82's links read.

The table is created in `init/1` and otherwise untouched by this
process - it is `:public` and `:named_table` so handlers read and write
it directly from the session process that calls
`OpentelemetryStatifier.Handler.handle_event/4`, with no message hop
through this `GenServer`. Owning the table this way (rather than letting
`setup/1` create it) means the table exists before any handler can fire,
survives every session process, and gives ots-lt6's sweep timer a
natural home - `opentelemetry_ecto`'s latent "owned by whoever called
setup" bug does not reach this package.

Row shapes, fixed here because ots-j82 and ots-lt6 both read them
directly against the table name carried in `OpentelemetryStatifier.Config`:

    {{:span, span_ref},             session_id, %SpanEntry{}}
    {{:last_span_ctx, session_id},  session_id, span_ctx}
    {{:invoke_parent, session_id},  session_id, span_ctx}
    {{:session_pid, session_id},    session_id, pid}
    {{:sibling_span, span_ref},     pid,        %SiblingEntry{}}
    {{:parent_span, ref},           pid,        %ParentEntry{}}

The `:sibling_span` row is the sibling families' half (ADR-0004): a
`[:statifier_persistence, :execution, :step, :start]` opens one keyed on the
same `span_ref` convention, and it carries a **pid** in element 2 where
the macrostep rows carry a `session_id`, because a step span is scoped
to the process that drove it rather than to a logical session. That is
what makes `fetch_innermost_sibling_span/2` a one-`match_object`
lookup for "is one of this bridge's own spans open around me, in this
process" - the question every nested sibling span and every durable
macrostep span asks.

The `:parent_span` row is the foreign-driver half of the same
mechanism (ADR-0004 decision 4, and its 2026-09-02 Notes): a host that
runs a durable stepper this package knows nothing about declares its
own enclosing span through `OpentelemetryStatifier.Parent.register/2`,
and the row it writes is read by exactly the same "is something of
mine open around me, in this process" lookup. It carries a **pid** in
element 2 for that reason. The one thing it is not is a span this
bridge owns: `sweep/1` deletes an abandoned `:parent_span` row without
ending its span, where a `:sibling_span` row's span is ended with an
error status.

The `:invoke_parent` row is written when a child session's `:init` event
names an `invoked_by` parent whose macrostep span is open at that
moment, and consumed (removed) by the child's own `:initialize`
macrostep start, which turns it into a span link.

The `:session_pid` row records the session process behind `session_id` -
the handler runs inside the session process, so `self()` at event time
is that pid. It exists for the sweep: `:terminate` does not fire on a
brutal kill, so `sweep/1` walks these rows and, for every session whose
process is no longer alive, ends any still-open macrostep spans with an
error status and deletes all the session's rows - the design note's
"sweeps entries whose sessions no longer exist rather than trusting the
event alone". This `GenServer` runs the sweep on a timer; tests call
`sweep/1` directly.

The `%SpanEntry{}` in a `:span` row gains its `macrostep` counter after
the fact: the macrostep `:start` event carries no such measurement, so
the handler stamps it from the first intra-macrostep event that lands
on the span. `fetch_open_span_ctx/3` is the keyed
`(session_id, macrostep)` read that field exists for.

`session_id` is duplicated into element 2 of every row shape so a
sweeper can find every row for a session with one
`:ets.match_object/2` or `:ets.match_delete/2`, without decoding either
row's structured value.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `delete_parent_span`

```elixir
@spec delete_parent_span(atom(), reference()) :: :ok
```

Removes the host-declared enclosing span under `ref`. The declared span
is **not** ended - the host that opened it owns its lifetime. Removing
a `ref` that is not there is `:ok`, so `unregister/1` is idempotent and
safe to call from an `after` block that may run twice.

# `delete_session`

```elixir
@spec delete_session(atom(), String.t(), String.t()) :: :ok
```

Removes every row `session_id` owns, first ending any still-open
macrostep spans with an error status carrying `orphan_message` - an
orphan is reported, never silently dropped. Called by the handler's
`:terminate` clause and by `sweep/1` for sessions whose process died
without a `:terminate`.

# `fetch_enclosing_ctx`

```elixir
@spec fetch_enclosing_ctx(atom(), pid()) :: {:ok, OpenTelemetry.Ctx.t()} | :error
```

The context a span opened in `pid` right now should nest under: the
innermost thing this bridge has recorded as enclosing that process,
whichever of the two kinds it is, or `:error` when there is none.

Two row shapes answer the same question and are ordered against each
other on the one clock they share (`System.monotonic_time/0`): a
`:sibling_span` row, which is a span this bridge opened itself from a
sibling package's `:start` event, and a `:parent_span` row, which is a
span a host declared through
`OpentelemetryStatifier.Parent.register/2`. The most recently opened of
them is the innermost, so a bridge-opened step span inside a host's
declared span parents what follows, and so does a declaration made
inside a step span.

A declaration whose registrant is dead is skipped: nobody is left to
end that span or to withdraw the declaration, so nesting under it would
attach live spans to an abandoned trace. Falling back to the next
enclosing row - or, usually, to no parent at all - is the honest
answer.

# `fetch_innermost_open_row`

```elixir
@spec fetch_innermost_open_row(atom(), String.t()) ::
  {:ok, reference(), OpentelemetryStatifier.SpanEntry.t()} | :error
```

As `fetch_innermost_open_span/2`, but also returns the `span_ref` the
entry is keyed under, so a caller that has just read the row can write
an updated one back through `put_open_span/3` without a second scan of
the table. `OpentelemetryStatifier.Handler` uses it on the span-event
path, where every event both lands on the innermost span and carries
the `macrostep` counter that span is missing.

# `fetch_innermost_open_span`

```elixir
@spec fetch_innermost_open_span(atom(), String.t()) ::
  {:ok, OpentelemetryStatifier.SpanEntry.t()} | :error
```

Fetches the innermost open span for `session_id` - the entry with the
greatest `started_at` among the session's open rows, since ADR-0039
re-entry can hold two spans open at once and an intra-macrostep event
belongs to the most recently opened one. Returns `:error` when the
session has no open span (an effect event racing a crash's cleanup is
contract-legal, not a bug).

# `fetch_innermost_sibling_span`

```elixir
@spec fetch_innermost_sibling_span(atom(), pid()) ::
  {:ok, OpentelemetryStatifier.SiblingEntry.t()} | :error
```

Fetches the innermost sibling span open in `pid` - the entry with the
greatest `started_at`, since a parent execution creating a durable child
inside its own step holds two step spans open on one process. Returns
`:error` when the process has none, which is the ordinary case for a
host that attached only the sibling setups it uses.

# `fetch_last_span_ctx`

```elixir
@spec fetch_last_span_ctx(atom(), String.t()) ::
  {:ok, OpenTelemetry.span_ctx()} | :error
```

Fetches the last span context recorded for `session_id`, returning
`{:ok, span_ctx}` on a hit or `:error` for an unknown session.

# `fetch_open_span_ctx`

```elixir
@spec fetch_open_span_ctx(atom(), String.t(), non_neg_integer()) ::
  {:ok, OpenTelemetry.span_ctx()} | :error
```

Fetches the span context of the open macrostep span `session_id`
recorded `macrostep` on, or `:error` when there is none - an unknown
session, a macrostep whose span has already closed, or an open span
that has not yet seen an event carrying the counter.

This is a **read**, and deliberately only a read: it returns a stored
context, never attaches one, so ADR-0003 decision 8's rule that the
bridge neither inherits nor clobbers a process's ambient context is
untouched. `OpentelemetryStatifier.SpanContext.lookup/2` is the public
face of it.

Under st-ADR-0039 re-entry a session can hold two macrostep spans open
at once; they carry different counters in the ordinary case, and the
innermost wins if they do not.

# `fetch_session_pid`

```elixir
@spec fetch_session_pid(atom(), String.t()) :: {:ok, pid()} | :error
```

Fetches the process recorded for `session_id` by `put_session_pid/3`,
or `:error` for a session the bridge has not seen. The sibling
handlers use it to check that a session's open macrostep span belongs
to *this* process before landing a span event on it - a delivery-seam
event on an Oban worker must never write onto a span another process
has open for the same scope.

# `new_table`

```elixir
@spec new_table(atom()) :: atom()
```

Creates (or returns, if it already exists under this process) a
`:public`/`:named_table` ETS table under `name`. Tests call this
directly with a unique name per test so they never depend on the
application-started table.

# `put_invoke_parent`

```elixir
@spec put_invoke_parent(atom(), String.t(), OpenTelemetry.span_ctx()) :: :ok
```

Records the parent macrostep span context a child session's
`:initialize` macrostep will link to, keyed by the child's `session_id`.

# `put_last_span_ctx`

```elixir
@spec put_last_span_ctx(atom(), String.t(), OpenTelemetry.span_ctx()) :: :ok
```

Records `span_ctx` as the last span context observed for `session_id`.

# `put_open_span`

```elixir
@spec put_open_span(atom(), reference(), OpentelemetryStatifier.SpanEntry.t()) :: :ok
```

Stores an open span's `%SpanEntry{}` under its `span_ref`.

# `put_parent_span`

```elixir
@spec put_parent_span(
  atom(),
  reference(),
  pid(),
  OpentelemetryStatifier.ParentEntry.t()
) :: :ok
```

Stores a host-declared enclosing span's `%ParentEntry{}` under `ref`,
keyed for lookup by the process whose spans nest under it.

# `put_session_pid`

```elixir
@spec put_session_pid(atom(), String.t(), pid()) :: :ok
```

Records `pid` as the session process behind `session_id`, for the
sweep's liveness check. Idempotent - the table is a `:set`, so a
session's row is written once per macrostep at no accumulating cost.

# `put_sibling_span`

```elixir
@spec put_sibling_span(
  atom(),
  reference(),
  pid(),
  OpentelemetryStatifier.SiblingEntry.t()
) :: :ok
```

Stores an open sibling span's `%SiblingEntry{}` under its `span_ref`,
keyed for lookup by the process that opened it.

# `sweep`

```elixir
@spec sweep(atom()) :: :ok
```

Ends the orphans a brutal kill leaves behind: for every `:session_pid`
row whose process is no longer alive, delegates to `delete_session/3`,
and for every `:sibling_span` row whose process is no longer alive,
ends that span with an error status and deletes the row. A live
process's rows are never touched - an open span on a live process is
just a step or a macrostep in flight, whatever its age.

A `:parent_span` row is swept when either its registrant or the process
it nests spans for is gone, and its span is **never** ended: the host
declared a span it owns, and ending someone else's span - possibly in
the middle of the work it covers - would be worse than leaking a
three-tuple until the next sweep.

# `take_invoke_parent`

```elixir
@spec take_invoke_parent(atom(), String.t()) ::
  {:ok, OpenTelemetry.span_ctx()} | :error
```

Looks up and removes the invoke-parent span context stored for
`session_id`, returning `{:ok, span_ctx}` on a hit or `:error` when no
parent was recorded - a session that was not invoked by anyone.

# `take_open_span`

```elixir
@spec take_open_span(atom(), reference()) ::
  {:ok, OpentelemetryStatifier.SpanEntry.t()} | :error
```

Looks up and removes the open span under `span_ref`, returning
`{:ok, entry}` on a hit or `:error` on a miss - a `:stop` event with no
matching `:start` is a contract-legal shape, not a bug, so this never
raises.

# `take_sibling_span`

```elixir
@spec take_sibling_span(atom(), reference()) ::
  {:ok, OpentelemetryStatifier.SiblingEntry.t()} | :error
```

Looks up and removes the open sibling span under `span_ref`. Returns
`:error` on a miss, which - as for `take_open_span/2` - is a
contract-legal shape rather than a bug.

---

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