Telemetry.Metrics reporter that bridges BEAM :telemetry
events into the OTel Metrics pipeline. Mirror of
Otel.LoggerHandler for the metrics pillar.
Add it to your supervision tree with a list of
Telemetry.Metrics definitions:
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
{Otel.TelemetryReporter, metrics: metrics()}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
defp metrics do
import Telemetry.Metrics
[
counter("phoenix.endpoint.stop.duration"),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
last_value("vm.memory.total", unit: {:byte, :kilobyte})
]
end
endType mapping
Telemetry.Metrics | OTel instrument | dispatch |
|---|---|---|
counter/2 | Otel.Metrics.Counter | Counter.add(inst, 1, attrs) — measurement ignored |
sum/2 | Otel.Metrics.UpDownCounter | UpDownCounter.add(inst, value, attrs) |
last_value/2 | Otel.Metrics.Gauge | Gauge.record(inst, value, attrs) |
summary/2 | Otel.Metrics.Histogram | Histogram.record(inst, value, attrs) |
distribution/2 | Otel.Metrics.Histogram | Histogram.record(inst, value, attrs) (uses buckets) |
Telemetry.Metrics.Sum carries no monotonic flag, so the
reporter conservatively maps it to UpDownCounter. If your
source values are guaranteed non-negative and you want monotonic
Sum semantics, pass reporter_options: [monotonic: true]:
sum("http.request.bytes_sent", reporter_options: [monotonic: true])Tags / tag_values
metric.tags selects which metadata keys to use as OTel
attribute keys; metric.tag_values (default identity) can
pre-process metadata before extraction. Tag values are
string-coerced (atoms via Atom.to_string/1, others kept
as-is) on dispatch.
Unit conversion
Unit conversion is performed by Telemetry.Metrics itself
at metric-definition time: when you pass unit: {from, to},
the metric's :measurement field is wrapped to convert
values into the target unit before they reach this reporter.
We forward the resulting target-unit atom (e.g.
:millisecond, :kilobyte) to the OTel instrument's unit
field. Note byte conversions are decimal per
Telemetry.Metrics convention (1 kB = 1000 B).
:keep / :drop predicates
Honored per Telemetry.Metrics: when :keep returns false or
:drop returns true, the measurement is skipped — the OTel
instrument is not updated.
Lifecycle
The reporter is a GenServer with trap_exit: true. Each
registered telemetry event gets a :telemetry.attach keyed by
{__MODULE__, event_name, self()}. On terminate/2 (supervisor
shutdown), every handler is detached so reloads / restarts
don't leave dangling handlers.
References
Telemetry.Metrics: https://hexdocs.pm/telemetry_metricsTelemetry.Metrics.ConsoleReporter— reference shape we mirror (group-by-event attach, single handle_event dispatch)
Summary
Types
Per-event handler config: the metrics defs grouped under one
event name, plus a shared metric_id → instrument lookup map.
Options accepted by start_link/1. Both keys are optional —
omitting :metrics yields a no-op reporter (no handlers
attached).
GenServer state — the list of telemetry event names we own handlers for.
Functions
Returns a specification to start this module under a supervisor.
Types
@type handler_config() :: {[Telemetry.Metrics.t()], %{required(term()) => Otel.Metrics.Instrument.t()}}
Per-event handler config: the metrics defs grouped under one
event name, plus a shared metric_id → instrument lookup map.
@type opts() :: [metrics: [Telemetry.Metrics.t()], name: GenServer.name()]
Options accepted by start_link/1. Both keys are optional —
omitting :metrics yields a no-op reporter (no handlers
attached).
@type primitive_any() :: primitive() | [primitive_any()] | %{required(String.t()) => primitive_any()}
@type state() :: %{events: [[atom()]]}
GenServer state — the list of telemetry event names we own handlers for.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec start_link(opts :: opts()) :: GenServer.on_start()