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

The bridge half for `statifier_persistence`'s own event family,
`[:statifier_persistence, ...]` - the durable stepper's storage
phases, frozen by that package's `docs/adr/0009-telemetry-events-for-the-durable-stepper.md`
and tabulated in its `docs/telemetry.md`.

It is a **separate setup call**, per st-ADR-0062 and `ots-ADR-0002`
decision 2: a host that steps durably calls

    OpentelemetryStatifier.setup()
    OpentelemetryStatifier.Persistence.setup()

the shape `opentelemetry_ecto` and `opentelemetry_oban` compose in.
Nothing here attaches unless the host asks for it, and a host with no
durable stepper pays nothing.

## What it produces

`[:statifier_persistence, :execution, :step, :start]` / `[..., :stop]` is
the first of the two paired seams the sibling contracts define, and it
becomes a `statifier_persistence.execution.step` span - the interval the durable
stepper owns and nothing else measures: lock, load, decode,
identity-check, advance, execute effects, persist. **The macrostep span
for the step nests inside it**, as do the `statifier_persistence.adapter.call`
and `statifier_persistence.execution.lock` spans, so a durable execution reads as
one tree rather than as two unrelated families. The nesting runs
through this bridge's own span table, not the process's ambient OTel
context (`docs/adr/0004-sibling-setup-calls-and-bridge-owned-nesting.md`).

`[..., :step, :exception]` closes the same span in place of `:stop` when
the drive raised, threw or exited, and the span ends with an error
status whose message is `"<kind>: <reason>"` (`"error: RuntimeError"`).
`statifier_persistence` narrows `reason` to an atom before it emits, so
the message is bounded. `kind` and `reason` also ride as attributes;
`stacktrace` does not - it is a list, and the attribute rules drop a
list (`OpentelemetryStatifier.Attributes`).

The close also records a span event named `exception` on the step
span, the OpenTelemetry exception semantic convention's shape, so a
tracing backend's own exception view shows the failure. It carries
`exception.type` - the exception's module for an `:error`
(`"Elixir.RuntimeError"`), and `"<kind>:<reason>"` for a `:throw` or an
`:exit` (`"exit:redacted"`) - and `exception.stacktrace`, the frames
`statifier_persistence` narrowed before it emitted (module, function,
arity, file and line, no arguments), formatted one per line as
`Exception.format_stacktrace/1` lays them out. Nothing in this package
caps how many frames that is: every frame the event arrives with is
formatted, and the narrowing keeps every frame it is given, so the
bound is the VM's backtrace depth (the `:backtrace_depth` system flag,
8 by default, set with `:erlang.system_flag/2`). The OpenTelemetry
SDK's attribute value length limit, when a host sets one, truncates
the formatted string; it is unset by default. There is no
`exception.message`: the narrowed event carries no message. The
original reason and stacktrace stay with the caller, which re-raises
with them. A step that closes with `:stop` records no such event.

`reason` has two renderings on an `:error`, and the split is
deliberate. The `statifier_persistence.reason` attribute and
`exception.type` carry the module as a string with its `Elixir.`
prefix (`"Elixir.RuntimeError"`): that is the value a query filters
on, and the rendering `OpenTelemetry.Span.record_exception/4` gives an
exception's type, the OpenTelemetry Elixir convention. The status
message carries the readable form without the prefix
(`"error: RuntimeError"`), because it is text a person reads. Both are
published values, so neither changes to match the other.

`[..., :step, :stop]`'s `selection` (`:selected`, `:none`, or `nil` on a
stop that delivered no event) rides as the
`statifier_persistence.selection` string attribute, and a `nil` is
omitted: the step clauses copy every metadata key through the attribute
rules rather than through a list of keys, so a key added upstream by
amendment arrives without a change here.

Every other event in the family is a point: a span event on the step
span open around it, or its own zero-duration span when there is none.
That includes `[..., :step, :reentered]`, the one four-segment point:
it carries no `span_ref`, and pairs with its step only by arriving
inside it, on the same process, between the `:start` and the close. It
becomes a `statifier_persistence.execution.step.reentered` span event on
that step span. Its `origin` (a tuple such as `{:transition, 2}`) renders
with `inspect/1`, the rule tuples already follow, and its `opts` (a
keyword list, `[sendid: id]` or `[]`) renders with `inspect/1` too
rather than being dropped as a list, because the `sendid` is what a
reader needs to tell one failed `<send>` from another.
`[..., :execution, :migrated]`'s `dropped` (a list of state ids) is not
dropped either, for the same reason: without it the point says a
migration happened and not what it cost. It becomes the
`statifier_persistence.dropped` string-array attribute, sorted, the
rendering `configuration` takes, so a backend can query one state id.
A migration that drops no state carries no `dropped` attribute: the
OpenTelemetry API refuses an empty list as an attribute value.

