Monitor OpenTelemetry Erlang/Elixir with Uptrace

This guide explains how to configure the OpenTelemetry Erlang/Elixir SDK to export telemetry data to Uptrace using OTLP/gRPC. You'll learn how to set up comprehensive monitoring for your Erlang and Elixir applications with distributed tracing capabilities.

Prerequisites

Before you begin, ensure you have:

  • An Erlang/Elixir application running on Erlang 23+ and Elixir 1.13+
  • An Uptrace account with a valid DSN (Data Source Name)
  • Basic familiarity with your application's supervision tree

Install dependencies

Add the necessary OpenTelemetry packages to your project dependencies:

elixir
# mix.exs
defp deps do
  [
    {:opentelemetry, "~> 1.3"},
    {:opentelemetry_api, "~> 1.2"},
    {:opentelemetry_exporter, "~> 1.6"}
  ]
end
erlang
%% rebar.config
{deps, [
  {opentelemetry, "~> 1.3"},
  {opentelemetry_api, "~> 1.2"},
  {opentelemetry_exporter, "~> 1.6"}
]}.

Run the appropriate command to install dependencies:

bash
# For Elixir projects
mix deps.get

# For Erlang projects
rebar3 compile

Basic configuration

The OpenTelemetry SDK starts its supervision tree on application boot, so initial configuration must be done through application configuration or environment variables.

elixir
# config/config.exs or config/runtime.exs
config :opentelemetry,
  span_processor: :batch,
  traces_exporter: :otlp

config :opentelemetry_exporter,
  otlp_protocol: :grpc,
  otlp_compression: :gzip,
  otlp_endpoint: "https://api.uptrace.dev:4317",
  otlp_headers: [{"uptrace-dsn", "<YOUR_UPTRACE_DSN>"}]
erlang
%% sys.config
[
 {opentelemetry,
  [{span_processor, batch},
   {traces_exporter, otlp}]},

 {opentelemetry_exporter,
  [{otlp_protocol, grpc},
   {otlp_compression, gzip},
   {otlp_endpoint, "https://api.uptrace.dev:4317"},
   {otlp_headers, [{"uptrace-dsn", "<YOUR_UPTRACE_DSN>"}]}]}
].

Replace <YOUR_UPTRACE_DSN> with your actual Uptrace DSN from your project settings.

For a complete list of available configuration options, see the official OpenTelemetry Exporter documentation.

Environment variables

You can also configure OpenTelemetry using environment variables, which is particularly useful for:

  • Docker deployments and containerized environments
  • CI/CD pipelines
  • Production configurations where credentials shouldn't be hardcoded
bash
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.uptrace.dev:4317
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_TRACES_COMPRESSION=gzip
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="uptrace-dsn=<YOUR_UPTRACE_DSN>"

For a complete list of supported environment variables, see the OpenTelemetry environment configuration documentation.

Application configuration

Configure OpenTelemetry to start as a temporary application to prevent crashes from affecting your main application:

elixir
# mix.exs
def project do
  [
    app: :my_app,
    # ... other config
    releases: [
      my_app: [
        applications: [opentelemetry: :temporary]
      ]
    ]
  ]
end
erlang
%% rebar.config
{relx, [
  {release, {my_app, "0.1.0"}, [
    {opentelemetry, temporary},
    my_app
  ]}
]}.

This ensures that if OpenTelemetry terminates, your main application continues running without telemetry rather than crashing entirely.

Phoenix framework integration

To instrument Phoenix applications with OpenTelemetry, add the Phoenix instrumentation library:

elixir
# mix.exs
defp deps do
  [
    # ... existing deps
    {:opentelemetry_phoenix, "~> 1.1"},
    {:opentelemetry_cowboy, "~> 0.2"}
  ]
end

Configure Phoenix instrumentation in your application start function:

elixir
# lib/my_app/application.ex
def start(_type, _args) do
  # Setup instrumentation before starting supervision tree
  :opentelemetry_cowboy.setup()
  OpentelemetryPhoenix.setup(adapter: :cowboy2)

  children = [
    {Phoenix.PubSub, name: MyApp.PubSub},
    MyAppWeb.Endpoint
  ]

  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end

Ensure your endpoint includes the telemetry plug:

elixir
# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
  # ... other plugs
end

This setup provides automatic instrumentation for:

  • HTTP requests and responses with timing
  • Phoenix controller actions and parameters
  • Template rendering performance
  • Phoenix channels and WebSocket connections
  • Error tracking and status codes

For additional configuration options, see the OpenTelemetry Phoenix documentation.

Ecto database integration

OpenTelemetry Ecto provides comprehensive database operation tracing for Ecto-based applications, automatically instrumenting:

  • Database queries with execution time and parameters
  • Query performance metrics and slow query detection
  • Database connection pool utilization
  • Transaction boundaries and rollbacks
  • Database-specific operations and adapters

Setup

Add the Ecto instrumentation library:

elixir
# mix.exs
defp deps do
  [
    # ... existing deps
    {:opentelemetry_ecto, "~> 1.2"}
  ]
end

Configure Ecto instrumentation in your application start function:

elixir
# lib/my_app/application.ex
def start(_type, _args) do
  # Setup Ecto instrumentation for your repositories
  # Use the telemetry prefix from your repo configuration
  OpentelemetryEcto.setup([:my_app, :repo])

  children = [
    MyApp.Repo,
    # ... other children
  ]

  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end

The telemetry prefix should match your repository's telemetry configuration. For most applications, this follows the pattern [:app_name, :repo], but check your repository module for the exact prefix:

elixir
# lib/my_app/repo.ex
defmodule MyApp.Repo do
  use Ecto.Repo,
    otp_app: :my_app,
    adapter: Ecto.Adapters.Postgres

  # The telemetry prefix defaults to [:my_app, :repo]
end

For advanced configuration options including query sanitization and custom attributes, see the OpenTelemetry Ecto documentation.

Manual instrumentation

For custom spans and manual instrumentation, use the OpenTelemetry API:

elixir
require OpenTelemetry.Tracer

def process_important_task(data) do
  OpenTelemetry.Tracer.with_span "process_task" do
    # Add custom attributes
    OpenTelemetry.Tracer.set_attribute("task.data_size", byte_size(data))

    # Your business logic here
    result = do_complex_work(data)

    # Add more attributes based on results
    OpenTelemetry.Tracer.set_attribute("task.result_count", length(result))

    result
  end
end
erlang
-include_lib("opentelemetry_api/include/otel_tracer.hrl").

process_important_task(Data) ->
    ?with_span(<<"process_task">>, #{}, fun() ->
        % Add custom attributes
        ?set_attribute(<<"task.data_size">>, byte_size(Data)),

        % Your business logic here
        Result = do_complex_work(Data),

        % Add more attributes based on results
        ?set_attribute(<<"task.result_count">>, length(Result)),

        Result
    end).

Verifying your setup

After configuration, your application will automatically start sending telemetry data to Uptrace. To verify the setup:

  1. Start your application with the new configuration
  2. Generate activity by making HTTP requests, database queries, or triggering your instrumented code paths
  3. Check the Uptrace dashboard for incoming traces within 1-2 minutes
  4. Review trace details to ensure spans are being created with proper attributes

Troubleshooting

If traces don't appear in Uptrace:

  • Verify DSN: Double-check your Uptrace DSN in the configuration
  • Check connectivity: Ensure your application can reach https://api.uptrace.dev:4317
  • Review logs: Look for OpenTelemetry error messages in your application logs
  • Test configuration: Use the stdout exporter for local testing:
elixir
# config/dev.exs
config :opentelemetry,
  traces_exporter: {:otel_exporter_stdout, []}

What's next?