telemetry is a free, open source monitoring & observability project written in Erlang and released under Apache-2.0. It has 923 GitHub stars, 76 forks and 6 open issues, and was last pushed 1 months ago. On this registry it ranks #232 of 270 tracked projects in Monitoring & Observability, with 5 head-to-head comparisons available.

What is telemetry?

Telemetry is a lightweight Erlang and Elixir library for dynamic dispatching of events, aimed at library authors who need to emit instrumentation and at application developers who want to hook custom handlers onto those events.

What it is

Telemetry is a small library that gives the Erlang VM ecosystem a single, shared interface for emitting and consuming instrumentation events. An event is identified by a name that is a list of atoms, such as [web, request, done], and each event carries a numeric measurement map plus a metadata map. Libraries call telemetry:execute/3 to publish an event; application code calls telemetry:attach/4 to bind a module function or anonymous function to that event name, and the attached handler runs whenever the event fires. The same usage pattern applies in both languages, with Elixir using :telemetry.execute/3 and Erlang using telemetry:execute/3.

The problem it solves is instrumentation duplication. Before Telemetry, each library in the Erlang and Elixir ecosystem rolled its own instrumentation layer, so consumers had to learn a different reporting mechanism for every dependency they wanted to observe. Telemetry replaces those per-library mechanisms with one dispatch API, so a single handler can subscribe to events from many libraries at once. The README is explicit that the library is agnostic to tooling and is not directly related to OpenTelemetry.

Key capabilities

  • Publishes events with telemetry:execute/3, taking an event name as a list of atoms along with a measurements map and a metadata map.
  • Binds handlers with telemetry:attach/4, which requires a unique handler ID, the event name, the handler function, and configuration passed through to that handler.
  • Invokes handlers through the handle_event/4 callback, receiving the event name, measurements, metadata, and config.
  • Generates paired start and stop events with telemetry:span/3, which wraps a function and emits EventPrefix ++ [start] followed by a stop or exception event.
  • Emits an EventPrefix ++ [exception] event when the wrapped function raises, then re-raises the caught error.
  • Populates the start event's measurements with a system_time key derived from erlang:system_time/0.
  • Supports attaching handlers written in either Elixir or Erlang against the same event names.

Who uses it and how

  • Library authors across Erlang and Elixir emit events from inside their own code, so downstream users can observe library behaviour without patching it.
  • Application developers attach handlers that log request latency and response status codes, as in the README's example that logs latency, request_path, and status_code on a [:web, :request, :done] event.
  • Teams building introspection tooling subscribe once to many library events instead of integrating each library's bespoke instrumentation separately.
  • Operators needing uniform timing data use telemetry:span/3 around discrete operations to get matched start and stop events.

Getting started

Telemetry is an Erlang library published through Hex, with documentation hosted at https://hexdocs.pm/telemetry. Adoption consists of adding the dependency and then calling telemetry:attach/4 against the telemetry:execute/3 event names the libraries in use already emit.

How it compares

Telemetry is tooling-agnostic and sits below the OpenTelemetry stack rather than competing with it. Projects that need OpenTelemetry semantics in the Erlang VM use opentelemetry-erlang, and opentelemetry_telemetry exists specifically to connect that library to Telemetry events.

When to use it — and when not to

There is no server, database, or mail service to operate: it is an in-process library, so the real cost is that the handle_event callback runs synchronously on every telemetry:execute call, and blocking work inside a handler stalls the caller unless it is offloaded to another process or pool. Teams that expect an out-of-the-box OpenTelemetry exporter should not adopt it directly, because it ships no exporter and no backend integration. The licence is Apache-2.0 and the project remains active, with the last push recorded on 2026-08-22.

project readme (upstream, from github) — read inline

Telemetry

Codecov

Documentation

Telemetry is a lightweight library for dynamic dispatching of events, with a focus on metrics and instrumentation. Any Erlang or Elixir library can use telemetry to emit events. Application code and other libraries can then hook into those events and run custom handlers.

Note: this library is agnostic to tooling and therefore is not directly related to OpenTelemetry. For OpenTelemetry in the Erlang VM, see opentelemetry-erlang, and check opentelemetry_telemetry to connect both libraries.

Usage

In a nutshell, you register a custom module and function to be invoked for certain events, which are executed whenever there is such an event. The event name is a list of atoms. Each event is composed of a numeric value and can have metadata attached to it. Let's look at an example.

Imagine that you have a web application and you'd like to log latency and response status for each incoming request. With Telemetry, you can build a module which does exactly that whenever a response is sent. The first step is to execute a measurement.

