Traces every request through a Req client, from one call where the client is built.
# wherever the app builds its Req client:
req =
Req.new(base_url: "https://api.github.com")
|> DDTrace.Integrations.Req.attach()
Req.get!(req, url: "/repos/elixir-lang/elixir")Each request through an attached client becomes one "http.request"
span, opened in the calling process just before the request goes out
and closed when the response comes back. Its resource is the method and
the quantized path — "GET /repos/*/*" — with span.type "http",
span.kind "client", http.method, http.url, http.status_code,
out.host, and the numeric network.destination.port.
The span joins whatever trace the request runs inside: a Phoenix
request, a DDTrace.trace/2 block, a decorated function. A request made
outside any trace roots one of its own.
Attachment is the switch
attach/2 is per client, and it is the only thing that decides what is
traced. A client you did not attach — the tracer's own exporter, a
poller you would rather not see — produces no span and gains no header,
so there is no filter to configure and nothing to exclude.
The headers go out inside the span
The x-datadog-* headers are injected into every attached request, after
the span opens and before the request is sent, so what the downstream
service extracts is the client span itself: its spans land under this
one, exactly where the waterfall wants them. Nothing at the call site
changes, and DDTrace.inject/1 is never written by hand.
The resource is the quantized path
http.url carries the concrete path, "/users/42". The resource is what
requests group by in Datadog, so it carries the path with everything
that looks like an identifier replaced: a segment made of letters,
- and _ is kept, a version segment like v2 is kept, and any other
segment — one with a digit, a dot, a percent-encoding, anything beyond
ASCII letters — becomes *.
GET /users/42/orders GET /users/*/orders
GET /v1/repos/elixir-lang GET /v1/repos/*
GET /files/report.pdf GET /files/*The query string, fragment and userinfo are never on the span, in the
resource or in http.url: a token in a query parameter has no business
in Datadog.
What counts as an error
A response in the client error window marks the span an error — by
default the 4xx range, the inverse of the server-side rule on purpose: a
404 says this request was wrong, while a 500 is the downstream service's
own failure, and that service's own span is where it is an error. The
window is the :http_client_error_statuses setting of DDTrace.Config,
in the same grammar as the server's — single codes and inclusive lo-hi
ranges, comma-separated:
DD_TRACE_HTTP_CLIENT_ERROR_STATUSES="400-499,501"
config :dd_trace_ex, http_client_error_statuses: "400-499,501"A status error carries the flag alone; the status is already on the span.
A request that never gets a response — a refused connection, a timeout,
a name that does not resolve — marks the span with the exception Req
reports, and Req then goes on to handle it as it would have anyway:
Req.get/2 still returns {:error, exception}, and Req.get!/2 still
raises.
Every attempt is a span
The span closes before Req's own retry and redirect steps run, so a
retried request is one span per attempt and a redirect one span per
hop, each with the status it got and the URL it was sent to, siblings
under whatever the caller had open.
A raise out of Req leaves the span to the enclosing one
The span is closed by a step, and a raise that leaves Req altogether —
from a Req.Test plug, from a hook of another plugin — skips every
step after it. Inside a DDTrace.trace/2 block or a traced request,
that costs nothing: the enclosing span finishes the client span as
abandoned when it closes. Outside any trace there is no enclosing span,
and the client span stays current in the process until something
finishes it — every span opened there afterwards lands under it, and
none of them ships. A long-lived process that rescues around an
attached client should make its calls inside a trace block, so the
rescue has a span to unwind to.
Options
:service— the service name for the client spans. Defaults to the tracer's own service, so outbound calls are part of the application that made them.:hooks— customization points, as a keyword list.:requestis the only one: a one-argument function receiving the{request, response}pair, run with the client span current, after every standard tag including the status code and the error mark. It customizes through the ambient mutators, and its return value is discarded. Because the resource is already set when the hook runs, anupdate_span(resource: ...)in it wins. A request that got no response runs no hook: the span already carries the exception.
Req.new(base_url: "https://api.github.com")
|> DDTrace.Integrations.Req.attach(
service: "github",
hooks: [request: &MyApp.Tracing.customize/1]
)
def customize({request, response}) do
DDTrace.update_span(resource: "GET " <> request.url.path)
DDTrace.set_tag("github.rate_limited", response.status == 403)
endA hook that raises is logged and survived: the span finishes with its standard tags, and the request is unaffected. So is a step of this integration's own that fails on a request it cannot read: that request goes out untraced rather than not at all, and the next one through the same client is traced again. Attaching a client twice is attaching it once — the second call returns it as it is — and attaching anything that is not a Req request returns it unchanged, with a word in the log.
There is no :filter option, on purpose: attachment is per client, so
the requests not to trace are the ones through a client that is not
attached. A per-request filter within one shared client is a thing to
add when something needs it.
Finch and Tesla
Req is the only client with an integration, for a reason each. Finch's
telemetry reports a request that is already built and immutable: a
handler could open a span but could never add a header to it, and a
span with no propagation is half of what this integration is for. Tesla
has a middleware mechanism that would fit, and no consumer here to fit
it to. Both are absent until either changes; DDTrace.inject/1 is the
manual way meanwhile.
Summary
Functions
Registers the steps that trace this client's requests, and returns the client.
Functions
Registers the steps that trace this client's requests, and returns the client.
A client already attached is returned as it is, so attaching twice never traces a request twice. Anything that is not a Req request is returned unchanged, with a word in the log; options that make no sense are reported and the defaults used, and the client is attached either way.
See the module documentation for the options.