ADR-0062: The OpenTelemetry bridge is a separate package, opentelemetry_statifier
Copy Markdown
View Source
Status: accepted (2026-08-20) - decides st-cmq.2's packaging question; the
span topology, context propagation, and cardinality decisions the bridge
implements are recorded in docs/opentelemetry.md
Context
ADR-0040 shipped the [:statifier, :session, ...] telemetry contract with
an OpenTelemetry bridge named as its first external consumer, and st-cmq.2
holds the design question that was deliberately left open: does that bridge
live in this repository as an optional Statifier.Telemetry.OTel module
behind a Code.ensure_loaded? guard, or in a separate package?
Constraints already fixed before this record: no OTel API call anywhere but the bridge; the bridge consumes only the public telemetry events, and a data gap is fixed by the event contract gaining a field (an ADR-0040 amendment), never by the bridge reaching into internals; trace-off behavior degrades to macrostep-grained spans structurally (ADR-0040's core-side gate).
Facts that bound the choice:
1. The ecosystem surveyed one way. Oban, Ecto, Phoenix, Broadway,
Finch, and Redix all keep OTel out of the core library and ship the bridge
as a separate opentelemetry_<lib> package, most under
open-telemetry/opentelemetry-erlang-contrib. The in-library optional
module is the minority pattern. (Survey table in docs/opentelemetry.md.)
2. The in-library module has recurring costs here. This repo's gate
holds 100% Doctor thresholds and warnings-as-errors; an optional dependency
means every full gate run happens twice (with and without
opentelemetry_api present) or silently checks only one side. OTel API
releases would force statifier releases. And the consume-only-public-
events constraint would be a review obligation inside the repo whose
internals are one alias away.
3. The bridge's natural scope is the family, not this repo.
statifier_persistence and statifier_oban will grow telemetry surfaces
of their own (each repo's tracker carries the mirrored design bead), and a
host wants one trace across load -> step -> effects -> timer hop -> resume.
A bridge module inside statifier can never own the spans and span links
that cross those package boundaries.
4. statifier is not on Hex (ADR-0061). A published Hex package
cannot carry a git dependency, so the bridge cannot publish before
statifier does - and ADR-0061 decision 5 names "a satellite package must
itself publish to Hex" as a trigger that reopens the no-publish rule.
Decision
1. The bridge is a separate package in its own repository:
opentelemetry_statifier. This repository gains no OTel dependency, no
optional module, and no Code.ensure_loaded? guard. Mix.Statifier.AdrGuard
needs no new exemption: the emission half stays exactly the two files
ADR-0040 named.
2. The name follows the OTel ecosystem convention, not the statifier_*
family convention. opentelemetry_statifier sits alphabetically and
mentally next to opentelemetry_oban and opentelemetry_ecto, which is
where its audience looks. The family convention (statifier_ui,
statifier_persistence, statifier_oban) names packages that extend
statifier; this one extends OpenTelemetry coverage to statifier, and the
ecosystem's naming encodes that direction. Recorded so the inconsistency
reads as chosen, not accidental.
3. Its scope is the statifier family. The package bridges this repo's
27-event contract first, and the sibling packages' telemetry surfaces as
they appear - as separate setup calls per library, the same shape
opentelemetry_ecto and opentelemetry_oban compose in a host today.
Cross-package trace stitching (span links across the persistence and timer
hops) belongs to it and to no single statifier repo.
4. It consumes only public contracts, structurally. Its statifier
dependency is a git pin to a main SHA under the ADR-0061 contract, like
every other consumer. When the bridge needs data the events do not carry -
the already-identified case is caller trace context on external events and
on %SendDelayed{}, so a delayed send firing later can link its
scheduling trace - the field is added here under ADR-0040's amendment
discipline and tracked as its own bead, mirrored where the consuming host
lives.
5. It stays unpublished until statifier is on Hex. Consumers take it
by git pin, same contract. The moment it genuinely needs to publish,
that is ADR-0061 decision 5's trigger firing, and the pre-release question
is re-decided there, not here.
6. What this repo owes the bridge is the freeze ADR-0040 already
states. The 27 event names, their measurement/metadata shapes, and the
fields of the effect structs riding in metadata.effect are a public
commitment; from the bridge's first release against them, a change is a
breaking change to a real consumer, surfaced in changelog.d/ per
ADR-0061 decision 3.
Consequences
docs/opentelemetry.mdholds the bridge-facing design (span topology, context propagation, sampling/cardinality, failure tolerance); this record holds only the packaging decision.docs/observability.mdconstraint 6 points at both.- The repository for
opentelemetry_statifieris created by the maintainer; its own tracker owns bridge implementation beads, and st-cmq.2 closes when this record and the design note land - the charter-transfer patternstatifier_persistence(sp-4an) andstatifier_oban(sob-q01) already used. - The named follow-up bead in this repo: an opaque caller-context slot on
external events and
%SendDelayed{}/durable-timer payloads, additive under ADR-0040's amendment rules, mirrored intostatifier_oban's tracker for the firing-side link. - What would reopen this record: the bridge needing per-microstep child
spans (rejected in the design note as span events), a second bridge
package appearing for one sibling library (decision 3 says one package),
or
statifierpublishing to Hex, which unlocks decision 5's publish question but does not by itself reopen packaging.