Otel.TelemetryTracer (otel v0.4.1)

Copy Markdown View Source

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
end

Each 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 fieldSource
nameevent prefix joined by . — e.g. [:my_app, :checkout]"my_app.checkout"
parent_span_idimplicit from the calling process's current OTel context (works for nested :telemetry.span/3 and mixed with_span/4)
attributesstart_metadatastop_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/3 calls automatically form a parent-child chain.
  • :telemetry.span/3Otel.Trace.with_span/4 mixed 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 explicit Otel.Ctx.attach/1 of the captured parent ctx — same constraint as with_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

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

event_prefix()

@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.

handler_config()

@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.

opts()

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

primitive()

@type primitive() ::
  String.t() | {:bytes, binary()} | boolean() | integer() | float() | nil

primitive_any()

@type primitive_any() ::
  primitive() | [primitive_any()] | %{required(String.t()) => primitive_any()}

state()

@type state() :: %{handlers: [:telemetry.handler_id()]}

GenServer state — the list of telemetry handler IDs we own.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

start_link(opts)

@spec start_link(opts :: opts()) :: GenServer.on_start()