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