OtelBridge

otel_bridge bridges existing Telemetry.Metrics definitions to OpenTelemetry metrics.

It is intended for applications that already emit telemetry events and already define metrics with Telemetry.Metrics, but want to export those metrics through the OpenTelemetry SDK.

It does not replace the OpenTelemetry SDK. Its role is narrow:

When to use it

Use otel_bridge when:

How it works

Most integrations follow this flow:

  1. define one or more spec modules with use OtelBridge.Spec
  2. start OtelBridge under your supervision tree
  3. configure an OpenTelemetry metric reader, optionally via a profile helper
MyApp.Metrics (OtelBridge.Spec)
|
v
OtelBridge
/ \
v v
Telemetry handlers telemetry_poller
\ /
v v
OpenTelemetry metrics
|
v
OtelBridge.Profile
|
v
OTLP backend

Installation

Add otel_bridge to your dependencies:

def deps do
[
{:otel_bridge, "~> 0.1.2"}
]
end

Quick start

1. Define metrics

Create a spec module with OtelBridge.Spec:

defmodule MyApp.Metrics do
use OtelBridge.Spec
@impl OtelBridge.Spec
def metrics(meta) do
[
summary("http.server.duration",
event_name: [:my_app, :http, :stop],
measurement: :duration,
unit: {:native, :millisecond},
tags: [:route, :status_code],
tag_values: fn metadata ->
metadata
|> Map.put(:route, metadata[:route] || "unknown")
|> Map.put(:status_code, metadata[:status_code] || 500)
|> Map.put(:service, Keyword.get(meta, :service))
end
)
]
end
end

The meta argument comes from the :meta option passed to OtelBridge. Use it for shared values such as service name, default tags, or environment data.

2. Start the bridge

Add OtelBridge to your supervision tree:

children = [
{OtelBridge,
specs: [MyApp.Metrics],
optional_specs: [MyApp.OptionalMetrics],
measurements: [{MyApp.Measurements, :dispatch, []}],
meta: [service: "my_app"],
poller: [period: 5_000]}
]

Common options:

3. Configure metric export

otel_bridge helps build metric reader configuration, but the OpenTelemetry SDK remains configured through the standard OpenTelemetry packages.

For VictoriaMetrics:

config :opentelemetry_experimental,
readers: [
OtelBridge.metric_reader!(:victoria_metrics,
export_interval_ms: 5_000,
endpoint: "http://localhost:4318"
)
]

You can also configure the reader yourself and use otel_bridge only for metrics bridging.

Metric mapping

The default bridge path maps:

During that process, the bridge also:

Telemetry.Metrics.LastValue is handled differently from synchronous metrics. Telemetry events update an internal latest-value store, and the OpenTelemetry reader observes that store through an observable gauge callback during collection. This preserves the current-state semantics of gauges without treating absolute values as counter deltas.

last_value cardinality protection

Each last_value series is keyed by {metric_name, tags}. Low-cardinality gauges such as VM memory, queue depth, or cache size are a natural fit. Avoid high-cardinality tags such as request IDs, user IDs, or raw dynamic URLs unless you configure bounds.

Use reporter_options[:otel][:last_value] to cap retained series:

last_value("queue.depth",
event_name: [:my_app, :queue, :stats],
measurement: :depth,
tags: [:queue],
reporter_options: [
otel: [
last_value: [
ttl_ms: 300_000,
max_series: 1_000,
on_overflow: :drop_new
]
]
]
)

Supported options:

Expired series are pruned when new last_value events arrive and when the OTel reader observes the metric.

Scope

Supported today:

Out of scope:

Examples and references

See the runnable examples in:

The first shows the smallest business integration shape. The second shows how to wire the VictoriaMetrics profile into config/runtime.exs.

Useful modules:

See CHANGELOG.md for release history.