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. Use when a service is instrumented with the OpenTelemetry SDK and downstream alerts, SLOs, or dashboards depend on specific span names or attributes that a refactor could silently drop.
Install with skills.sh (any agent)
npx skills add testland/qa --skill opentelemetry-trace-assertionsopentelemetry-trace-assertions
Per the OpenTelemetry traces concept docs (opens in new window), 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
Step 1 - Install (per language)
| Language | Install |
|---|---|
| Python | pip install opentelemetry-sdk |
| JS/TS | npm install @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node |
| Java (Maven) | <dependency><groupId>io.opentelemetry</groupId><artifactId>opentelemetry-sdk-testing</artifactId></dependency> |
| .NET | dotnet add package OpenTelemetry --prerelease + OpenTelemetry.Exporter.InMemory |
Step 2 - In-memory exporter setup (Python)
Per the Python SDK trace docs (opens in new window), 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 (opens in new window), 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.INTERNALPer the OpenTelemetry traces concept docs (opens in new window): 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 (opens in new window), 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_idStep 5 - Semantic conventions verification
Per the HTTP semantic conventions docs (opens in new window), 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"] == 201Deprecation note: older SDKs emitted http.method (deprecated). Per the HTTP semantic conventions docs (opens in new window), 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/ -vAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Asserting on exact span IDs | IDs are random per run | Assert structure (parent.span_id == X.context.span_id), not values (Step 4) |
| Asserting span count without name lookup | Brittle to instrumentation reorder | Build span_by_name dict (Step 3) |
Use BatchSpanProcessor for tests | Async batching → flaky get_finished_spans() | Always SimpleSpanProcessor for tests (Step 2) |
Skip memory_exporter.clear() between tests | Cross-test span leakage; flake | clear() in fixture teardown (Step 2) |
Assert on legacy http.method keys | Breaks on SDK upgrade to v1.20+ | Use http.request.method (Step 5) |
Limitations
References
Related skills
jaeger-trace-tests
Author integration tests that query Jaeger for cross-service trace verification - Jaeger all-in-one Docker for CI (OTLP gRPC :4317 + HTTP :4318 ingest, query API on :16686), `/api/traces?service=X&operation=Y` query patterns, span set + parent-child + duration assertions. Use when verifying that a request produces the expected spans across service boundaries in a running Jaeger backend.
otel-collector-config-tester
Validates OpenTelemetry Collector pipeline configurations and verifies spans flow end-to-end through the collector: runs `otelcol validate --config`, wires the `debug`/`file` exporter for span-output assertions, and integrates the full cycle into CI. Use when a collector config change (new receiver, processor swap, exporter wiring) needs correctness verification before deployment.
tempo-trace-tests
Authors integration tests that query Grafana Tempo for cross-service trace verification - TraceQL `{ }` span selectors targeting `span.`, `resource.`, and intrinsic fields; Tempo HTTP API (`/api/search` with `q=`, `/api/traces/{id}`) for span-set and attribute assertions; local Tempo via Docker single-binary (ports 4317/4318/3200). Use when the production observability stack uses Tempo as the trace backend and tests must verify distributed trace shape, span attributes, or service topology after instrumentation changes.
trace-spec-author
Build a trace specification document per feature - defines the trace shape (root span + child spans + key attributes per OpenTelemetry semantic conventions) that production code MUST emit. The spec drives both implementation reviews AND trace-assertion tests, so a single declarative document is the source of truth for what observability "looks like" for a feature. Use before instrumenting a new feature, when existing spans have grown organically with no agreed shape, or after an incident where a debugging session stalled on missing span attributes.
zipkin-trace-tests
Author integration tests that query Zipkin for trace verification - Zipkin all-in-one Docker for CI, REST API (`/api/v2/traces`, `/api/v2/services`, `/api/v2/dependencies`), B3 propagation header tests (single-header and multi-header X-B3-* form), dependency-graph assertions. Use when the team uses Zipkin (legacy or Spring Cloud Sleuth heritage).