CI / lint (pull_request) Successful in 5m7s
CI / unit (pull_request) Successful in 2m2s
CI / frontend (pull_request) Successful in 3m58s
CI / mutation (pull_request) Successful in 6m30s
CI / verify-stack (pull_request) Successful in 9m34s
CI / build (pull_request) Successful in 4m57s
Wire OTel metrics into the four remaining .NET services (acl, domain, event-subscriber, projection-api) exactly as the BFF: ASP.NET Core + HttpClient instrumentation + the built-in System.Runtime meter, exposed at /metrics via the Prometheus AspNetCore exporter (ADR-0024). Prometheus scrapes one job per service; Grafana ships a pre-built 'Request path — golden signals' dashboard (traffic/errors/latency/saturation). A verify-metrics CI step proves the endpoints are scraped end to end.
76 lines
2.3 KiB
Python
Executable File
76 lines
2.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""S-16c (#124): prove the golden-signal metrics pipeline works end to end.
|
|
|
|
Generate anonymous BFF traffic (GET /openbaar/register — no auth, no OpenZaak egress),
|
|
then query Prometheus and assert (1) every .NET service's scrape target is UP, and (2)
|
|
the http.server.request.duration histogram is actually being scraped — i.e. the services
|
|
expose /metrics AND Prometheus collects it, which is exactly what the golden-signal
|
|
dashboard reads.
|
|
|
|
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
|
|
PROM = os.environ["PROMETHEUS"] # http://<prometheus-ip>:9090
|
|
TIMEOUT = int(os.environ.get("METRICS_TIMEOUT", "90"))
|
|
SERVICES = {"acl", "domain", "bff", "event-subscriber", "projection-api"}
|
|
|
|
|
|
def _get(url):
|
|
with urllib.request.urlopen(url, timeout=10) as r:
|
|
return r.read()
|
|
|
|
|
|
def generate_traffic():
|
|
for _ in range(3):
|
|
try:
|
|
_get(f"{BFF}/openbaar/register")
|
|
except urllib.error.HTTPError:
|
|
pass # a non-2xx still records an http.server metric
|
|
|
|
|
|
def query(promql):
|
|
q = urllib.parse.quote(promql)
|
|
try:
|
|
data = json.loads(_get(f"{PROM}/api/v1/query?query={q}"))
|
|
except Exception:
|
|
return []
|
|
return data.get("data", {}).get("result", [])
|
|
|
|
|
|
def jobs_up():
|
|
return {r["metric"].get("job") for r in query("up == 1")}
|
|
|
|
|
|
def jobs_with_request_metric():
|
|
return {r["metric"].get("job")
|
|
for r in query("http_server_request_duration_seconds_count")}
|
|
|
|
|
|
def main():
|
|
deadline = time.time() + TIMEOUT
|
|
while time.time() < deadline:
|
|
generate_traffic()
|
|
up = jobs_up()
|
|
scraped = jobs_with_request_metric()
|
|
if SERVICES.issubset(up) and SERVICES.issubset(scraped):
|
|
print(f"OK — targets up: {sorted(up & SERVICES)}; "
|
|
f"request metric scraped from: {sorted(scraped & SERVICES)}")
|
|
return 0
|
|
time.sleep(3)
|
|
print(f"FAIL — up: {sorted(jobs_up() & SERVICES)}; "
|
|
f"request metric from: {sorted(jobs_with_request_metric() & SERVICES)}; "
|
|
f"expected all of {sorted(SERVICES)}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|