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:

  • keep metric definitions in plain Telemetry.Metrics
  • translate supported metric shapes into OpenTelemetry instruments
  • provide small profile helpers for backend-specific reader configuration

When to use it

Use otel_bridge when:

  • your app already uses Telemetry.Metrics
  • you want to adopt OpenTelemetry metrics without rewriting existing metric definitions
  • you want backend-specific export configuration to stay outside business code

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:

  • :metrics - raw Telemetry.Metrics definitions
  • :specs - modules implementing OtelBridge.Spec
  • :optional_specs - spec modules to load when available
  • :measurements - :telemetry_poller measurements
  • :meta - shared keyword metadata passed to spec modules
  • :poller - :telemetry_poller options
  • :observer_children - custom children for observable or gauge-like metrics

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:

  • groups metrics by telemetry event name
  • extracts measurements from event payloads
  • applies keep filters when present
  • derives exported tags from tag_values
  • carries over unit, description, and explicit OTel reporter options

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:

  • :ttl_ms - deletes stale series after the given age in milliseconds; defaults to :infinity
  • :max_series - maximum retained tag combinations per metric; defaults to :infinity
  • :on_overflow - :drop_new to ignore new tag combinations, or :drop_oldest to evict the oldest retained series; defaults to :drop_new

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

Scope

Supported today:

Out of scope:

  • tracing APIs
  • logs
  • automatic dashboard generation

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.