All jobs

Add OpenTelemetry Tracing to a Phoenix App

Yes. An agent wires OpenTelemetry into your Phoenix endpoint, Ecto repo and Oban jobs, exports over OTLP, and opens a pull request that compiles clean.

Run this on RushIn a Project that has your repository.

What the agent does

The agent reads what the app already measures before it adds a tracer. In a Phoenix app like hexpm/hexpm, mix.exs requires Elixir 1.20 and pulls phoenix ~> 1.6, phoenix_live_view ~> 1.0, bandit ~> 1.0, ecto_sql, postgrex, oban ~> 2.23, broadway, finch, sentry ~> 13.0, prom_ex ~> 1.11, telemetry_metrics, telemetry_poller and logger_json ~> 7.0. HexpmWeb.Telemetry is a supervisor with a telemetry_poller and metric definitions for phoenix.endpoint.stop.duration, phoenix.router_dispatch.stop.duration and the hexpm.repo_base.query.* events, which tells the agent the repo is Hexpm.RepoBase, and Hexpm.PromEx ships plugins for Phoenix, Ecto, DbConnection, outbound HTTP, email and Oban. Hexpm.Application.start/2 attaches HexpmWeb.BanditTelemetry and Hexpm.Emails.Telemetry before the children start. Metrics and error reporting exist; what is missing is a trace that joins a request, its queries and the jobs it enqueues.

It adds the dependencies to mix.exs with mix deps.get: opentelemetry_api, opentelemetry, opentelemetry_exporter, and the instrumentation libraries that match what the app runs, opentelemetry_bandit, opentelemetry_phoenix, opentelemetry_ecto, opentelemetry_oban and opentelemetry_finch. In Hexpm.Application.start/2, next to the two attach() calls already there, it adds the setup calls: Bandit and Phoenix with the Bandit adapter so one span covers the request from the socket to the controller, Ecto on the [:hexpm, :repo_base] event prefix the existing metrics already name, and Oban so a job carries the trace of the request that enqueued it. The releases entry in mix.exs gets the application ordering the exporter documents, since the Dockerfile builds a MIX_ENV=prod release.

Configuration follows the file layout the app uses. config/config.exs turns on the batch span processor; config/test.exs sets the traces exporter to none so mix test is unchanged; config/dev.exs uses the stdout exporter so a developer sees a span per request in the console; and config/runtime.exs, which already reads every production value with System.fetch_env!("HEXPM_..."), reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS the same way and sets the OTLP exporter to http_protobuf. The service name comes from the release, and the agent adds the current trace and span ids to the Logger metadata so the JSON lines logger_json writes line up with the trace in the backend.

Then it proves the change compiles and runs. .github/workflows/main.yml runs mix compile --warnings-as-errors and mix format --check-formatted with a Postgres 18 service and the test alias that creates, loads and migrates the database, so the agent starts Postgres in its sandbox, runs the same three commands, and boots the app in dev to make one request to the endpoint and read the span the stdout exporter prints, with the Ecto child spans under it. It touches no controller, no PromEx plugin and no Sentry configuration: the existing metrics keep working beside the new traces.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Add OpenTelemetry tracing to this Phoenix app and open a pull request. Read mix.exs, lib/hexpm/application.ex, lib/hexpm_web/endpoint.ex, lib/hexpm_web/telemetry.ex, lib/hexpm_web/bandit_telemetry.ex, lib/hexpm/prom_ex.ex, every file under config, the Dockerfile and .github/workflows/main.yml first. Add opentelemetry_api, opentelemetry, opentelemetry_exporter and the instrumentation libraries for Bandit, Phoenix, Ecto, Oban and Finch to mix.exs and run mix deps.get. In Hexpm.Application.start/2, beside the existing attach calls, set up Bandit and Phoenix tracing with the Bandit adapter, Ecto tracing on the repo's telemetry prefix, and Oban tracing. Configure the batch processor in config.exs, no exporter in test.exs, the stdout exporter in dev.exs, and in runtime.exs read OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS with System.fetch_env! like the HEXPM_ values and configure the OTLP exporter over http_protobuf. Add the release application ordering the exporter needs in mix.exs, and put the trace and span ids into Logger metadata so logger_json carries them. Start Postgres in the sandbox, run mix compile --warnings-as-errors, mix format --check-formatted and mix test, boot the app in dev, make one request and paste the span the stdout exporter prints in the pull request. Do not change controllers, PromEx plugins or Sentry configuration.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is a pull request that touches mix.exs, mix.lock, lib/hexpm/application.ex and the five files under config, and nothing else. The description lists each dependency added, quotes the setup block as it sits beside the existing attach() calls, and names the two environment variables production will need, with the lines in runtime.exs that read them.

The proof is in the description too: the output of mix compile --warnings-as-errors, mix format --check-formatted and mix test from the branch, and the span tree the stdout exporter printed for one request in dev, with the HTTP span at the root, the Phoenix router span under it and the Ecto query spans under that. The CI workflow runs the same compile and format checks on the pull request.

A reviewer checks that the Ecto prefix matches Hexpm.RepoBase, that test.exs really disables the exporter so the suite does not try to reach a collector, and that the release ordering in mix.exs is right for the Dockerfile build. Deploying means setting the endpoint and headers for the tracing backend; until then the application starts and the exporter logs that it has nowhere to send spans.

Run it on every service without tracing

An organisation that runs several Phoenix services starts one run per repository with POST /o/{org}/sessions, this prompt and the repository in resources. Each run reads that service's own mix.exs, application.ex and config and picks the instrumentation libraries that match what it runs, so a service without Oban gets no Oban setup, and each opens its own pull request.

A webhook_url on the API key receives run.completed for each one with its session_url, so a script can collect the pull request links as the runs finish, and the key's max_concurrent limit decides how many services are being instrumented at once.

The use case this job follows, with its automation and trigger: Hand off from your terminal.

Why run it on Rush?

Adding tracing to an app this size is mostly reading: which adapter the endpoint runs on, what the repo's telemetry prefix is, where production configuration is read, and how the release is built. Getting one of those wrong compiles fine and traces nothing. In a Rush sandbox the agent reads the files, makes the change, and then boots the app and looks at a real span, with the exporter output kept in the transcript next to the diff that produced it.

The pull request carries the same checks CI runs, mix compile --warnings-as-errors among them, so a reviewer sees that the change passes the project's own bar before opening the diff. And because the sandbox had Postgres running, mix test ran for real rather than being skipped as too much trouble on a laptop.

Frequently asked questions

The app already has PromEx metrics and Sentry. Does this replace them?

No. PromEx keeps producing metrics from the same :telemetry events, Sentry keeps capturing exceptions through Sentry.PlugCapture in the endpoint, and the OpenTelemetry libraries attach to those events as another consumer. The pull request changes neither of them.

Will the test suite try to send spans somewhere?

No. config/test.exs sets the traces exporter to none, so mix test runs as before, and the pull request includes the mix test output from the sandbox to show it. Development uses the stdout exporter, so a span prints in the console and nothing leaves the machine.

Which tracing backend does it send to?

Any that accepts OTLP over HTTP. config/runtime.exs reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS at boot, the same way it reads the HEXPM_ values with System.fetch_env!, so the backend and its credentials are a deployment setting rather than something in the repository.

Do Oban jobs show up under the request that enqueued them?

Yes. The Oban instrumentation records the trace context when a job is inserted and continues it when the job runs, so a job's span links back to the request span. The pull request lists the Oban events it attaches to beside the existing Oban PromEx plugins.