Metrics export pipeline — timer-driven snapshot of the per-table
XxxStorage GenServers + OTLP encode + HTTP POST. Single
GenServer collapsing what was previously a separate
Otel.Metrics.MetricReader.PeriodicExporting GenServer
(timer + collect) plus a passive HTTP-only MetricExporter
module.
Unlike Otel.Trace.SpanExporter and
Otel.Logs.LogRecordExporter (which drain a queue),
Metrics exports a snapshot of live state — the
Otel.Metrics.{InstrumentsStorage, MetricsStorage, ExemplarsStorage} GenServers hold accumulating
aggregation / exemplar state. collect/0 walks the
instruments ETS table and returns one metric() per
registered instrument; the next collect tick reads the
same state with fresh aggregation values.
Lifecycle
| Trigger | Action |
|---|---|
:loop self-message every @scheduled_delay_ms | collect/1 + encode + POST |
force_flush/1 | force collect + encode + POST synchronously |
terminate/2 | force collect + encode + POST before exit |
OTLP HTTP transport
POSTs OTLP/protobuf via Req.
User config is read from
Application.get_env(:otel, :req_options, []) on every export
and forwarded to Req.post/1 — anything Req accepts (TLS,
auth, timeouts, retry overrides, mock plugs) works.
The SDK only forces :body (the encoded protobuf). Defaults
via Keyword.put_new:
:base_url→http://localhost:4318if absent:url→/v1/metricsif absent:retry→ predicate matching the OTLP-spec retryable response codes (opentelemetry-proto/docs/specification.mdL564-575: 429 / 502 / 503 / 504 SHOULD be retried, all other 4xx / 5xx MUST NOT) plus network-level exceptions. Backoff strategy (exponential + jitter) andRetry-Afterhonoring come from Req's default:retry_delay, which satisfies the spec MUST inopentelemetry-specification/specification/protocol/exporter.mdL182-202.content-type: application/x-protobufanduser-agentheaders merged into the user's:headers
:max_retries is left to Req's default (3 retries = 4
attempts) — the OTLP spec mandates the strategy but not a
specific attempt count.
Concurrency
Spec metrics/sdk.md L1880-L1881 (Status: Stable) —
"Collect, ForceFlush (for periodic exporting MetricReader)
and Shutdown MUST be safe to be called concurrently."
The single GenServer mailbox serialises force_flush/1
against the timer-driven :loop message, satisfying the MUST.
collect/1 is a pure pull from ETS / :persistent_term
state and is safe to call from any process.
References
- OTel Metrics SDK §MetricReader:
opentelemetry-specification/specification/metrics/sdk.mdL1280-L1442 - OTel Metrics SDK §Periodic exporting MetricReader:
opentelemetry-specification/specification/metrics/sdk.mdL1443-L1500 - OTel Metrics SDK §MetricExporter:
opentelemetry-specification/specification/metrics/sdk.mdL1530-L1660 - OTLP retryable response codes:
opentelemetry-proto/docs/specification.mdL565-L573
Summary
Functions
Returns a specification to start this module under a supervisor.
SDK — Walks Otel.Metrics.InstrumentsStorage and returns
one metric() per registered instrument.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec collect() :: [Otel.Metrics.Metric.t()]
SDK — Walks Otel.Metrics.InstrumentsStorage and returns
one metric() per registered instrument.
Pure pull from ETS state — safe to invoke from any process.
@spec force_flush(timeout :: timeout()) :: :ok
@spec start_link(opts :: keyword()) :: GenServer.on_start()