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:
    bash
    elixir --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:

bash
export OTEL_EXPORTER_OTLP_ENDPOINT="https://<MW_UID>.middleware.io:443"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_HEADERS="authorization=<MW_API_KEY>"
export OTEL_SERVICE_NAME="<service-name>"
export 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:

elixir
config :opentelemetry,
  resource: [service: [name: "<service-name>"]]

config :opentelemetry_exporter,
  otlp_protocol: :http_protobuf,
  otlp_endpoint: "https://<MW_UID>.middleware.io:443",
  otlp_headers: [{"authorization", "<MW_API_KEY>"}]

2 Add dependencies#

Add the OpenTelemetry packages to mix.exs:

elixir
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

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:

bash
mix deps.get

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

elixir
def project do
  [
    app: :my_app,
    # ...
    releases: [
      my_app: [
        applications: [opentelemetry_exporter: :permanent, opentelemetry: :temporary]
      ]
    ]
  ]
end

3 Initialize instrumentation#

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

elixir
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

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#

bash
mix phx.server

Create custom spans#

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

elixir
defmodule MyApp.Orders do
  require OpenTelemetry.Tracer, as: Tracer

  def handle_order(order_id) do
    Tracer.with_span "handle-order" do
      Tracer.set_attributes(%{"order.id": order_id})
      # ...
    end
  end
end

Other deployment targets#

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

bash
docker run \
  -e OTEL_EXPORTER_OTLP_ENDPOINT="https://<MW_UID>.middleware.io:443" \
  -e OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" \
  -e OTEL_EXPORTER_OTLP_HEADERS="authorization=<MW_API_KEY>" \
  -e OTEL_SERVICE_NAME="<service-name>" \
  <your-image>
yaml
env:
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: "https://<MW_UID>.middleware.io:443"
  - name: OTEL_EXPORTER_OTLP_PROTOCOL
    value: "http/protobuf"
  - name: OTEL_EXPORTER_OTLP_HEADERS
    value: "authorization=<MW_API_KEY>"
  - name: OTEL_SERVICE_NAME
    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.

What did you think of this content?