In Elixir:

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

In Erlang:

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

Then you can create a module to be invoked whenever the event happens.

In 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

In Erlang:

-module(log_response_handler).

-include_lib("kernel/include/logger.hrl").

handle_event([web, request, done], #{latency := Latency}, #{request_path := Path,
                                                            status_code := Status}, _Config) ->
  ?LOG_INFO("[~s] ~p sent in ~p", [Path, Status, Latency]).

Important note:

The handle_event callback of each handler is invoked synchronously on each telemetry:execute call. Therefore, it is extremely important to avoid blocking operations. If you need to perform any action that is not immediate, consider offloading the work to a separate process (or a pool of processes) by sending a message.

Finally, all you need to do is to attach the module to the executed event.

In Elixir:

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

In Erlang:

ok = telemetry:attach(
  %% unique handler id
  <<"log-response-handler">>,
  [web, request, done],
  fun log_response_handler:handle_event/4,
  []
)

You might think that it isn't very useful, because you could just as well write a log statement instead of calling telemetry:execute/3 – and you would be right! But now imagine that each Elixir library would publish its own set of events with information useful for introspection. Currently each library rolls their own instrumentation layer – Telemetry aims to provide a single interface for these use cases across the whole ecosystem.

Spans

In order to provide uniform events that capture the start and end of discrete events, it is recommended that you use the telemetry:span/3 call. This function will generate a start event and a stop or exception event depending on whether the provided function executed successfully or raised an error. Under the hood, the telemetry:span/3 function leverages the telemetry:execute/3 function, so all the same usage patterns apply. If an exception does occur, an EventPrefix ++ [exception] event will be emitted and the caught error will be re-raised.

The measurements for the EventPrefix ++ [start] event will contain a key called system_time which is derived by calling erlang:system_time/0. For EventPrefix ++ [stop] and EventPrefix ++ [exception] events, the measurements will contain a key called duration and its value is derived by calling erlang:monotonic_time() - StartMonotonicTime. All events include a monotonic_time measurement too. All of them represent time as native units.

To convert the duration from native units you can use:

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

To create span events you would do something like this:

In Elixir:

def process_message(message) do
  start_metadata = %{message: message}

  result =
    :telemetry.span(
      [:worker, :processing],
      start_metadata,
      fn ->
        result = ... # Process the message
        {result, %{metadata: "Information related to the processing of the message"}}
      end
    )
end

In Erlang:

process_message(Message) ->
  StartMetadata =  #{message => Message},
  Result = telemetry:span(
    [worker, processing],
    StartMetadata,
    fun() ->
      Result = % Process the message
      {Result, #{metadata => "Information related to the processing of the message"}}
    end
  ).

To then attach to the events that telemetry:span/3 emits you would do the following:

In Elixir:

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

In Erlang:

ok = telemetry:attach_many(
  <<"log-response-handler">>,
  [
    [worker, processing, start],
    [worker, processing, stop],
    [worker, processing, exception]
  ],
  fun log_response_handler:handle_event/4,
  []
)

With the following event handler module defined:

In Elixir:

defmodule LogResponseHandler do
  require Logger

  def handle_event(event, measurements, metadata, _config) do
    Logger.info("Event: #{inspect(event)}")
    Logger.info("Measurements: #{inspect(measurements)}")
    Logger.info("Metadata: #{inspect(metadata)}")
  end
end

In Erlang:

-module(log_response_handler).

-include_lib("kernel/include/logger.hrl").

handle_event(Event, Measurements, Metadata, _Config) ->
  ?LOG_INFO("Event: ~p", [Event]),
  ?LOG_INFO("Measurements: ~p", [Measurements]),
  ?LOG_INFO("Metadata: ~p", [Metadata]).

Installation

Telemetry is available on Hex. To install, just add it to your dependencies in mix.exs:

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

or rebar.config:

{deps, [{telemetry, "~> 1.0"}]}.

Copyright and License

Copyright (c) 2019 Erlang Ecosystem Foundation and Erlang Solutions.

Telemetry's source code is released under the Apache License, Version 2.0.

See the LICENSE and NOTICE files for more information.

Frequently asked questions

Is telemetry free to use?

telemetry is open source under the Apache-2.0 licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does telemetry do?

Dynamic dispatching library for metrics and instrumentations.

What is telemetry written in?

telemetry is primarily written in Erlang. Its source is publicly available at https://github.com/beam-telemetry/telemetry, and it has 923 GitHub stars.