Setting up the tracer

Copy Markdown View Source

This guide walks a new application from zero to traces in Datadog: add the dependency, point the tracer at an agent, name the service, and turn on the integrations. Every setting named here is described in full in DDTrace.Config — that module is the reference; this page is the path through it.

Install

Add the dependency:

# mix.exs
defp deps do
  [
    {:dd_trace_ex, "~> 0.1.0"}
  ]
end

That is the whole installation. dd_trace_ex is an OTP application that starts with your release: there is no tracer module to define, no child to add to your supervision tree, and no use line in your application. Code that calls the trace/2 macro needs a require DDTrace (or an import); everything else on the module is plain functions.

Point it at an agent

Spans travel to a Datadog agent, which forwards them to Datadog. The tracer assumes the standard address, http://localhost:8126, so a host with a local agent needs nothing configured.

When the agent lives elsewhere — another container, a sidecar, a node port — say where, either way:

config :dd_trace_ex, agent_url: "http://datadog-agent:8126"

or through the environment the agent's own images already speak: DD_TRACE_AGENT_URL, or DD_AGENT_HOST and DD_TRACE_AGENT_PORT. Only http:// URLs are supported.

An unreachable agent never harms the application: submissions fail, the failure is logged, and the request that produced the spans is unaffected.

Name the service

Three values identify everything the tracer sends — the service's name, its deployment environment, and its version. Datadog calls this unified service tagging.

# config/runtime.exs
config :dd_trace_ex, service: "storefront"

The same three arrive as DD_SERVICE, DD_ENV and DD_VERSION with no config at all — every setting resolves from application config first, then its DD_* environment variable, then its default. Application config wins, matching the other Datadog tracers. Naming the service in config and leaving env and version to the environment is the usual split: the name is the application's to state, and the other two are facts about the deployment.

A tracer configured with no service ships spans as unnamed-elixir-service: visible in the UI, and there to be recognized as a tracer nobody configured.

Verify

The tracer logs its resolved settings in one line at boot:

[info] dd_trace_ex started, config: %{service: "storefront", env: "prod", ...}

The same facts are available on demand from DDTrace.info/0. Then open a span and watch it arrive:

require DDTrace

DDTrace.trace "smoke.test" do
  :ok
end

Spans buffer and flush on a cadence, so give it a few seconds before looking for the trace in Datadog's UI.

Trace Phoenix requests

One call at startup, before the endpoint:

# 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

Every request becomes a "phoenix.request" span, resourced by its route pattern, carrying the standard http.* tags, and joined to the caller's trace when the request arrived with x-datadog-* headers. DDTrace.Integrations.Phoenix documents the options: a filter: for health checks, a request: hook for custom tags, and what the endpoint has to have (the Plug.Telemetry line every generated endpoint already contains).

Trace Ecto queries

One call per repo, also at startup:

DDTrace.Integrations.Ecto.setup(repo: MyApp.Repo)

Every query becomes a span with the SQL as its resource, the connection's db.* tags, and Ecto's queue/query/decode breakdown as numeric metrics, landing under whatever trace the query ran inside. DDTrace.Integrations.Ecto documents the options and the decisions behind them — most notably that bind parameters never leave the database.

Trace outbound Req requests

One call where the client is built:

req =
  Req.new(base_url: "https://api.github.com")
  |> DDTrace.Integrations.Req.attach()

Req.get!(req, url: "/repos/elixir-lang/elixir")

Every request through an attached client becomes an "http.request" span under whatever trace it ran inside, with the x-datadog-* headers injected before it goes out, so the service on the other end continues the trace. A client you do not attach is not traced. DDTrace.Integrations.Req documents the options and what counts as an error — the 4xx window, the inverse of the server's.

Trace your own functions

The trace/2 block macro is the primitive; the decorator is the same span without restructuring the function:

defmodule MyApp.Orders do
  use DDTrace.Decorators

  @decorate trace()
  def process(order) do
    DDTrace.set_tag("order.channel", order.channel)
    # ...
  end
end

See DDTrace for the block macro, the mutators, and crossing processes; DDTrace.Decorators for decorating whole modules.

Continue traces across services

Outbound, an attached Req client injects the trace's headers by itself. Any other HTTP client carries the trace the same way, by hand:

headers = DDTrace.inject([{"content-type", "application/json"}])
Finch.build(:post, url, headers, body) |> Finch.request(MyApp.Finch)

Inbound, the Phoenix integration extracts automatically. A non-HTTP entry point extracts by hand with DDTrace.extract/1 and opens its span with parent:.

Correlate logs with traces

While a span is open, the process's Logger metadata carries the dd.trace_id family of keys, which Datadog's UI uses to link a log line to its trace. Any JSON logger that emits metadata gets this for free. A metadata: allowlist has to spell the keys as the quoted atoms they are:

config :logger, :default_formatter,
  metadata: [:request_id, :"dd.trace_id", :"dd.span_id", :"dd.service"]

metadata: :all needs no list. DDTrace.Config has the full story, including turning injection off.

Assert on spans in tests

use DDTrace.Test in a test module captures every span the test finishes — no agent involved — and imports assert_span and friends:

use ExUnit.Case, async: true
use DDTrace.Test

test "processing an order is traced" do
  MyApp.Orders.process(order)

  assert_span "order.process", resource: "ImportOrders"
end

DDTrace.Test documents the matchers and how spans find their test across processes.

Every setting

The table in DDTrace.Config lists every setting, its DD_* variable, and its default — tracing on/off, the agent address, log injection, the x-datadog-tags budget, which HTTP statuses count as errors, and the shutdown flush budget. Bad configuration degrades the tracer; it never stops the host application from booting.