Test traces emitted by your code

Use TracesTestkit to run code against an in-memory tracing backend and assert the exported spans.

1. Add the testkit dependency

Add the testkit to the test scope in build.sbt:

libraryDependencies +=
  "org.typelevel" %% "otel4s-oteljava-testkit" % "1.1.0" % Test

Add the test dependency to the test source file:

//> using test.dep "org.typelevel::otel4s-oteljava-testkit:1.1.0"

2. Allocate the tracer once

Create one tracer for the service's instrumentation scope and pass it directly to the code that creates spans.

import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer

final class Service(implicit tracer: Tracer[IO]) {
  def handleRequest: IO[Unit] =
    Tracer[IO].span("service.request").surround {
      Tracer[IO].span("service.fetch").surround(IO.unit)
    }
}

3. Define an assertion helper

The structural expectation API returns mismatches for the exported trace forest. Convert them into the failure type used by your test framework.

import io.opentelemetry.sdk.trace.data.SpanData
import org.typelevel.otel4s.oteljava.testkit.trace.{TraceExpectations, TraceForestExpectation}

def assertTraces(spans: List[SpanData], expected: TraceForestExpectation): IO[Unit] =
  TraceExpectations.check(spans, expected) match {
    case Right(_) =>
      IO.unit
    case Left(mismatches) =>
      IO.raiseError(new AssertionError(TraceExpectations.format(mismatches)))
  }

This helper uses IO.raiseError so it remains independent of a specific test framework. Replace it with the framework's failure mechanism when appropriate.

4. Run the code with TracesTestkit

Create the testkit as a Resource, get a tracer from its tracerProvider, and pass the tracer directly to the service. Collect the finished spans before the resource closes.

import org.typelevel.otel4s.oteljava.testkit.trace.{
  SpanExpectation,
  TraceExpectation,
  TracesTestkit
}

def test: IO[Unit] =
  TracesTestkit.inMemory[IO]().use { testkit =>
    for {
      tracer <- testkit.tracerProvider.get("service")
      service = new Service()(tracer)
      _ <- service.handleRequest
      spans <- testkit.finishedSpans
      _ <- assertTraces(
        spans,
        TraceForestExpectation.unordered(
          TraceExpectation.unordered(
            SpanExpectation.name("service.request").noParentSpanContext,
            TraceExpectation.leaf(SpanExpectation.name("service.fetch"))
          )
        )
      )
    } yield ()
  }

TraceExpectation checks the parent-child structure shown in the expectation. Unspecified span fields, such as timestamps and attributes, are ignored.

What's next

For flat span matching, attributes, events, links, status, and custom predicates, see Traces testkit reference.