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

OpenTelemetry instrumentation for the Statifier family of statechart
packages.

Attaches to the public `:telemetry` events the family emits - starting
with statifier's `[:statifier, :session, ...]` contract
(`Statifier.Session.Telemetry`) - and turns them into OpenTelemetry
spans, span events, and span links. The library depends only on
`opentelemetry_api`; hosts bring their own SDK and exporter.

`setup/0`/`setup/1` attach `OpentelemetryStatifier.Handler.handle_event/4`
to every name `Statifier.Session.Telemetry.events/0` returns, one
`:telemetry.attach/4` call per name under a per-event handler id, so a
raise in one event's handling never costs the others
(`deps/telemetry/src/telemetry.erl:109-116`). `teardown/0` detaches all of
them.

A macrostep `:start` opens a root span named `statifier.macrostep`, and
the `:stop` whose `span_ref` matches closes it, carrying
`statifier.session_id`, `statifier.trigger`, `statifier.outcome`, the
macrostep's counters, and its resulting `statifier.configuration` as
attributes (`OpentelemetryStatifier.Handler`,
`OpentelemetryStatifier.SpanTable`). Span start times come from each
event's own `monotonic_time`, so a macrostep span's wall time tracks the
`statifier.duration` measurement closely - with one documented
exception. Statifier emits the `:initialize` macrostep's `:start` from
the session's `init/1` and its `:stop` from the following
`handle_continue`, so that span opens after some of the work it covers,
and reads materially shorter than `statifier.duration` reports. The skew
is accepted rather than corrected; see the README and
`docs/adr/0003-handler-attach-and-span-table-mechanism.md`.

Everything that fires between a `:start` and its `:stop` - the eleven
`[:statifier, :session, :effect, _]` events, the nine
`[:statifier, :session, :trace, _]` events, `:interpret`, `:unroutable`,
and `:halt` - lands as a span event on the session's open macrostep
span, attributes mapped uniformly by `OpentelemetryStatifier.Attributes`
(locations flattened to `statifier.source.line`/`.column`, the raw
effect struct never serialized, datamodel values excluded unless
`setup/1` received `record_datamodel_values: true`). Each macrostep span
is the root of its own trace, stitched to its neighbors with span
links: one to the same session's previous macrostep span, and - on a
child session's `:initialize` macrostep - one to the invoking parent's
macrostep span, resolved from the `:init` event's `invoked_by`.
The bridge does not leak on crashes: `:terminate` removes the session's
rows from the span table (ending a still-open macrostep span with an
error status), and because `:terminate` does not fire on a brutal kill,
`OpentelemetryStatifier.SpanTable` also sweeps on a timer, ending the
orphaned spans of sessions whose process no longer exists the same way.
With `trace: false` the core emits no trace-family events at all, so the
bridge degrades to macrostep-grained spans with effect-level span events
and needs no configuration of its own. The design this package
implements lives in statifier-ex: `docs/opentelemetry.md` (span
topology, context propagation, cardinality, failure tolerance) and
st-ADR-0062 (packaging and scope).

The family's sibling packages are bridged by their own setup calls -
`OpentelemetryStatifier.Persistence.setup/1` and
`OpentelemetryStatifier.Oban.setup/1`, each attaching only its own
family - the shape `opentelemetry_ecto` and `opentelemetry_oban`
compose in a host. With `Persistence` attached, a durable macrostep
span nests inside the `statifier_persistence.execution.step` span around it
rather than rooting its own trace; with neither attached, nothing here
behaves differently (ADR-0004).

A host running a durable stepper of its own asks for that same nesting
through `OpentelemetryStatifier.Parent`: `register/2` and
`unregister/1`, or the scoped `within/3`, declare the span a process's
macrostep spans belong inside. It is a declaration and never an ambient
read - the bridge still neither inherits nor clobbers the process's
OTel context - and a host that never calls it sees no change at all
(ADR-0004 decision 4, generalized in that record's 2026-09-02 note).

`OpentelemetryStatifier.SpanContext.lookup/2` is the one read the bridge
offers back: the open macrostep span for a `(session_id, macrostep)`
pair, as its W3C trace and span ids. It exists so a `statifier_ui`
trace subscriber can be handed it as the `:otel_context` producer that
package's ADR-0013 specifies, and it is only ever a read of the span
table - no OTel context is created, entered, or attached.

# `setup`

```elixir
@spec setup() :: :ok | {:error, term()}
```

Attaches the bridge with default options. Delegates to `setup/1`.

## Examples

    iex> OpentelemetryStatifier.setup()
    :ok

# `setup`

```elixir
@spec setup(keyword()) :: :ok | {:error, term()}
```

Validates `opts` into a `OpentelemetryStatifier.Config.t()` and attaches
the bridge's handler to every name `Statifier.Session.Telemetry.events/0`
returns.

Idempotent: any ids this package already owns are detached first, so a
second call - with the same or different options - replaces the first
attachment rather than returning an "already exists" error.

Returns `{:error, reason}` from `OpentelemetryStatifier.Config.new/1`
without attaching anything when `opts` is invalid.

# `teardown`

```elixir
@spec teardown() :: :ok
```

Detaches every handler id this package owns. Always returns `:ok`, even
when nothing was attached.

---

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