Elixir

This guide shows you how to instrument an Elixir application with the upstream OpenTelemetry Erlang/Elixir SDK and send traces directly to Middleware. It covers Phoenix, Bandit (or Cowboy) and Ecto out of the box. No Middleware Host Agent is required: the OTLP exporter in your application sends data straight to your Middleware account.

Prerequisites#

  • Elixir 1.15+ on Erlang/OTP 25+. Check using:
    1elixir --version
  • Middleware account & project: your Middleware UID (https://<uid>.middleware.io) and API key (MW_API_KEY)
  • Network access from your application to https://<uid>.middleware.io:443

For a deeper overview of Middleware's OTLP endpoints and headers, see the OpenTelemetry getting started guide.

Setup#

1 Set environment variables#

The OpenTelemetry exporter reads its configuration from standard environment variables. Set them wherever your application runs:

1export OTEL_EXPORTER_OTLP_ENDPOINT="https://<MW_UID>.middleware.io:443"
2export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
3export OTEL_EXPORTER_OTLP_HEADERS="authorization=<MW_API_KEY>"
4export OTEL_SERVICE_NAME="<service-name>"
5export OTEL_RESOURCE_ATTRIBUTES="service.version=<service-version>,deployment.environment=<environment>"

Replace:

  • <uid> with your Middleware project UID.
  • <MW_API_KEY> with your Middleware API key.
  • <service-name> with a meaningful name for this service (for example, my-phoenix-app).

Set OTEL_EXPORTER_OTLP_ENDPOINT to the root URL only. The OTLP/HTTP exporter appends /v1/traces for you.

Prefer application config over environment variables? Set the same values in config/runtime.exs:

1config :opentelemetry,
2  resource: [service: [name: "<service-name>"]]
3
4config :opentelemetry_exporter,
5  otlp_protocol: :http_protobuf,
6  otlp_endpoint: "https://<MW_UID>.middleware.io:443",
7  otlp_headers: [{"authorization", "<MW_API_KEY>"}]

2 Add dependencies#

Add the OpenTelemetry packages to mix.exs:

1defp deps do
2  [
3    # ... your existing dependencies
4    {:opentelemetry_exporter, "~> 1.11"},
5    {:opentelemetry, "~> 1.7"},
6    {:opentelemetry_api, "~> 1.5"},
7    {:opentelemetry_phoenix, "~> 2.0"},
8    {:opentelemetry_bandit, "~> 0.3"},
9    {:opentelemetry_ecto, "~> 1.2"}
10  ]
11end

Keep :opentelemetry_exporter above :opentelemetry so the exporter starts first. Leave out opentelemetry_phoenix, opentelemetry_bandit or opentelemetry_ecto if your app doesn't use that library.

Then fetch the dependencies:

1mix deps.get

If you deploy with mix release, start the exporter before the SDK:

1def project do
2  [
3    app: :my_app,
4    # ...
5    releases: [
6      my_app: [
7        applications: [opentelemetry_exporter: :permanent, opentelemetry: :temporary]
8      ]
9    ]
10  ]
11end

3 Initialize instrumentation#

Attach the instrumentation handlers in lib/my_app/application.ex, before the supervision tree starts:

1defmodule MyApp.Application do
2  @moduledoc false
3
4  use Application
5
6  @impl true
7  def start(_type, _args) do
8    OpentelemetryBandit.setup()
9    OpentelemetryPhoenix.setup(adapter: :bandit)
10    OpentelemetryEcto.setup([:my_app, :repo])
11
12    children = [
13      MyApp.Repo,
14      MyAppWeb.Endpoint
15    ]
16
17    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
18    Supervisor.start_link(children, opts)
19  end
20end

Running Cowboy instead of Bandit? Replace {:opentelemetry_bandit, "~> 0.3"} with {:opentelemetry_cowboy, "~> 1.0"}, call :opentelemetry_cowboy.setup() instead of OpentelemetryBandit.setup(), and use OpentelemetryPhoenix.setup(adapter: :cowboy2).

OpentelemetryEcto.setup/1 takes your repo's telemetry prefix. By default this is your app name followed by :repo, for example [:my_app, :repo].

4 Run your application#

1mix phx.server

Create custom spans#

Wrap your own code in spans with OpenTelemetry.Tracer, and attach attributes you can filter on in Middleware:

1defmodule MyApp.Orders do
2  require OpenTelemetry.Tracer, as: Tracer
3
4  def handle_order(order_id) do
5    Tracer.with_span "handle-order" do
6      Tracer.set_attributes(%{"order.id": order_id})
7      # ...
8    end
9  end
10end

Other deployment targets#

The OpenTelemetry configuration is the same everywhere; only the way you declare environment variables changes.

1docker run \
2  -e OTEL_EXPORTER_OTLP_ENDPOINT="https://<MW_UID>.middleware.io:443" \
3  -e OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" \
4  -e OTEL_EXPORTER_OTLP_HEADERS="authorization=<MW_API_KEY>" \
5  -e OTEL_SERVICE_NAME="<service-name>" \
6  <your-image>
1env:
2  - name: OTEL_EXPORTER_OTLP_ENDPOINT
3    value: "https://<MW_UID>.middleware.io:443"
4  - name: OTEL_EXPORTER_OTLP_PROTOCOL
5    value: "http/protobuf"
6  - name: OTEL_EXPORTER_OTLP_HEADERS
7    value: "authorization=<MW_API_KEY>"
8  - name: OTEL_SERVICE_NAME
9    value: "<service-name>"

Store the API key in a Kubernetes Secret and reference it with valueFrom.secretKeyRef in production.

View your data#

After you start the application and send some traffic, wait 3–5 minutes, then open APM → Services or the Traces explorer in Middleware and filter by your service name.

Environment variables#

VariablePurposeExample
OTEL_EXPORTER_OTLP_ENDPOINTMiddleware OTLP endpoint for your project.https://<uid>.middleware.io:443
OTEL_EXPORTER_OTLP_PROTOCOLOTLP transport used by the exporter.http/protobuf
OTEL_EXPORTER_OTLP_HEADERSAuthenticates requests with your Middleware API key.authorization=<MW_API_KEY>
OTEL_SERVICE_NAMEService name shown in Middleware APM.my-phoenix-app
OTEL_RESOURCE_ATTRIBUTESExtra resource attributes attached to every span.service.version=1.4.2,deployment.environment=prod

Troubleshooting#

  • No data in Middleware: confirm the environment variables are set in the BEAM process that runs your app, and that the endpoint does not already include /v1/traces.
  • Spans dropped in releases: make sure opentelemetry_exporter is listed before opentelemetry in deps and in the release applications list, as shown in step 2.
  • Ecto queries missing: check that the prefix passed to OpentelemetryEcto.setup/1 matches your repo's telemetry_prefix.
  • Debugging locally: to print spans to stdout instead of exporting them, temporarily add config :opentelemetry, traces_exporter: {:otel_exporter_stdout, []} to your config. The Erlang/Elixir SDK does not support OTEL_TRACES_EXPORTER=console.

Need assistance or want to learn more about Middleware? Get in touch with us via our Contact Us or join our Slack channel.