#!/usr/bin/env python3 """Validate and summarise the target registry (targets.yaml + fragments/*.yaml, services.yaml + services-fragments/*.yaml). python3 scripts/registry-check.py # report + exit 1 on errors python3 scripts/registry-check.py --fix # also normalise: tier ≥ 3 non-infra targets → checks [http]; numeric ids → str """ from __future__ import annotations import collections import pathlib import re import sys import yaml ROOT = pathlib.Path(__file__).resolve().parents[1] T_MAIN = ROOT / "data/targets/targets.yaml" T_FRAG = sorted((ROOT / "data/targets/fragments").glob("*.yaml")) S_MAIN = ROOT / "data/seed/services.yaml" S_FRAG = sorted((ROOT / "data/seed/services-fragments").glob("*.yaml")) CATS = {"dns", "cdn", "cloud", "developer", "infrastructure", "search", "messaging", "social", "streaming", "commerce", "finance", "government", "news", "ai"} INFRA_CATS = {"dns", "cdn", "cloud", "infrastructure"} CC_RE = re.compile(r"^[A-Z]{2}$") fix = "--fix" in sys.argv errors: list[str] = [] def load_targets(path: pathlib.Path) -> list[dict]: doc = yaml.safe_load(path.read_text()) or {} return doc.get("targets") or [] targets_by_file = {T_MAIN: load_targets(T_MAIN), **{f: load_targets(f) for f in T_FRAG}} services = [] for f in [S_MAIN, *S_FRAG]: services += (yaml.safe_load(f.read_text()) or {}).get("services") or [] slugs = collections.Counter(s["slug"] for s in services) dup_slugs = [slug for slug, n in slugs.items() if n > 1] if dup_slugs: print(f"note: {len(dup_slugs)} service slugs defined in several fragments (the seed keeps the first): {', '.join(dup_slugs[:10])}") ids: collections.Counter = collections.Counter() hosts: collections.Counter = collections.Counter() per_cat: collections.Counter = collections.Counter() per_tier: collections.Counter = collections.Counter() per_cc: collections.Counter = collections.Counter() checks_count: collections.Counter = collections.Counter() tr = 0 total = 0 for f, ts in targets_by_file.items(): for t in ts: total += 1 tid = str(t.get("id")) ids[tid] += 1 hosts[(t.get("host"), t.get("url"), t.get("ip"))] += 1 cat = t.get("cat") if cat not in CATS: errors.append(f"{f.name}: {tid} bad cat {cat!r}") cc = t.get("cc") if cc is not None and not (isinstance(cc, str) and CC_RE.match(cc)): errors.append(f"{f.name}: {tid} bad cc {cc!r}") tier = int(t.get("tier", 2)) if tier not in (1, 2, 3, 4): errors.append(f"{f.name}: {tid} bad tier {tier}") imp = int(t.get("imp", 3)) if imp not in (1, 2, 3, 4, 5): errors.append(f"{f.name}: {tid} bad imp {imp}") svc = t.get("svc") if svc and svc not in slugs: errors.append(f"{f.name}: {tid} unknown svc {svc}") checks = t.get("checks", ["http", "dns", "ping"]) if fix: if not isinstance(t.get("id"), str): t["id"] = tid if tier >= 3 and cat not in INFRA_CATS and checks != ["http"]: t["checks"] = ["http"] checks = ["http"] per_cat[cat] += 1 per_tier[tier] += 1 per_cc[cc or "global"] += 1 for c in checks: checks_count[c] += 1 if t.get("tr"): tr += 1 for tid, n in ids.items(): if n > 1: errors.append(f"duplicate target id {tid} ×{n}") dup_hosts = sum(n - 1 for n in hosts.values() if n > 1) if dup_hosts: print(f"note: {dup_hosts} cross-fragment duplicate host/url entries (the seed keeps the first occurrence)") # probe load estimate (per probe, per second) with the configured cadence cfg = yaml.safe_load((ROOT / "packages/config/pressure.yaml").read_text())["scheduler"] tiers = {int(k): int(v) for k, v in cfg["tiers"].items()} http_ps = sum(1 / tiers.get(int(t.get("tier", 2)), 300) for ts in targets_by_file.values() for t in ts if "http" in t.get("checks", ["http", "dns", "ping"])) dns_ps = sum(4 / cfg["dns_every"] for ts in targets_by_file.values() for t in ts if "dns" in t.get("checks", ["http", "dns", "ping"])) ping_ps = sum(1 / cfg["ping_every"] for ts in targets_by_file.values() for t in ts if "ping" in t.get("checks", ["http", "dns", "ping"])) print(f"targets: {total} (main {len(targets_by_file[T_MAIN])} + fragments {total - len(targets_by_file[T_MAIN])})") print(f"services: {len(services)} with status connector: {sum(1 for s in services if s.get('status'))}") print("per category:", dict(per_cat.most_common())) print("per tier:", dict(sorted(per_tier.items()))) print(f"countries covered: {len(per_cc) - 1} traceroute targets: {tr}") print("checks:", dict(checks_count)) print(f"estimated load per probe: http {http_ps:.1f}/s · dns {dns_ps:.1f}/s · ping {ping_ps:.1f}/s → {http_ps + dns_ps + ping_ps:.1f} measurements/s " f"(×8 probes ≈ {(http_ps + dns_ps + ping_ps) * 8 * 86400 / 1e6:.0f} M rows/day)") if fix: for f, ts in targets_by_file.items(): doc = yaml.safe_load(f.read_text()) or {} doc["targets"] = ts # keep the compact flow style: dump one entry per line lines = [f"# {f.name} — normalised by scripts/registry-check.py --fix", "targets:"] if f == T_MAIN and doc.get("defaults"): lines.insert(1, "defaults: " + yaml.safe_dump(doc["defaults"], default_flow_style=True, width=10_000).strip()) for t in ts: lines.append(" - " + yaml.safe_dump(t, default_flow_style=True, width=10_000, allow_unicode=True, sort_keys=False).strip()) f.write_text("\n".join(lines) + "\n") print("normalised files written") if errors: print(f"\n{len(errors)} error(s):") for e in errors[:60]: print(" -", e) sys.exit(1) print("OK")