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_phoenix2.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 1. Set environment variables
Add these environment variables to your deployment manifest:
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: 'https://ingest.<region>.signoz.cloud:443'
- name: OTEL_EXPORTER_OTLP_HEADERS
value: 'signoz-ingestion-key=<your-ingestion-key>'
- name: OTEL_SERVICE_NAME
value: '<service-name>'
- name: OTEL_RESOURCE_ATTRIBUTES
value: '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).
Store the ingestion key in a Kubernetes Secret and reference it with valueFrom.secretKeyRef instead of writing it into the manifest.
Step 1. Set environment variables
Add the environment variables to your Dockerfile:
ENV OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
ENV OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
ENV OTEL_SERVICE_NAME="<service-name>"
ENV OTEL_RESOURCE_ATTRIBUTES="service.version=<service-version>"Or pass them at run time with docker run:
docker run -e OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443" \
-e OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>" \
-e OTEL_SERVICE_NAME="<service-name>" \
-e OTEL_RESOURCE_ATTRIBUTES="service.version=<service-version>" \
your-image:latestVerify 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 1. Set environment variables (PowerShell)
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "https://ingest.<region>.signoz.cloud:443"
$env:OTEL_EXPORTER_OTLP_HEADERS = "signoz-ingestion-key=<your-ingestion-key>"
$env:OTEL_SERVICE_NAME = "<service-name>"
$env: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:
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"}
]
endFetch them:
mix deps.getThe packages do different jobs:
opentelemetry_exportersends spans to SigNoz over OTLP.opentelemetryis the SDK, andopentelemetry_apiis the API you call for custom spans.opentelemetry_phoenixcreates a span for each Phoenix request.opentelemetry_banditcovers 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_ectocreates 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:
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
endVerify these values:
adapter:is:banditfor Phoenix 1.8 and for any endpoint that usesBandit.PhoenixAdapter. Use:cowboy2with: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, soMyApp.Repoin the:my_appapplication becomes[:my_app, :repo].
opentelemetry_phoenix reads Plug.Telemetry events, so your endpoint must have this plug. Phoenix generates it by default:
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]Step 4. Run your application
Start your application the way you normally do:
mix phx.serverSend 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:
{:opentelemetry_bandit, "~> 0.3"}OpentelemetryBandit.setup()
OpentelemetryPhoenix.setup(adapter: :bandit)Cowboy, the default in Phoenix 1.7 and earlier:
{:opentelemetry_cowboy, "~> 1.0"}:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)To find your adapter, look for the adapter: key under your endpoint configuration in config/config.exs.
opentelemetry_ecto creates a span for each query, with the source table and the database system as attributes.
{:opentelemetry_ecto, "~> 1.2"}OpentelemetryEcto.setup([:my_app, :repo])The argument is the telemetry prefix of the repo. For MyApp.Repo the prefix is [:my_app, :repo]. Call setup/2 once for each repo if your application has more than one.
The query text is off by default, because a query can carry personal data. To record it, set db_statement:
OpentelemetryEcto.setup([:my_app, :repo], db_statement: :enabled)db_statement also accepts a function of one argument. The function receives the query and returns the text to record, which lets you strip values before they leave your servers.
opentelemetry_oban creates a span for each job that Oban runs, and links the job span to the request that inserted the job.
{:opentelemetry_oban, "~> 1.2"}OpentelemetryOban.setup()Insert jobs through OpentelemetryOban.insert instead of Oban.insert, so the trace context travels with the job:
OpentelemetryOban.insert(MyApp.Workers.SendEmail.new(%{user_id: 1}))An HTTP client package creates a client span for each outgoing call. Turn on trace header propagation as well, and the trace continues into the service you call.
Req:
{:opentelemetry_req, "~> 1.0"}req =
Req.new(url: "https://example.com")
|> OpentelemetryReq.attach(propagate_trace_headers: true)
Req.get!(req)propagate_trace_headers is false by default, so set it to send the traceparent header with the request.
Tesla:
{:opentelemetry_tesla, "~> 2.5"}Add the middleware to your Tesla client. Place it before Tesla.Middleware.PathParams, so the span name keeps the parameterized path (/users/:id) instead of the resolved one (/users/123):
client =
Tesla.client([
{Tesla.Middleware.BaseUrl, "https://example.com"},
Tesla.Middleware.OpenTelemetry,
Tesla.Middleware.PathParams
])
Tesla.get(client, "/users/:id", opts: [path_params: [id: "123"]])The OpenTelemetry Erlang contrib repository ships handlers for more libraries. Each one follows the same pattern: add the package, then call its setup function in start/2.
| Library | Package |
|---|---|
| Absinthe (GraphQL) | opentelemetry_absinthe |
| Broadway | opentelemetry_broadway |
| Commanded | opentelemetry_commanded |
| Dataloader | opentelemetry_dataloader |
| Nebulex | opentelemetry_nebulex |
| Redix | opentelemetry_redix |
| Xandra (Cassandra) | opentelemetry_xandra |
See the instrumentation directory for the current list and the options each package accepts.
Validate
After you run your instrumented application, check that traces reach SigNoz:
- Send a few requests to your application.
- Open SigNoz and go to Traces.
- Select Refresh and look for entries with the
service.nameyou set. - Select a trace to see the request span, and the Ecto spans below it.

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 :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:
- Your endpoint has
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint].opentelemetry_phoenixproduces nothing without it. - The
adapter:value matches your web server.:banditand:cowboy2listen to different telemetry events. OpentelemetryPhoenix.setup/1runs instart/2, above the call toSupervisor.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
httpsand port443for 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/tracesWhy 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
- For a hands-on walkthrough, our Elixir OpenTelemetry tutorial sets up a Phoenix and Ecto application step by step.
- Need custom spans or attributes of your own? See the OpenTelemetry Erlang and Elixir instrumentation guide.
- Set up alerts for your Elixir service.
- Create dashboards to visualize your metrics.
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.