OpentelemetryStatifier.Persistence (OpentelemetryStatifier v0.10.0)

Copy Markdown View Source

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.

Summary

Functions

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

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

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.

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

Functions

events()

@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()

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

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

Examples

iex> OpentelemetryStatifier.Persistence.setup()
:ok

setup(opts)

@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()

@spec teardown() :: :ok

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