Library / SDK
beam-telemetry/telemetry avatar
beam-telemetry/telemetry

telemetry gives the BEAM one event name that every library can publish and anyone can hook

Dynamic dispatching library for metrics and instrumentations.

923 stars74 forksErlangApache-2.0

At a glance

What is it?
The telemetry library is a dispatch mechanism for the Erlang VM and Elixir: libraries emit named events, other code attaches handlers to those names, and every handler runs synchronously on the emitting call. It is deliberately not an OpenTelemetry implementation, and the README points you at separate projects for that.
Who is it for?
telemetry suits library authors who want their internals inspectable without dictating a metrics backend, and application teams who want one event vocabulary across many dependencies. It does not suit a team looking for an OpenTelemetry client, because the README states plainly that the library is agnostic to tooling, and it does not suit a team whose handlers do expensive work inline, because every handler is invoked synchronously on the emitting call.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
Mainly Erlang, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A dispatch table, not a metrics backend

Telemetry is described as a lightweight library for dynamic dispatching of events, with a focus on metrics and instrumentation. Any Erlang or Elixir library can use it to emit events, and application code or other libraries hook into those events and run custom handlers. The important word in that sentence is dispatching. The library does not store, aggregate or export anything, and it does not define a wire format. It gives you a naming convention and a place to register callbacks. The README is direct about the reasoning: currently each library rolls its own instrumentation layer, and Telemetry aims to provide a single interface for these use cases across the whole ecosystem. That is a much smaller promise than a full telemetry backend, and the library is deliberately built so the instrumenting library does not need to know what will later consume its numbers.

Events are a list of atoms, a numeric value and metadata

The shape of an event is fixed and simple. The event name is a list of atoms, such as the three element list web, request, done. Each event is composed of a numeric value and can have metadata attached to it. In Elixir the emission looks like this:

elixir
:telemetry.execute(
  [:web, :request, :done],
  %{latency: latency},
  %{request_path: path, status_code: status}
)

The Erlang form carries the same structure with map syntax:

erlang
telemetry:execute(
  [web, request, done],
  #{latency => Latency},
  #{request_path => Path, status_code => Status}
)

Because the name is a list of atoms rather than a string or a binary, matching it in a handler is a pattern match on the list, and attaching to it is a lookup by the same list. That is the whole addressing scheme, and it is why the library can be called lightweight rather than merely small.

Handlers run synchronously on the emitting call

This is the constraint that decides how you use the library, and the README puts it in bold as an important note. The handle_event callback of each handler is invoked synchronously on each telemetry:execute call, so it is extremely important to avoid blocking operations. If you need to perform any action that is not immediate, the recommendation is to offload the work to a separate process, or a pool of processes, by sending a message. That has a direct performance consequence. The emitting process pays for every attached handler on every event, so the cost of an event is the sum of your handlers, not the cost of one handler. It also means the library is safe to use from inside a request path, but only if your handlers are safe to use there too. Nothing in the library queues, batches or drops work for you.

Attaching a handler with a unique id

A handler is a module and a function, attached to an event name with an id that must be unique. The id matters because it is how you detach later, and because attaching the same id twice replaces the previous handler rather than stacking a second one. In Elixir the attach call returns :ok:

elixir
:ok =
  :telemetry.attach(
    # unique handler id
    "log-response-handler",
    [:web, :request, :done],
    &LogResponseHandler.handle_event/4,
    nil
  )

The fourth argument is the configuration passed through to the handler, and the callback itself has a four argument shape: the event name, the measurements, the metadata and the configuration. The Erlang equivalent takes a binary id and an empty list as the configuration. A handler module in Elixir is a plain defmodule with a handle_event/4 clause:

elixir
defmodule LogResponseHandler do
  require Logger

  def handle_event([:web, :request, :done], measurements, metadata, _config) do
    Logger.info(
      "[#{metadata.request_path}] #{metadata.status_code} sent in #{measurements.latency}"
    )
  end
end

spans give you start, stop and exception with one call

For discrete operations with a beginning and an end, the README recommends the telemetry:span/3 call instead of hand rolling events. It generates a start event and a stop or exception event depending on whether the provided function executed successfully or raised an error, and under the hood it uses telemetry:execute/3, so all the same usage patterns apply. If an exception occurs, an EventPrefix ++ [exception] event is emitted and the caught error is re-raised. The measurements are defined for you: the start event carries a system_time key derived from erlang:system_time/0, the stop and exception events carry a duration key computed as erlang:monotonic_time() minus the start time, and all events include a monotonic_time measurement. Times are native units, so converting requires:

