Testland
Browse all skills & agents

opentelemetry-trace-assertions

Author trace-shape assertions in tests using OpenTelemetry SDK in-memory exporter - capture spans during test execution, assert on span name + attributes + status + parent-child structure + duration. Cross-language patterns (Python `InMemorySpanExporter` + `SimpleSpanProcessor`, JS `getRecordedSpans()`, Java `OpenTelemetryExtension`); CI integration.

opentelemetry-trace-assertions

Per the OpenTelemetry traces concept docs, a trace is "the path of a request through your application" and a span is "a unit of work or operation". Spans form a directed acyclic graph (DAG) via parent-child relationships, all sharing the same trace_id.

When to use

  • Service is instrumented with OpenTelemetry SDK and the team's observability stack (alerts, SLOs) depends on specific span attributes - silent removal of an attribute breaks downstream alerting.
  • Trace-shape changes have caused production incidents.
  • Onboarding new instrumentation needs a regression baseline.

Step 1 - Install (per language)

LanguageInstall
Pythonpip install opentelemetry-sdk
JS/TSnpm install @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node
Java (Maven)<dependency><groupId>io.opentelemetry</groupId><artifactId>opentelemetry-sdk-testing</artifactId></dependency>
.NETdotnet add package OpenTelemetry --prerelease + OpenTelemetry.Exporter.InMemory

Step 2 - In-memory exporter setup (Python)

Per the Python SDK trace docs, use SimpleSpanProcessor for tests because it "passes ended spans directly to the configured SpanExporter" synchronously - no batching, no flush wait:

from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry import trace

memory_exporter = InMemorySpanExporter()
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(SimpleSpanProcessor(memory_exporter))
trace.set_tracer_provider(tracer_provider)

# ... exercise code under test ...

spans = memory_exporter.get_finished_spans()
assert len(spans) == 3
memory_exporter.clear()

The clear() call between tests prevents cross-test span leakage.

Step 3 - Span shape assertions

Per the OpenTelemetry traces concept docs, spans expose name, attributes, status, and kind:

def test_order_creation_emits_correct_trace():
    with use_tracer():
        create_order(items=[item])

    spans = memory_exporter.get_finished_spans()
    span_by_name = {s.name: s for s in spans}

    assert "order.create" in span_by_name
    order_span = span_by_name["order.create"]
    assert order_span.attributes.get("order.item_count") == 1
    assert order_span.status.status_code == StatusCode.OK
    assert order_span.kind == SpanKind.INTERNAL

Per the OpenTelemetry traces concept docs: span kind values are INTERNAL (in-process), CLIENT (outgoing remote), SERVER (incoming remote), PRODUCER (queue publish), CONSUMER (queue process).

Step 4 - Parent-child structure

Per the OpenTelemetry traces concept docs, spans share a trace_id and reference their parent via parent_id. Assert structure (not values - IDs are random per run):

db_span = span_by_name["db.query"]
order_span = span_by_name["order.create"]

# Same trace
assert db_span.context.trace_id == order_span.context.trace_id

# DB is a child of order.create
assert db_span.parent.span_id == order_span.context.span_id

Step 5 - Semantic conventions verification

Per the HTTP semantic conventions docs, required HTTP client span attributes include http.request.method, url.full, server.address, server.port, http.response.status_code, and error.type:

assert http_span.attributes["http.request.method"] == "POST"
assert http_span.attributes["url.full"].startswith("https://api.example.com/orders")
assert http_span.attributes["http.response.status_code"] == 201

Deprecation note: older SDKs emitted http.method (deprecated). Per the HTTP semantic conventions docs, the current key is http.request.method. The OTEL_SEMCONV_STABILITY_OPT_IN env var controls dual-emit during migration. Tests should assert the new keys; failures during SDK upgrade are the signal that instrumentation needs migrating, not that the test is wrong.

Step 6 - JS / TS pattern

import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';

const memoryExporter = new InMemorySpanExporter();
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
provider.register();

// ... exercise ...

const spans = memoryExporter.getFinishedSpans();
expect(spans).toHaveLength(3);
memoryExporter.reset();

Step 7 - Java JUnit 5 pattern

@RegisterExtension
static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();

@Test
void orderCreateEmitsTrace() {
    createOrder(/* ... */);

    List<SpanData> spans = otelTesting.getSpans();
    assertThat(spans).extracting(SpanData::getName).contains("order.create");
}

OpenTelemetryExtension (from opentelemetry-sdk-testing) auto-resets between tests; no manual clear() needed.

Step 8 - CI integration

Pin the SDK version in test deps. SDK upgrades change attribute keys (see Step 5 deprecation note); pinning prevents trace assertions from silently changing meaning between releases.

# pytest in CI
- run: |
    pip install opentelemetry-sdk==1.29.0
    pytest tests/trace/ -v

Anti-patterns

Anti-patternWhy it failsFix
Asserting on exact span IDsIDs are random per runAssert structure (parent.span_id == X.context.span_id), not values (Step 4)
Asserting span count without name lookupBrittle to instrumentation reorderBuild span_by_name dict (Step 3)
Use BatchSpanProcessor for testsAsync batching → flaky get_finished_spans()Always SimpleSpanProcessor for tests (Step 2)
Skip memory_exporter.clear() between testsCross-test span leakage; flakeclear() in fixture teardown (Step 2)
Assert on legacy http.method keysBreaks on SDK upgrade to v1.20+Use http.request.method (Step 5)

Limitations

  • In-memory exporter doesn't catch async-boundary issues across process boundaries - pair with jaeger-trace-tests / zipkin-trace-tests for end-to-end trace verification.
  • SDK-version drift can change span attribute names; pin SDK version in CI (Step 8).
  • Span links (per the OpenTelemetry traces concept docs) are not asserted in this skill's pattern; extend per use case.

References