Assert on the spans your code produces, without an agent.
defmodule MyApp.OrdersTest do
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"
refute_span "analytics.record_event"
assert find_span("db.insert").parent_id == find_span("order.process").span_id
end
enduse DDTrace.Test captures every span the test finishes and imports the
four matchers below. Capture happens where a span finishes, before
anything is sent anywhere, so a suite asserts on spans whether or not
the tracer is enabled and whether or not an agent exists.
Matchers are subset matches: they check the fields you name and
ignore the rest, so a span with more tags than you asserted still
matches. They are also point in time — they assert on what has been
captured by the time they run and never wait, so Task.await/2 your work
before asserting.
Capturing at finish means a captured span does not yet carry the
trace-level markings — the sampling priority, _dd.p.tid, _dd.p.dm —
which are written to the first span of the chunk once the whole trace is
complete. Those are facts about the trace, so assert on them against the
chunk rather than here. A DDTrace.keep_trace/0 in particular cannot be
asserted with assert_span/2: it changes no span, only the chunk the
trace ships as.
Which test a span belongs to
Work a test hands to another process is still that test's, and
concurrent tests never see each other's spans — so a suite that spawns
runs async: true. Three things are asked in order, and the first one
with an answer settles it.
The process finishing the span, when it started a capture of its own. A process that said it is an assertion scope reads its own spans back, whatever trace it is running inside.
The trace the span is in, when a test snapshotted it. A
DDTrace.Task child carries its spawning test's
DDTrace.current_context/0 snapshot, so its spans land in that test's
trace, in that test — including a stream built in one process and
enumerated in another, or after the trace that built it has finished.
So does any other process handed a snapshot, which is how a test asserts
on a long-lived GenServer it hands its context to.
The chain of processes that spawned this one, nearest first, for a
span in no snapshotted trace. This is what carries a bare Task: its
spans are captured as a trace root of their own, which is what a bare
Task exports in production too.
A process none of the three reaches — one started with spawn/1, or a
long-lived GenServer answering a call it was handed no context with —
is captured by no test rather than an arbitrary one.
Strict mode
Fail-open is a production posture, not a licence for a typo to survive
CI — so under use DDTrace.Test a structural mistake raises instead
of being logged: an option or field no span has, an argument of the
wrong shape, something that is not a DDTrace.SpanContext handed over
as one. These are deterministic bugs, and a test that provokes one
should fail.
A runtime data problem still degrades and logs, strict or not: a tag whose value has no string form is bad data rather than a bug in the call, and the span ships without it exactly as it would in production. So do the exporter's own troubles — an unreachable agent, a full buffer — which say nothing about the code under test.
The error is raised where the mistake was made, in whatever process
made it, so a Task that gets an option wrong crashes with a stacktrace
pointing at the call rather than reporting somewhere the context is
gone.
config :dd_trace_ex, strict: true turns strict on for a whole run,
including the tests that do not use DDTrace.Test at all. To test the
fail-open behaviour itself, turn it off for that module:
use DDTrace.Test, strict: falseTo write the matchers without parentheses, as above, add this library to your formatter's imported dependencies:
# .formatter.exs
[import_deps: [:dd_trace_ex]]
Summary
Types
The DDTrace.Span fields to match, as a keyword list.
Functions
Asserts that a captured span matches name and match, and returns it.
Returns the oldest captured span matching name and match, or nil.
Asserts that no captured span matches name and match.
Returns every span captured so far, oldest finish first.
Starts capturing spans finished in the calling process.
Stops owner capturing and forgets the spans it captured.
Types
@type match() :: keyword()
The DDTrace.Span fields to match, as a keyword list.
:meta and :metrics are themselves matched as subsets; every other
field is compared for equality. A field no span has raises, since it
could otherwise only ever fail to match.
Functions
@spec assert_span(String.t(), match()) :: DDTrace.Span.t() | no_return()
Asserts that a captured span matches name and match, and returns it.
Failure prints every span the test captured.
Examples
assert_span "order.process", meta: %{"account.id" => "acct_123"}
@spec find_span(String.t(), match()) :: DDTrace.Span.t() | nil
Returns the oldest captured span matching name and match, or nil.
Examples
assert find_span("db.insert").metrics["rows"] == 42
assert find_span("db.insert", resource: "INSERT INTO orders")
Asserts that no captured span matches name and match.
Examples
refute_span "analytics.record_event"
@spec spans() :: [DDTrace.Span.t()]
Returns every span captured so far, oldest finish first.
Examples
assert Enum.map(spans(), & &1.name) == ["db.insert", "order.process"]
@spec start_capture(boolean()) :: :ok
Starts capturing spans finished in the calling process.
Anything the caller captured before is forgotten, so a second call is a reset rather than an error.
strict? decides whether a structural mistake made in this test — or in
any process it spawns — raises rather than being logged. See the
"Strict mode" section above.
use DDTrace.Test calls this for you, and pairs it with
stop_capture/1; call it directly only where you are not using the
setup it installs, and pair it yourself.
@spec stop_capture(pid()) :: :ok
Stops owner capturing and forgets the spans it captured.
use DDTrace.Test calls this for you when a test ends; call it directly
only where you are not using the setup it installs.