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
82 lines
2.5 KiB
Python
Executable File
82 lines
2.5 KiB
Python
Executable File
#!/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())
|