Quick start

# mix.exs — SDK auto-starts with OTLP HTTP exporter at localhost:4318
{:otel, "~> 0.1"}
scope = %Otel.API.InstrumentationScope{name: "my_app"}
meter = Otel.API.Metrics.MeterProvider.get_meter(scope)

counter = Otel.API.Metrics.Meter.create_counter(meter, "http.requests")
Otel.API.Metrics.Counter.add(counter, 1, %{"http.method" => "GET"})

See Configuration for endpoint, export interval, views, exemplar filter, etc.

Pick an instrument

InstrumentUse whenExample
Countervalue only goes uphttp.server.requests, db.queries
UpDownCountervalue goes up and downqueue.depth, connections.active
Histogramdistribution of valueshttp.server.duration, db.query.duration
Gauge (sync)current value, sample inlinecpu.temperature
ObservableCountercounter measured on demandprocess.runtime.uptime
ObservableUpDownCounterup-down via callbackprocess.runtime.memory
ObservableGaugecurrent value, callback-drivensystem.memory.usage

Sync instruments report each measurement immediately. Async (observable) instruments register a callback that the SDK invokes at each collection cycle to pull the current value (default 60s interval).

Get a meter

scope = %Otel.API.InstrumentationScope{name: "my_app", version: "1.0.0"}
meter = Otel.API.Metrics.MeterProvider.get_meter(scope)

Synchronous instruments

Counter

counter = Otel.API.Metrics.Meter.create_counter(meter, "http.requests",
  unit: "1",
  description: "Number of HTTP requests"
)

Otel.API.Metrics.Counter.add(counter, 1, %{
  "http.method" => "GET",
  "http.status_code" => 200
})

UpDownCounter

gauge_like = Otel.API.Metrics.Meter.create_updown_counter(meter, "queue.depth",
  unit: "1"
)

Otel.API.Metrics.UpDownCounter.add(gauge_like, 1, %{"queue" => "ingest"})
Otel.API.Metrics.UpDownCounter.add(gauge_like, -1, %{"queue" => "ingest"})

Histogram

duration = Otel.API.Metrics.Meter.create_histogram(meter, "http.server.duration",
  unit: "ms",
  description: "HTTP server request duration"
)

Otel.API.Metrics.Histogram.record(duration, 47, %{"http.route" => "/orders/:id"})

Custom bucket boundaries:

Otel.API.Metrics.Meter.create_histogram(meter, "http.server.duration",
  unit: "ms",
  advisory: [explicit_bucket_boundaries: [10, 50, 100, 500, 1000]]
)

Gauge

temperature = Otel.API.Metrics.Meter.create_gauge(meter, "cpu.temperature",
  unit: "Cel"
)

Otel.API.Metrics.Gauge.record(temperature, 67.5, %{"core" => "0"})

Asynchronous (observable) instruments

The callback returns a list of %Measurement{}, one per attribute set. The SDK invokes it at each collection cycle.

ObservableCounter

Otel.API.Metrics.Meter.create_observable_counter(
  meter,
  "process.runtime.uptime",
  fn _args ->
    {wall_clock_ms, _} = :erlang.statistics(:wall_clock)
    [%Otel.API.Metrics.Measurement{value: div(wall_clock_ms, 1000), attributes: %{}}]
  end,
  nil,
  unit: "s"
)

ObservableUpDownCounter

Otel.API.Metrics.Meter.create_observable_updown_counter(
  meter,
  "process.runtime.memory",
  fn _args ->
    info = :erlang.memory()

    [
      %Otel.API.Metrics.Measurement{value: info[:total], attributes: %{"type" => "total"}},
      %Otel.API.Metrics.Measurement{value: info[:processes], attributes: %{"type" => "processes"}}
    ]
  end,
  nil,
  unit: "By"
)

ObservableGauge

Otel.API.Metrics.Meter.create_observable_gauge(
  meter,
  "queue.depth",
  fn _args ->
    [%Otel.API.Metrics.Measurement{value: MyApp.Queue.size(), attributes: %{}}]
  end,
  nil,
  unit: "1"
)

The 4th argument (nil above) is callback_args — passed back into the callback. Useful when one callback feeds multiple instruments via Meter.register_callback/5.

Units

unit: follows UCUM. Common values:

UnitMeaning
"1"dimensionless count
"s", "ms", "us", "ns"seconds / milliseconds / microseconds / nanoseconds
"By", "KiBy", "MiBy", "GiBy"bytes / KiB / MiB / GiB
"Cel"degrees Celsius
"%"percent

Attributes

Every add/3 / record/3 / Measurement{} accepts an attribute map.

Otel.API.Metrics.Counter.add(counter, 1, %{
  "http.method" => "GET",
  "http.status_code" => 200,
  "http.route" => "/orders/:id"
})

Cardinality limit

Each unique attribute set creates a separate time series. The SDK caps this at 2000 per instrument; the (2001)st onward folds into a single otel.metric.overflow=true series so a runaway label doesn't kill the backend. Adjust per-View:

%Otel.SDK.Metrics.View{
  criteria: %{name: "http.requests"},
  config: %{aggregation_cardinality_limit: 500}
}

Views

A View customises how the SDK aggregates a stream of measurements after the instrument is created.

Rename an instrument

%Otel.SDK.Metrics.View{
  criteria: %{name: "old.name"},
  config: %{name: "new.name"}
}

Filter attribute keys

Drop unwanted high-cardinality labels:

%Otel.SDK.Metrics.View{
  criteria: %{name: "http.requests"},
  config: %{attribute_keys: {:include, ["http.method", "http.status_code"]}}
}

Override aggregation

Promote a Counter to a Histogram, or swap to base-2 exponential buckets:

%Otel.SDK.Metrics.View{
  criteria: %{name: "queue.latency"},
  config: %{aggregation: Otel.SDK.Metrics.Aggregation.ExplicitBucketHistogram}
}

Drop

Suppress the instrument entirely (zero application cost):

%Otel.SDK.Metrics.View{
  criteria: %{name: "noisy.metric"},
  config: %{aggregation: Otel.SDK.Metrics.Aggregation.Drop}
}

Wire Views via config :otel, metrics: [views: [...]] or programmatically:

Otel.SDK.Metrics.MeterProvider.add_view(
  Otel.SDK.Metrics.MeterProvider,
  %{name: "queue.latency"},
  %{aggregation: Otel.SDK.Metrics.Aggregation.ExplicitBucketHistogram}
)

Temporality

Default :cumulative — counters report running totals. Switch a reader to :delta per kind:

config :otel,
  metrics: [
    readers: [
      {Otel.SDK.Metrics.MetricReader.PeriodicExporting,
        %{
          exporter: {Otel.OTLP.Metrics.MetricExporter.HTTP, %{}},
          temporality_mapping: %{counter: :delta}
        }}
    ]
  ]

Most Prometheus-derived backends (Mimir, Cortex) expect cumulative; delta works with backends that explicitly support delta-to-cumulative conversion.

Exemplars

Histograms and counters can attach trace exemplars (a sampled trace_id linked to the measurement). Default filter is :trace_based — only sampled spans contribute exemplars.

config :otel, metrics: [exemplar_filter: :always_on]    # every measurement
config :otel, metrics: [exemplar_filter: :always_off]   # none

Limits

See Configuration §"Metrics pillar" for export interval, export timeout, exemplar filter, and other knobs.