elixir
milliseconds = System.convert_time_unit(duration, :native, :millisecond)

Because the three event names are known in advance, a handler subscribes to them with attach_many rather than three separate attaches.

Attaching to a span means subscribing to three names at once

The span helper emits the start, stop and exception variants under a prefix, so the attachment lists all three:

elixir
:ok =
  :telemetry.attach_many(
    "log-response-handler",
    [
      [:worker, :processing, :start],
      [:worker, :processing, :stop],
      [:worker, :processing, :exception]
    ],
    &LogResponseHandler.handle_event/4,
    nil
  )

The handler for a span receives the event name as its first argument, so one clause can distinguish the three cases by matching on the name, and the measurements differ between them, with system_time on start and duration on stop and exception. The metadata you passed to the span is delivered to the handler alongside whatever the function returned, so a span can carry both the inputs and the outputs of the operation. The Erlang form mirrors this with telemetry:attach_many and a list of three atom lists.

Not OpenTelemetry, and the README says so

There is a note at the top of the README that saves a lot of confusion. The library is agnostic to tooling and therefore is not directly related to OpenTelemetry. For OpenTelemetry in the Erlang VM, the README points to the opentelemetry-erlang project, and to opentelemetry_telemetry in the opentelemetry-erlang-contrib repository to connect the two libraries. That separation is the whole design: telemetry is the in-VM event bus, and the OpenTelemetry work is the bridge that exports those events into an external observability stack. If your requirement is a unified trace pipeline across services, this library is the bottom layer and not the answer on its own. If your requirement is that your library's internals can be inspected by whoever uses it, without committing them to a vendor, then telemetry is exactly the right size.

A Hex dependency and a small Apache-2.0 tree

The installation route is Hex, the package manager for Erlang and Elixir, and adding the dependency to mix.exs is the whole step:

elixir
defp deps() do
  [
    {:telemetry, "~> 1.0"}
  ]
end

The repository is correspondingly small. The top level holds mix.exs, rebar.config and rebar.lock, so the library builds under both Elixir's Mix and plain rebar3, plus src/ and test/ directories, a CHANGELOG.md, a RELEASE.md, a LICENSE and a NOTICE file, and a .codecov.yml alongside the GitHub workflows. The licence field on the repository says Apache-2.0, which carries an explicit patent grant and is a reasonable choice for a library that many others depend on. The last push was on 2026-09-30 and the repository is not archived, although it publishes no GitHub releases, so its version history lives in mix.exs and the changelog rather than in tagged releases.

Editorial conclusion

telemetry suits library authors who want their internals inspectable without dictating a metrics backend, and application teams who want one event vocabulary across many dependencies. It does not suit a team looking for an OpenTelemetry client, because the README states plainly that the library is agnostic to tooling, and it does not suit a team whose handlers do expensive work inline, because every handler is invoked synchronously on the emitting call. Before you adopt it, decide which library will own the event names, and move any handler that touches the network or a database into its own process, because the calling process pays for it synchronously.

Frequently asked questions

How do I install telemetry in an Elixir project?

The library is available on Hex, and the documentation says to add it to your dependencies in mix.exs as {:telemetry, "~> 1.0"} inside the deps list.

Is telemetry related to OpenTelemetry?

No. The README states that this library is agnostic to tooling and is therefore not directly related to OpenTelemetry, and it points to opentelemetry-erlang plus the opentelemetry_telemetry utility for connecting the two.

Are telemetry handlers called asynchronously?

No. The handle_event callback of each handler is invoked synchronously on each telemetry:execute call, so blocking operations must be avoided. The documentation recommends offloading work that is not immediate to a separate process or pool of processes by sending a message.

What does telemetry:span/3 emit?

It generates a start event and a stop or exception event depending on whether the function succeeded or raised, using telemetry:execute/3 underneath. The start event carries a system_time measurement, the stop and exception events carry duration, and all events include monotonic_time, all in native units.

How do I subscribe to a span's events?

Use :telemetry.attach_many with the start, stop and exception event names listed together, since a span emits all three and they are known in advance.

Official sources

  1. beam-telemetry/telemetry on GitHub
  2. Issues
  3. License: Apache-2.0
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/beam-telemetry-telemetry.svg)](https://hysenlabs.com/projects/beam-telemetry-telemetry)

Was this useful?