Author integration tests that query a tracing backend for cross-service trace verification - Jaeger, Zipkin, or Grafana Tempo, same run-query-assert workflow. 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; Zipkin (:9411 REST API, B3 single/multi-header propagation tests, dependency graph) in references/zipkin.md; Tempo (TraceQL span selectors + structural operators, /api/search, single-binary Docker) in references/tempo.md. Use when verifying that a request produces the expected spans across service boundaries in a running Jaeger, Zipkin, or Tempo backend.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Zipkin is the original distributed-tracing system (predates
OpenTelemetry); still common in Java shops via Spring Cloud Sleuth
heritage, and these tests protect a Zipkin → Jaeger/OTel cutover. The
workflow is identical to Jaeger's: run the backend in CI, ship spans,
force_flush(), query the REST API, assert on the span set.
Per the Zipkin quickstart:
docker run -d -p 9411:9411 openzipkin/zipkinservices:
zipkin:
image: openzipkin/zipkin
ports: ["9411:9411"]Per the Zipkin API spec:
| Endpoint | Returns |
|---|---|
GET /api/v2/services | Service names |
GET /api/v2/spans?serviceName=X | Operations |
GET /api/v2/traces?serviceName=X&spanName=Y&lookback=300000&limit=10 | Traces (lookback in ms) |
GET /api/v2/trace/{traceId} | Single trace |
GET /api/v2/dependencies?endTs=...&lookback=... | Service dependency graph |
POST /api/v2/spans | Submit spans (V2 JSON) |
from opentelemetry.exporter.zipkin.json import ZipkinExporter
# BatchSpanProcessor(ZipkinExporter(endpoint="http://localhost:9411/api/v2/spans"))
def test_order_trace_in_zipkin():
with use_tracer():
create_order(items=[item])
trace.get_tracer_provider().force_flush(timeout_millis=5000)
time.sleep(0.5)
traces = requests.get(
"http://localhost:9411/api/v2/traces",
params={"serviceName": "orders", "spanName": "order.create",
"lookback": 60000, "limit": 1},
).json() # list of lists of spans
assert len(traces) == 1
span = next(s for s in traces[0] if s["name"] == "order.create")
assert span["tags"]["order.item_count"] == "1" # Zipkin V2 tags are ALL stringsZipkin V2 stores tag values as strings (vs Jaeger's typed tags) - cast in assertions accordingly.
Per the B3 propagation spec:
X-B3-TraceId (32/16 lower-hex), X-B3-SpanId (16),
X-B3-ParentSpanId (absent on root), X-B3-Sampled (1/0),
X-B3-Flags (1 debug).b3: {TraceId}-{SpanId}-{SamplingState}-{ParentSpanId},
sampling 1 accept / 0 deny / d debug / absent defer.Test both forms - modern services increasingly send only the single-header form:
def test_b3_single_header_propagates():
headers = {"b3": f"{trace_id}-{span_id}-1-{parent_id}"}
requests.get("http://localhost:8080/orders", headers=headers)
time.sleep(0.5)
spans = requests.get(f"http://localhost:9411/api/v2/trace/{trace_id}").json()
assert any(s["traceId"] == trace_id for s in spans)Zipkin computes service dependencies from observed traces; aggregation is lazy (in-memory computes inline; Cassandra uses Spark batch) - allow
=2s:
deps = requests.get("http://localhost:9411/api/v2/dependencies",
params={"endTs": int(time.time() * 1000), "lookback": 60000}).json()
pair = next((d for d in deps if d["parent"] == "orders" and d["child"] == "payments"), None)
assert pair and pair["callCount"] >= 1| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Assert tag values as integers | V2 tags are all strings | Compare as string |
| Test only multi-header B3 | Single-header form is common | Test both |
| Expect the dependency graph immediately | Aggregation is lazy | Allow ≥2s delay |