Use when tests must prove a request produces the expected spans across services in a running Jaeger, Zipkin or Grafana Tempo backend. Runs the backend in CI, queries traces by service and operation, and asserts on spans, parent-child links and durations.
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
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.
Jaeger exposes trace data over an HTTP query API on :16686:
| Endpoint | Returns |
|---|---|
GET /api/services | List of service names |
GET /api/services/{service}/operations | Operations for a service |
GET /api/traces?service=X&operation=Y&lookback=5m&limit=10 | Trace 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.
referencesJaeger 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 500msRun 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:4318Full port map: 16686 query API + UI, 4317 OTLP/gRPC ingest, 4318
OTLP/HTTP ingest, 5778 sampling config, 9411 Zipkin (B3) compatibility.
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 itIn-memory storage is bounded by Jaeger's eviction; long test runs should restart the container or accept eviction.
All-in-one uses transient in-memory storage per the Jaeger getting-started docs. 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.yamlProduction storage backends (Cassandra, Elasticsearch, OpenSearch, Badger) matter for real deployments; the in-memory all-in-one is sufficient for CI.