Bridges BEAM :telemetry.span/3 events into the OTel Trace
pipeline. Trace pillar's analog of Otel.LoggerHandler (Logs)
and Otel.TelemetryReporter (Metrics).
Add to your supervision tree with the event prefixes that should be promoted to OTel spans:
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
{Otel.TelemetryTracer, events: [
[:my_app, :checkout],
[:phoenix, :endpoint],
[:my_app, :repo, :query]
]}
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
endEach entry in events: is the prefix (matches the first
argument of :telemetry.span/3); the bridge subscribes to the
three lifecycle events derived from it
(prefix ++ [:start | :stop | :exception]).
What lands in Tempo
| OTel field | Source |
|---|---|
name | event prefix joined by . — e.g. [:my_app, :checkout] → "my_app.checkout" |
parent_span_id | implicit from the calling process's current OTel context (works for nested :telemetry.span/3 and mixed with_span/4) |
attributes | start_metadata ∪ stop_metadata, minus :telemetry-internal keys (see "Reserved metadata keys" below). All keys coerced to String.t() |
status | :ok on :stop; :error on :exception (description = Exception.message/1 for exceptions, inspect(reason) for :exit/:throw) |
exception.* | record_exception/4 on the :exception event, using metadata.reason + metadata.stacktrace |
Span kind defaults to INTERNAL. Pass
metadata.span_kind: :server | :client | :producer | :consumer
on the :start event to override.
Context propagation
The :start handler runs synchronously in the calling process
before the user function executes; :stop / :exception
handlers run after. Between them, the OTel current-span ctx is
the new span (stored in process dictionary keyed by the
:telemetry.span/3-issued ref). This means:
- Nested
:telemetry.span/3calls automatically form a parent-child chain. :telemetry.span/3↔Otel.Trace.with_span/4mixed in the same process also form a chain (both APIs share the process-dictionary ctx channel).- Cross-process work (
Task.async,GenServer.cast, etc.) still requires explicitOtel.Ctx.attach/1of the captured parent ctx — same constraint aswith_span/4.
Reserved metadata keys
:telemetry.span/3 inserts a few internal keys into the
metadata map (notably :telemetry_span_context, the
start/stop matching ref). The bridge filters these so they
don't leak as span attributes. Keys filtered:
:telemetry_span_context, :duration, :monotonic_time,
:system_time, :kind, :reason, :stacktrace, :span_kind.
All remaining metadata keys are stringified (to_string/1)
and merged onto the span as attributes.
Lifecycle
The bridge is a GenServer with trap_exit: true. Each
configured event prefix attaches three handlers via
:telemetry.attach/4 keyed by {__MODULE__, event_name, self()}. terminate/2 detaches all of them so a clean
shutdown leaves no stale handlers.
Multiple instances under different supervisors can co-exist — each instance owns handlers keyed by its own pid.
References
:telemetry.span/3: https://hexdocs.pm/telemetry/telemetry.html#span/3- OTel Trace API §Span Creation:
opentelemetry-specification/specification/trace/api.mdL378-L414 - OTel Trace API §record_exception:
trace/api.mdL654-L705
Summary
Types
A :telemetry.span/3 event prefix — the first arg to
:telemetry.span/3. The bridge subscribes to
prefix ++ [:start | :stop | :exception] for each entry.
Per-handler config passed via :telemetry.attach/4's
config argument and received as the 4th arg of
handle_event/4.
Options accepted by start_link/1. Both keys are optional —
omitting :events yields a no-op tracer (no handlers
attached).
GenServer state — the list of telemetry handler IDs we own.
Functions
Returns a specification to start this module under a supervisor.
Types
@type event_prefix() :: [atom()]
A :telemetry.span/3 event prefix — the first arg to
:telemetry.span/3. The bridge subscribes to
prefix ++ [:start | :stop | :exception] for each entry.
@type handler_config() :: %{ prefix: event_prefix(), suffix: :start | :stop | :exception }
Per-handler config passed via :telemetry.attach/4's
config argument and received as the 4th arg of
handle_event/4.
@type opts() :: [events: [event_prefix()], name: GenServer.name()]
Options accepted by start_link/1. Both keys are optional —
omitting :events yields a no-op tracer (no handlers
attached).
@type primitive_any() :: primitive() | [primitive_any()] | %{required(String.t()) => primitive_any()}
@type state() :: %{handlers: [:telemetry.handler_id()]}
GenServer state — the list of telemetry handler IDs we own.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec start_link(opts :: opts()) :: GenServer.on_start()