DDTrace.Test (dd_trace_ex v0.1.0)

Copy Markdown View Source

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
end

use 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: false

To 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

match()

@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

assert_span(name, match \\ [])

@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"}

find_span(name, match \\ [])

@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")

refute_span(name, match \\ [])

@spec refute_span(String.t(), match()) :: :ok | no_return()

Asserts that no captured span matches name and match.

Examples

refute_span "analytics.record_event"

spans()

@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"]

start_capture(strict? \\ true)

@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.

stop_capture(owner)

@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.