## The batch migration span

`[:statifier_persistence, :execution, :migrate_batch, :start]` /
`[..., :stop]` is the second paired seam: one
`StatifierPersistence.Executions.migrate_batch/3` call, dry run
included, becomes a `statifier_persistence.execution.migrate_batch`
span, opened, closed and failed exactly as the step span is, on the
same `span_ref` pairing. Its attributes are the plan's two content
hashes (`statifier_persistence.from`, `statifier_persistence.to`),
`statifier_persistence.dry_run`, and on the close
`statifier_persistence.outcome` (`"ok"` or `"error"`),
`statifier_persistence.reason` (the refusal of the whole batch, omitted
when there is none), `statifier_persistence.duration`, and one integer
attribute per outcome of the mode carrying the report's count, zeros
included: `would_migrate`, `would_refuse` and `skipped` for a dry run;
`migrated`, `refused`, `parked` and `skipped` for an apply. A batch
refused whole closes with every count at zero and no error status, as
a step whose `outcome` is `:error` does. `[..., :migrate_batch,
:exception]` ends it with the error status and the `exception` span
event the step span's exception close records.

Each execution the apply moves emits its own
`[:statifier_persistence, :execution, :migrated]` on the calling
process, inside the batch, so it lands as a
`statifier_persistence.execution.migrated` span event on the batch
span, by the point rule below; a dry run emits none, so its span
carries no such event. The lock and adapter-call spans each execution's
turn opens nest inside the batch span, as they nest inside a step. A
`:migrated` fired outside any batch is unchanged: a point, as before.

## What it does not do

`statifier_persistence` also emits the *interpreter's* family with
`driver: :persistence` (st-ADR-0067), and that needs no bridge work of
its own - `OpentelemetryStatifier.setup/1` already attaches it, and the
`driver` metadata maps to `statifier.driver` through the mapping the
bridge already has. Attaching this module without that one produces
step spans with nothing inside them.

## The event list

The 23 names below are literal here rather than read from
`StatifierPersistence.Telemetry.events/0`, because this package takes
no dependency on its siblings: a bridge that made `statifier_persistence`
(and through it Ecto, and a database driver) a dependency of every host
that wants statechart tracing would have the dependency direction
exactly backwards.

The two lists are not checked by hand. `statifier_persistence` is a
`only: :test, runtime: false` dependency of this package, and
`test/opentelemetry_statifier/sibling_event_drift_test.exs` asserts this
list against `StatifierPersistence.Telemetry.events/0` on every gate
run, so a name added, removed, or renamed upstream turns this repo red
rather than silently going unbridged. A host pays nothing for that: an
`only: :test` dependency is not a published Hex requirement.

# `events`

```elixir
@spec events() :: [:telemetry.event_name()]
```

The event names this module attaches to - `statifier_persistence`'s
family two, in full.

## Examples

    iex> length(OpentelemetryStatifier.Persistence.events())
    23

# `setup`

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

Attaches this family with default options. Delegates to `setup/1`.

## Examples

    iex> OpentelemetryStatifier.Persistence.setup()
    :ok

# `setup`

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

Validates `opts` into a `OpentelemetryStatifier.Config.t()` and attaches
this module's handler to every name `events/0` returns, one
`:telemetry.attach/4` call per name under a per-event handler id -
ADR-0003 decision 2's discipline, so a raise while handling one event
name costs that name and nothing else.

Idempotent, exactly as `OpentelemetryStatifier.setup/1` is: the ids this
module owns are detached before they are attached, so a second call
replaces the first attachment. The two setups are independent - this one
detaches and attaches nothing outside its own family.

The `:adapter_calls` option is `:nested_only` (the default) or `:all`.
With `:nested_only`, a `statifier_persistence.adapter.call` span is
emitted only inside an enclosing span; with `:all`, every adapter call
span is emitted.

# `teardown`

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

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

---

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