DDTrace.Integrations.Phoenix (dd_trace_ex v0.1.0)

Copy Markdown View Source

Traces every request through a Phoenix endpoint, from one call at startup.

# lib/my_app/application.ex
def start(_type, _args) do
  DDTrace.Integrations.Phoenix.setup()

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

Each request becomes one "phoenix.request" span, in the request's own process. Inbound x-datadog-* headers are extracted first, so the span joins the caller's trace; the resource is the method and the route pattern — "GET /users/:id", never the concrete path — and the standard http.* tags are set as the request goes by. Spans your code opens inside the request, with DDTrace.trace/2, a decorator, or DDTrace.Task, land under it with no extra wiring.

What the endpoint has to have

plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] — the line every generated Phoenix endpoint already contains. It is the event this integration listens to. An endpoint without it produces no spans, and nothing else goes wrong.

Options

  • :filter — a one-argument function taking the conn and returning whether to trace it. True means trace. A request it rejects produces no span at all: nothing is extracted, nothing propagates onward, nothing is buffered or sent. Health checks and metrics scrapes are what this is for. Defaults to tracing everything.

  • :hooks — customization points, as a keyword list. :request is the only one: a one-argument function receiving the final conn, run with the request span current, after every standard tag including the status code. It customizes through the ambient mutators, and its return value is discarded. Because the resource is already set when the hook runs, an update_span(resource: ...) in it wins.

  • :endpoint_prefix — the Plug.Telemetry event prefix to listen to. Defaults to [:phoenix, :endpoint]. See "Plug without Phoenix".

DDTrace.Integrations.Phoenix.setup(
  filter: &(&1.request_path not in ["/health", "/metrics"]),
  hooks: [request: &MyApp.Tracing.customize/1]
)

def customize(conn) do
  DDTrace.update_span(resource: custom_route_name(conn))
  DDTrace.set_tag("account.id", conn.assigns.account_id)
end

A filter or hook that raises is logged and survived: the request is unaffected and the integration keeps tracing. A raising filter traces the request rather than dropping it.

Which statuses are errors

Responses in the 500–599 range mark the span an error. Any set of statuses works instead:

DD_TRACE_HTTP_SERVER_ERROR_STATUSES="500-599,429"
config :dd_trace_ex, http_server_error_statuses: "500-599,429"

Single codes and inclusive lo-hi ranges, comma-separated. An unhandled exception does not mark the span by itself: it is remembered, and describes the error only if the response's final status is one of these. An exception your code rescues, or one that error handling turns into a 200, leaves the span unmarked.

Plug without Phoenix

A plain Plug pipeline gets the same request span by pointing setup/1 at its own Plug.Telemetry prefix:

plug Plug.Telemetry, event_prefix: [:my_app, :router]

DDTrace.Integrations.Phoenix.setup(endpoint_prefix: [:my_app, :router])

Everything works the same, with two differences: component is "plug", and the resource comes from the route Plug.Router matched rather than from a Phoenix router.

Two endpoints are two setup/1 calls with two prefixes, and each keeps its own filter: and hooks:. They may be nested — an endpoint that forwards to a router with a Plug.Telemetry of its own — in which case a request through both produces two spans, the inner one a child of the outer. Calling setup/1 twice with the same prefix is a quiet :ok, so a supervision tree that restarts never crashes on it.

The one request it cannot trace

Plug.Telemetry emits its stop event from Plug.Conn.register_before_send/2, so a request process that dies without sending any response never fires it, and that request's span is lost rather than finished. Phoenix's own error rendering sends a response before re-raising, so an exception in a controller is traced normally; this is about a process killed outright.

Summary

Functions

Attaches the handlers that trace requests. Call once, at startup.

Functions

setup(opts \\ [])

@spec setup(keyword()) :: :ok

Attaches the handlers that trace requests. Call once, at startup.

Returns :ok, including when the same prefix is already attached and when the options make no sense — an integration that refused to start would take the application down with it, and the point of tracing is that it cannot.

See the module documentation for the options.