# `Otel.Metrics.MetricExporter`
[🔗](https://github.com/yangbancode/otel/blob/main/lib/otel/metrics/metric_exporter.ex#L1)

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`](https://hex.pm/packages/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:4318` if absent
- `:url` → `/v1/metrics` if absent
- `:retry` → predicate matching the OTLP-spec retryable
  response codes (`opentelemetry-proto/docs/specification.md`
  L564-575: 429 / 502 / 503 / 504 SHOULD be retried, all
  other 4xx / 5xx MUST NOT) plus network-level exceptions.
  Backoff strategy (exponential + jitter) and `Retry-After`
  honoring come from Req's default `:retry_delay`, which
  satisfies the spec MUST in
  `opentelemetry-specification/specification/protocol/exporter.md`
  L182-202.
- `content-type: application/x-protobuf` and `user-agent`
  headers 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.md` L1280-L1442
- OTel Metrics SDK §Periodic exporting MetricReader: `opentelemetry-specification/specification/metrics/sdk.md` L1443-L1500
- OTel Metrics SDK §MetricExporter: `opentelemetry-specification/specification/metrics/sdk.md` L1530-L1660
- OTLP retryable response codes: `opentelemetry-proto/docs/specification.md` L565-L573

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `collect`

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

# `force_flush`

```elixir
@spec force_flush(timeout :: timeout()) :: :ok
```

# `start_link`

```elixir
@spec start_link(opts :: keyword()) :: GenServer.on_start()
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
