For the complete documentation index, see llms.txt. Markdown versions are available by appending .md to documentation URLs.

Elixir OpenTelemetry Instrumentation Guide

SigNoz Cloud - This page applies to SigNoz Cloud editions.
Self-Host - This page applies to self-hosted SigNoz editions.

This guide shows you how to instrument an Elixir application with OpenTelemetry and send traces to SigNoz. The OpenTelemetry Erlang libraries attach to telemetry events, so Phoenix, Ecto, Bandit, Cowboy, Oban, and other libraries produce spans after you start their handlers.

Prerequisites

  • Elixir 1.17 or newer on Erlang/OTP 27 or newer
  • Phoenix 1.7 or newer if you use Phoenix. The opentelemetry_phoenix 2.x line needs it.
  • An instance of SigNoz (either Cloud or Self-Hosted)

Send traces to SigNoz

Step 1. Set environment variables

Set these environment variables in the shell that starts your application:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
export OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
export OTEL_SERVICE_NAME="<service-name>"
export OTEL_RESOURCE_ATTRIBUTES="service.version=<service-version>"

Verify these values:

  • <region>: Your SigNoz Cloud region.
  • <your-ingestion-key>: Your SigNoz ingestion key.
  • <service-name>: A descriptive name for your service (e.g., my-phoenix-app).
  • <service-version> (optional): Your release version, image tag, or git SHA (e.g., 1.4.2, a01dbef8).

Step 2. Add OpenTelemetry dependencies

Add these packages to the deps list in mix.exs:

mix.exs
defp deps do
  [
    # ... your existing dependencies
    {:opentelemetry_exporter, "~> 1.11"},
    {:opentelemetry, "~> 1.7"},
    {:opentelemetry_api, "~> 1.5"},
    {:opentelemetry_phoenix, "~> 2.0"},
    {:opentelemetry_bandit, "~> 0.3"},
    {:opentelemetry_ecto, "~> 1.2"}
  ]
end

Fetch them:

mix deps.get

The packages do different jobs:

  • opentelemetry_exporter sends spans to SigNoz over OTLP.
  • opentelemetry is the SDK, and opentelemetry_api is the API you call for custom spans.
  • opentelemetry_phoenix creates a span for each Phoenix request.
  • opentelemetry_bandit covers the full request lifecycle for the Bandit web server, which is the default in Phoenix 1.8. Use {:opentelemetry_cowboy, "~> 1.0"} instead if your endpoint still runs Cowboy.
  • opentelemetry_ecto creates a span for each Ecto query.

Step 3. Start the instrumentation handlers

Each library ships a setup/0 or setup/1 function that attaches its telemetry handlers. Call them at the top of start/2, before the supervisor starts:

lib/my_app/application.ex
defmodule MyApp.Application do
  @moduledoc false
 
  use Application
 
  @impl true
  def start(_type, _args) do
    OpentelemetryBandit.setup()
    OpentelemetryPhoenix.setup(adapter: :bandit)
    OpentelemetryEcto.setup([:my_app, :repo])
 
    children = [
      MyApp.Repo,
      MyAppWeb.Endpoint
    ]
 
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Verify these values:

  • adapter: is :bandit for Phoenix 1.8 and for any endpoint that uses Bandit.PhoenixAdapter. Use :cowboy2 with :opentelemetry_cowboy.setup() if your endpoint runs Cowboy.
  • [:my_app, :repo] is the telemetry prefix of your Ecto repo. It is your OTP application name and repo module in lowercase atoms, so MyApp.Repo in the :my_app application becomes [:my_app, :repo].

opentelemetry_phoenix reads Plug.Telemetry events, so your endpoint must have this plug. Phoenix generates it by default:

lib/my_app_web/endpoint.ex
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

Step 4. Run your application

Start your application the way you normally do:

mix phx.server

Send a few requests to your application so it produces spans.

Library instrumentation

Add the package for the library you want to trace, then call its setup function next to the ones in Step 3.

Phoenix reports only the part of the request that it handles. Add the web server package as well, or requests that fail at the socket level never produce a span.

Bandit, the default web server in Phoenix 1.8:

