Traces testkit reference
The traces testkit provides in-memory span collection and structural expectation APIs for OpenTelemetry Java
SpanData.
Use this page as an API reference for TracesTestkit, SpanExpectation, TraceExpectation,
TraceForestExpectation, SpanExpectations, TraceExpectations, and the event, link, status, and span-context
expectation types.
For an end-to-end test setup, see Test traces emitted by your code. For the overview of all signal testkits, see Testkit.
The examples below assume these imports:
import io.opentelemetry.sdk.trace.data.SpanData
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.oteljava.testkit.AttributesExpectation
import org.typelevel.otel4s.oteljava.testkit.{
InstrumentationScopeExpectation,
TelemetryResourceExpectation
}
import org.typelevel.otel4s.oteljava.testkit.trace._
TracesTestkit
TracesTestkit is the signal-specific in-memory backend for traces.
| Member | Purpose |
|---|---|
TracesTestkit.inMemory[F]() |
Creates a Resource[F, TracesTestkit[F]] backed by an in-memory span exporter. |
TracesTestkit.builder[F] |
Creates a builder for customizing the underlying SdkTracerProviderBuilder and propagators. |
tracerProvider |
The otel4s TracerProvider[F] used by code under test. |
finishedSpans |
Returns List[SpanData] from the in-memory span exporter. |
resetSpans |
Clears the in-memory span exporter. |
propagators |
The context propagators used by the tracer provider. |
localContext |
The LocalContext[F] used by the tracer provider. |
OtelJavaTestkit also exposes the same trace members when a test needs traces together with metrics or logs.
Flat vs structural matching
The traces expectation API has two layers:
SpanExpectationsfor flat exported-span matchingTraceExpectationsfor exact tree and forest matching
Use SpanExpectations when you only care that some exported spans exist:
SpanExpectations.checkAllDistinct(
Nil,
SpanExpectation.server("GET /users"),
SpanExpectation.client("SELECT users")
)
Use TraceExpectations when parent-child topology matters:
TraceForestExpectation.unordered(
TraceExpectation.unordered(
SpanExpectation.server("GET /users").noParentSpanContext,
TraceExpectation.leaf(SpanExpectation.client("SELECT users"))
)
)
Partial matching
SpanExpectation values are partial.
This means:
- unspecified span fields are ignored
- you can assert only the relevant properties for the current test
- you can still add more detail when needed
For example:
SpanExpectation.name("app.span")
matches any span named app.span, regardless of timing, attributes, events, links, scope, or resource.
import scala.concurrent.duration._
SpanExpectation
.name("app.span")
.startTimestamp(1.second)
.endTimestamp(1500.millis)
adds exact timing checks on top of the name match.
The same principle applies recursively:
TraceExpectationonly checks the subtree shape you describeSpanExpectationonly checks the span fields you setEventExpectationandLinkExpectationonly check the fields you setEventSetExpectationandLinkSetExpectationonly check the collection properties you set
Trees and forests
The structural API uses two types:
TraceExpectationfor one subtreeTraceForestExpectationfor the full exported forest
Use:
TraceExpectation.leaf(...)for a span with no expected childrenTraceExpectation.ordered(...)for a subtree whose direct children must appear in orderTraceExpectation.unordered(...)for a subtree whose direct children may appear in any order
At the forest level:
TraceForestExpectation.ordered(...)requires roots in orderTraceForestExpectation.unordered(...)ignores root orderTraceForestExpectation.emptyrequires no finished root spans
TraceExpectation.leaf(SpanExpectation.name("db.query"))
TraceExpectation.ordered(
SpanExpectation.name("request").noParentSpanContext,
TraceExpectation.leaf(SpanExpectation.name("decode")),
TraceExpectation.leaf(SpanExpectation.name("persist"))
)
TraceExpectation.unordered(
SpanExpectation.name("request").noParentSpanContext,
TraceExpectation.leaf(SpanExpectation.name("cache")),
TraceExpectation.leaf(SpanExpectation.name("db.query"))
)
Both ordered and unordered modes still require the exact number of direct children or roots. What changes is whether relative order matters.
This is especially useful when sibling spans can finish in a nondeterministic order.
Span expectations
SpanExpectation is the building block for each trace node.
Start with one of the entry points:
SpanExpectation.anySpanExpectation.name(...)SpanExpectation.internal(...)SpanExpectation.server(...)SpanExpectation.client(...)SpanExpectation.producer(...)SpanExpectation.consumer(...)
import org.typelevel.otel4s.trace.SpanKind
SpanExpectation.any
SpanExpectation.name("service.call")
SpanExpectation.internal("cache.lookup")
SpanExpectation.name("db.query").kind(SpanKind.Client)
Timing and lifecycle
You can assert timing and end-state directly:
import scala.concurrent.duration._
SpanExpectation
.name("request")
.startTimestamp(1.second)
.endTimestamp(1500.millis)
.hasEnded
SpanExpectation
.name("still-open")
.endTimestamp(None)
.hasNotEnded
Attributes
Span attributes follow the same conventions as the metrics testkit:
attributesExact(...)attributesSubset(...)attributes(AttributesExpectation...)attributesEmpty
SpanExpectation
.name("request")
.attributesExact(
Attribute("http.method", "GET"),
Attribute("http.route", "/users")
)
SpanExpectation
.name("request")
.attributesSubset(Attribute("http.method", "GET"))
SpanExpectation
.name("request")
.attributes(
AttributesExpectation.where("must contain at least one attribute")(_.nonEmpty)
)
Status
Status matching is available through StatusExpectation:
import org.typelevel.otel4s.trace.StatusCode
SpanExpectation
.name("request")
.status(StatusExpectation.ok)
SpanExpectation
.name("request")
.status(StatusExpectation.error.description("boom"))
SpanExpectation
.name("request")
.status(StatusExpectation.code(StatusCode.Error).description(None))
Span context and parent context
You can match the span context itself, its parent, or selected context fields.
SpanExpectation
.name("child")
.parentSpanContext(
SpanContextExpectation
.any
.traceIdHex("0af7651916cd43dd8448eb211c80319c")
.sampled(true)
)
SpanExpectation
.name("root")
.noParentSpanContext
If you already have a concrete otel4s SpanContext, use exact matching:
import org.typelevel.otel4s.trace.{SpanContext, TraceFlags, TraceState}
import scodec.bits.ByteVector
val spanContext =
SpanContext(
traceId = ByteVector.fromValidHex("0af7651916cd43dd8448eb211c80319c"),
spanId = ByteVector.fromValidHex("0102030405060708"),
traceFlags = TraceFlags.Default,
traceState = TraceState.empty,
remote = false
)
SpanExpectation.name("request").spanContextExact(spanContext)
Scope and resource
Instrumentation scope and telemetry resource are matched the same way as in the metrics testkit reference:
SpanExpectation
.name("request")
.scope(
InstrumentationScopeExpectation
.name("service")
.version("1.0")
.attributesEmpty
)
.resource(
TelemetryResourceExpectation.any
.attributesSubset(Attribute("service.name", "user-service"))
)
Events
Events are matched with:
EventExpectationfor one eventEventSetExpectationfor the event collection
EventExpectation supports:
anyname(...)timestamp(...)attributesExact(...)attributesSubset(...)attributesEmptywhere(...)
import scala.concurrent.duration._
EventExpectation.name("started")
EventExpectation
.name("exception")
.timestamp(2.seconds)
.attributesSubset(Attribute("exception.message", "boom"))
EventSetExpectation is collection-based. Use:
anyexistsforallcontainsexactlycountminCountmaxCountnonepredicate.and(...)and.or(...)
SpanExpectation
.name("work")
.events(
EventSetExpectation
.contains(
EventExpectation.name("started"),
EventExpectation.name("finished")
)
.and(EventSetExpectation.count(2))
)
SpanExpectation
.name("work")
.events(
EventSetExpectation.none(EventExpectation.name("exception"))
)
The convenience span-level helpers are:
containsEvents(...)exactlyEvents(...)eventCount(...)
Links
Links are matched with:
LinkExpectationfor one linkLinkSetExpectationfor the link collection
LinkExpectation supports:
anyspanContext(...)spanContextExact(...)traceId(...)traceIdHex(...)spanId(...)spanIdHex(...)samplednotSampledattributesExact(...)attributesSubset(...)attributesEmptywhere(...)
LinkExpectation.any
LinkExpectation
.any
.traceIdHex("0af7651916cd43dd8448eb211c80319c")
.sampled
LinkSetExpectation follows the same collection-level conventions as events:
anyexistsforallcontainsexactlycountminCountmaxCountnonepredicate.and(...)and.or(...)
Span-level convenience helpers:
containsLinks(...)exactlyLinks(...)linkCount(...)
Flat span matching
When exact topology is not important, use SpanExpectations directly:
SpanExpectations.checkAllDistinct(
Nil,
SpanExpectation.server("GET /users"),
SpanExpectation.client("SELECT users")
)
The top-level helpers are:
existsfindcheckcheckAllcheckAllDistinctmissingmissingDistinctallMatchallMatchDistinctformat
checkAll(...) is non-consuming: the same exported span may satisfy multiple expectations.
checkAllDistinct(...) enforces distinct assignment and is the safer default when repeated expectations should match
different collected spans.
Clues and custom predicates
Every level of the trace API supports custom predicates and optional clues.
SpanExpectation
.name("request")
.where("must be ended")(_.hasEnded())
.clue("request span")
TraceExpectation
.leaf(SpanExpectation.name("request"))
.clue("root request subtree")
Clues are preserved in mismatch messages and make failures much easier to read in large tests.
Formatting mismatches
Use the formatting helpers when connecting expectations to your test framework:
def assertTrace(
spans: List[SpanData],
expected: TraceForestExpectation
): Unit =
TraceExpectations.check(spans, expected) match {
case Right(_) =>
()
case Left(mismatches) =>
sys.error(TraceExpectations.format(mismatches))
}
For flat span checks:
def assertSpans(
spans: List[SpanData],
expected: SpanExpectation*
): Unit =
SpanExpectations.checkAllDistinct(spans, expected: _*) match {
case Right(_) =>
()
case Left(mismatches) =>
sys.error(SpanExpectations.format(mismatches))
}