Testland
Browse all skills & agents

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill jaeger-trace-tests
View source

jaeger-trace-tests

Jaeger ingests traces over OTLP and exposes a query API for verification. Per the Jaeger getting-started docs (opens in new window), the all-in-one image "combines collector and query components in a single process and uses a transient in-memory storage for trace data" - perfect for CI.

When to use

  • E2E or integration test exercises multiple services and you need to verify the full distributed trace shape (not just per-process spans).
  • Production observability stack uses Jaeger; tests should reflect the same query API your alerts/SLOs depend on.
  • Smoke test after instrumentation changes - confirm spans actually reach Jaeger (not just the SDK exporter).

How to use

  1. Start Jaeger all-in-one in CI as a Docker service (Step 1) - it exposes OTLP ingest on :4317/:4318 and the query API on :16686.
  2. Point the app's OpenTelemetry SDK at the collector's OTLP endpoint (Step 2).
  3. Exercise the flow, force_flush() the span processor, and let the ingest pipeline settle before querying (Worked example).
  4. Query GET /api/traces?service=X&operation=Y and assert on the returned span set and tags (Worked example); parent-child + duration assertions and the full query API live in references/query-api-and-ci-wiring.md.
  5. Scope each test to a unique service.name so shared-CI trace data does not cross-contaminate (see references).

Step 1 - Run Jaeger all-in-one in CI

Per the Jaeger getting-started docs (opens in new window):

docker run --rm --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 5778:5778 \
  -p 9411:9411 \
  cr.jaegertracing.io/jaegertracing/jaeger:2.17.0
PortPurpose
16686Jaeger UI + query HTTP API
4317OTLP/gRPC ingest
4318OTLP/HTTP ingest

The full port map (sampling :5778, Zipkin :9411) and the GitHub Actions service block are in references/query-api-and-ci-wiring.md.

Step 2 - Configure SDK to ship to Jaeger

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True))
)
trace.set_tracer_provider(provider)

BatchSpanProcessor defers shipping, so the Worked example flushes manually before querying.

Worked example

Exercise the flow, force a flush, then query Jaeger and assert the span set plus a tag value end to end:

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

    # Flush all spans to Jaeger before query
    trace.get_tracer_provider().force_flush(timeout_millis=5000)

    # Allow Jaeger ingest pipeline a moment
    time.sleep(0.5)

    resp = requests.get(
        "http://localhost:16686/api/traces",
        params={"service": "orders", "operation": "order.create", "lookback": "1m", "limit": 1},
    )
    traces = resp.json()["data"]
    assert len(traces) == 1
    span = next(s for s in traces[0]["spans"] if s["operationName"] == "order.create")
    tag = next(t for t in span["tags"] if t["key"] == "order.item_count")
    assert tag["value"] == 1

The force_flush + brief sleep is mandatory: BatchSpanProcessor batches exports, so an immediate query races the ingest pipeline and misses the span. For parent-child links and duration ceilings, see references/query-api-and-ci-wiring.md.

Anti-patterns

Anti-patternWhy it failsFix
Query Jaeger immediately after exerciseSpans may not have shipped yetforce_flush() + brief sleep (Worked example)
Use prod Jaeger from CITest traces pollute prod dataAlways Docker all-in-one (Step 1)
Hard-code service name across testsCross-test contamination on shared CIUnique service.name per test (references)
Assume long retentionAll-in-one is in-memory; old traces evictedRestart container or shorten test runs
Skip flushing pipelineBatchSpanProcessor defers ship; queries miss spansAlways flush before query (Worked example)

Limitations

  • Jaeger v2 changed deployment + binary names from v1; verify current image tag at the Jaeger getting-started docs (opens in new window).
  • Storage backends (Cassandra, Elasticsearch, OpenSearch, Badger) matter for production but Docker all-in-one is sufficient for CI.
  • Jaeger UI is for humans; only query HTTP API in tests (no scraping HTML).

References

Jaeger query API, advanced assertions, and CI wiring

View source (opens in new window)

Jaeger query API, advanced assertions, and CI wiring

Deep reference for the jaeger-trace-tests SKILL.md. Consult for the full Jaeger query API surface, parent-child + duration assertion patterns, the GitHub Actions service block, and per-test isolation / retention on a shared CI backend.

Query API endpoints

Jaeger exposes trace data over an HTTP query API on :16686:

EndpointReturns
GET /api/servicesList of service names
GET /api/services/{service}/operationsOperations for a service
GET /api/traces?service=X&operation=Y&lookback=5m&limit=10Trace JSON
GET /api/traces/{traceId}Single trace by ID

Trace JSON response shape (selected fields):

{
  "data": [{
    "traceID": "abc...",
    "spans": [
      {
        "spanID": "def...",
        "operationName": "order.create",
        "duration": 12345,
        "tags": [{"key": "order.item_count", "type": "int64", "value": 1}],
        "references": [{"refType": "CHILD_OF", "spanID": "parent..."}]
      }
    ]
  }]
}

duration is in microseconds; tags is a flat list of typed key/value pairs; parent links live in references, not on the child span directly.

Parent-child assertions via references

Jaeger encodes parent links as references with refType: "CHILD_OF".

def parent_id(span):
    refs = span.get("references", [])
    child_of = [r for r in refs if r["refType"] == "CHILD_OF"]
    return child_of[0]["spanID"] if child_of else None

assert parent_id(db_span) == order_span["spanID"]

Assert a duration ceiling straight off the microsecond duration field:

assert order_span["duration"] < 500_000  # under 500ms

GitHub Actions service

Run the all-in-one image as a job service so every step can reach OTLP ingest and the query API:

services:
  jaeger:
    image: cr.jaegertracing.io/jaegertracing/jaeger:2.17.0
    ports:
      - 16686:16686
      - 4317:4317
      - 4318:4318

Full port map: 16686 query API + UI, 4317 OTLP/gRPC ingest, 4318 OTLP/HTTP ingest, 5778 sampling config, 9411 Zipkin (B3) compatibility.

Per-test isolation on shared CI

CI runs many tests against one Jaeger. Scope each test with a unique service.name (or a unique trace tag) so a query never sees another test's spans:

service_name = f"orders-test-{uuid4()}"
# configure the SDK with this service name, then query filtered by it

In-memory storage is bounded by Jaeger's eviction; long test runs should restart the container or accept eviction.

Retention

All-in-one uses transient in-memory storage per the Jaeger getting-started docs (opens in new window). For longer runs mount a config or restart the container between workflows:

docker run ... \
  -v /path/to/config.yaml:/jaeger/config.yaml \
  cr.jaegertracing.io/jaegertracing/jaeger:2.17.0 \
  --config /jaeger/config.yaml

Production storage backends (Cassandra, Elasticsearch, OpenSearch, Badger) matter for real deployments; the in-memory all-in-one is sufficient for CI.

Related skills

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.

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