mix.exs
{:opentelemetry_bandit, "~> 0.3"}
lib/my_app/application.ex
OpentelemetryBandit.setup()
OpentelemetryPhoenix.setup(adapter: :bandit)

Cowboy, the default in Phoenix 1.7 and earlier:

mix.exs
{:opentelemetry_cowboy, "~> 1.0"}
lib/my_app/application.ex
:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)

To find your adapter, look for the adapter: key under your endpoint configuration in config/config.exs.

Validate

After you run your instrumented application, check that traces reach SigNoz:

  1. Send a few requests to your application.
  2. Open SigNoz and go to Traces.
  3. Select Refresh and look for entries with the service.name you set.
  4. Select a trace to see the request span, and the Ecto spans below it.
SigNoz Traces Explorer listing Phoenix request spans and Ecto query spans from an instrumented Elixir service
Phoenix request spans and their Ecto query spans, filtered by service name

Spans leave in batches, so wait a few seconds after the requests before you look for them.

Troubleshooting

Why don't traces appear in SigNoz?

Confirm the environment variables reach the running application. The OpenTelemetry Erlang libraries read them at start, so a variable that you set after the application starts has no effect. Print them in iex:

System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT")
System.get_env("OTEL_SERVICE_NAME")

Turn on the console exporter in development to see the spans that your application creates:

config/dev.exs
config :opentelemetry, traces_exporter: {:otel_exporter_stdout, []}

If spans print here but do not reach SigNoz, look at the exporter configuration and the network path.

For a full walkthrough covering SDK diagnostics, Collector connectivity, and common ingestion errors (also for logs and metrics), see Debug missing traces, logs, and metrics in SigNoz.

Why do I see no Phoenix request spans?

Check three things in order:

  1. Your endpoint has plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]. opentelemetry_phoenix produces nothing without it.
  2. The adapter: value matches your web server. :bandit and :cowboy2 listen to different telemetry events.
  3. OpentelemetryPhoenix.setup/1 runs in start/2, above the call to Supervisor.start_link/2.

Why do requests show a Phoenix span but no Ecto spans?

The telemetry prefix in OpentelemetryEcto.setup/1 must match your repo. For MyApp.Repo in the :my_app application, the prefix is [:my_app, :repo]. A prefix that does not match attaches a handler to events that never fire, and the call still returns :ok.

Why do exports fail with connection refused or a TLS error?

  • Confirm the endpoint uses https and port 443 for SigNoz Cloud.
  • Confirm the host that runs your application allows outbound HTTPS to ingest.<region>.signoz.cloud.
  • Check the ingestion key. A wrong key returns an HTTP 401 from the endpoint, which the exporter logs as an export failure.
curl -v https://ingest.<region>.signoz.cloud:443/v1/traces

Why does the SDK fail to start with an exporter error?

:opentelemetry_exporter must start before :opentelemetry. List it first in the deps function in mix.exs. For a release, set the application order under releases: as shown in Step 2.

Setup OpenTelemetry Collector (Optional)

What is the OpenTelemetry Collector?

Think of the OTel Collector as a middleman between your app and SigNoz. Instead of your application sending data directly to SigNoz, it sends everything to the Collector first, which then forwards it along.

Why use it?

  • Cleaning up data — Filter out noisy traces you don't care about, or remove sensitive info before it leaves your servers.
  • Keeping your app lightweight — Let the Collector handle batching, retries, and compression instead of your application code.
  • Adding context automatically — The Collector can tag your data with useful info like which Kubernetes pod or cloud region it came from.
  • Future flexibility — Want to send data to multiple backends later? The Collector makes that easy without changing your app.

To send through a Collector, point the endpoint at it and drop the ingestion key header. The Collector holds the key instead:

export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
export OTEL_SERVICE_NAME="<service-name>"

See Switch from direct export to Collector for step-by-step instructions to convert your setup.

For more details, see Why use the OpenTelemetry Collector? and the Collector configuration guide.

Next steps

Get Help

If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack. If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.

Is this page helpful

Last updated—September 21, 2026

Edit on GitHub