test(infra): verify-tracing asserts one connected trace across services in Tempo (refs #123)
Generates anonymous BFF→projection-api traffic and asserts Tempo holds a single trace containing both service.names — proving OTLP export plus traceparent propagation across the HttpClient hop. Fails until the services are instrumented (next commit). Runs in-network like the other verify checks. refs #123
This commit is contained in:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""S-16b (#123): prove distributed tracing works end to end.
|
||||
|
||||
Generate anonymous BFF traffic (GET /openbaar/register, which the BFF serves by
|
||||
calling projection-api — no auth, no OpenZaak egress), then query Tempo and assert
|
||||
that ONE trace contains spans from both `bff` and `projection-api`. That proves the
|
||||
services export OTLP to Tempo AND that the W3C traceparent propagates across the
|
||||
HttpClient hop, stitching the request into a single connected trace.
|
||||
|
||||
Stdlib only (urllib/json) so it runs in a bare python:3-slim container in-network.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
BFF = os.environ["BFF"] # http://<bff-ip>:8080
|
||||
TEMPO = os.environ["TEMPO"] # http://<tempo-ip>:3200
|
||||
TIMEOUT = int(os.environ.get("TRACING_TIMEOUT", "90"))
|
||||
WANT = {"bff", "projection-api"} # the two services that must share one trace
|
||||
|
||||
|
||||
def _get(url):
|
||||
with urllib.request.urlopen(url, timeout=10) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def generate_traffic():
|
||||
# A non-2xx still produces spans; only total unreachability of the BFF is fatal.
|
||||
for _ in range(3):
|
||||
try:
|
||||
_get(f"{BFF}/openbaar/register")
|
||||
except urllib.error.HTTPError:
|
||||
pass
|
||||
|
||||
|
||||
def search_trace_ids():
|
||||
q = urllib.parse.quote('{ resource.service.name = "bff" }')
|
||||
try:
|
||||
data = json.loads(_get(f"{TEMPO}/api/search?q={q}&limit=50"))
|
||||
except Exception:
|
||||
return []
|
||||
return [t["traceID"] for t in data.get("traces", [])]
|
||||
|
||||
|
||||
def services_in_trace(trace_id):
|
||||
try:
|
||||
data = json.loads(_get(f"{TEMPO}/api/traces/{trace_id}"))
|
||||
except Exception:
|
||||
return set()
|
||||
names = set()
|
||||
for batch in data.get("batches", []):
|
||||
for attr in batch.get("resource", {}).get("attributes", []):
|
||||
if attr.get("key") == "service.name":
|
||||
names.add(attr.get("value", {}).get("stringValue"))
|
||||
return names
|
||||
|
||||
|
||||
def main():
|
||||
deadline = time.time() + TIMEOUT
|
||||
generate_traffic()
|
||||
seen = set()
|
||||
while time.time() < deadline:
|
||||
for tid in search_trace_ids():
|
||||
names = services_in_trace(tid)
|
||||
seen |= names
|
||||
if WANT.issubset(names):
|
||||
print(f"OK — trace {tid} spans {sorted(names)}")
|
||||
return 0
|
||||
time.sleep(3)
|
||||
generate_traffic()
|
||||
print(f"FAIL — no single trace spanned {sorted(WANT)}; services seen: {sorted(seen)}",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user