"""Pull traffic for dota2.refining.dev via Cloudflare GraphQL Analytics API. Credentials from env (keyzoo inject). Read-only; prints a short summary. """ from __future__ import annotations import json import os import urllib.error import urllib.request from datetime import date, timedelta HOST = "dota2.refining.dev" ZONE_NAME = "refining.dev" DAYS = 14 def headers() -> dict[str, str]: email = os.environ.get("CLOUDFLARE_EMAIL") or os.environ.get( "KEYZOO_ASSET_META_USERNAME" ) key = os.environ.get("CLOUDFLARE_API_KEY") or os.environ.get( "KEYZOO_ASSET_SECRET_GLOBAL_API_KEY" ) if not email or not key: raise SystemExit("missing credentials in env") return { "X-Auth-Email": email, "X-Auth-Key": key, "Content-Type": "application/json", } def api(path: str) -> dict: req = urllib.request.Request( "https://api.cloudflare.com/client/v4" + path, method="GET", headers=headers(), ) with urllib.request.urlopen(req) as r: return json.loads(r.read().decode()) def gql(query: str, variables: dict) -> dict: req = urllib.request.Request( "https://api.cloudflare.com/client/v4/graphql", data=json.dumps({"query": query, "variables": variables}).encode(), method="POST", headers=headers(), ) try: with urllib.request.urlopen(req) as r: return json.loads(r.read().decode()) except urllib.error.HTTPError as e: body = e.read().decode() raise SystemExit(f"HTTP {e.code}: {body[:2000]}") from e def fmt_bytes(n: int | float | None) -> str: if n is None: return "-" n = float(n) for unit in ("B", "KB", "MB", "GB", "TB"): if abs(n) < 1024: return f"{n:.1f} {unit}" n /= 1024 return f"{n:.1f} PB" def main() -> None: zd = api(f"/zones?name={ZONE_NAME}") zone = (zd.get("result") or [None])[0] if not zone: raise SystemExit(f"zone not found: {zd.get('errors')}") zid = zone["id"] end = date.today() start = end - timedelta(days=DAYS) # Free plan: httpRequests1dGroups works for multi-day zone totals. # Adaptive (host/path filter) is limited to a 1-day window. zone_q = """ query($zoneTag: string!, $start: Date!, $end: Date!) { viewer { zones(filter: {zoneTag: $zoneTag}) { httpRequests1dGroups( limit: 20 filter: {date_geq: $start, date_leq: $end} orderBy: [date_ASC] ) { dimensions { date } sum { requests bytes cachedRequests threats } uniq { uniques } } } } } """ data = gql( zone_q, {"zoneTag": zid, "start": start.isoformat(), "end": end.isoformat()}, ) if data.get("errors"): raise SystemExit(json.dumps(data["errors"], indent=2)[:4000]) rows = data["data"]["viewer"]["zones"][0]["httpRequests1dGroups"] print(f"\n{ZONE_NAME} zone (all hosts) {start} .. {end}") print(f"{'date':<12} {'requests':>10} {'uniques':>10} {'bytes':>12} {'threats':>8}") tot_r = tot_u = tot_b = tot_t = 0 for row in rows: d = row["dimensions"]["date"] s = row["sum"] u = row["uniq"]["uniques"] print( f"{d:<12} {s['requests']:>10} {u:>10} " f"{fmt_bytes(s['bytes']):>12} {s['threats']:>8}" ) tot_r += s["requests"] tot_u += u tot_b += s["bytes"] tot_t += s["threats"] print( f"{'TOTAL':<12} {tot_r:>10} {tot_u:>10} " f"{fmt_bytes(tot_b):>12} {tot_t:>8}" ) # Host-scoped detail: one day at a time (Free quota). host_q = """ query($zoneTag: string!, $day: Date!, $host: string!) { viewer { zones(filter: {zoneTag: $zoneTag}) { total: httpRequestsAdaptiveGroups( limit: 1 filter: {date: $day, clientRequestHTTPHost: $host} ) { count sum { edgeResponseBytes visits } } byPath: httpRequestsAdaptiveGroups( limit: 15 filter: {date: $day, clientRequestHTTPHost: $host} orderBy: [count_DESC] ) { dimensions { clientRequestPath } count sum { visits } } byCountry: httpRequestsAdaptiveGroups( limit: 8 filter: {date: $day, clientRequestHTTPHost: $host} orderBy: [count_DESC] ) { dimensions { clientCountryName } count sum { visits } } byStatus: httpRequestsAdaptiveGroups( limit: 8 filter: {date: $day, clientRequestHTTPHost: $host} orderBy: [count_DESC] ) { dimensions { edgeResponseStatus } count } } } } """ print(f"\n{HOST} host detail (last {min(DAYS, 7)} days, Free=1d/query)") print(f"{'date':<12} {'requests':>10} {'visits':>10} {'bytes':>12}") host_days = [] for i in range(min(DAYS, 7), -1, -1): day = end - timedelta(days=i) hd = gql( host_q, {"zoneTag": zid, "day": day.isoformat(), "host": HOST}, ) if hd.get("errors"): print(f"{day.isoformat():<12} error: {hd['errors'][0].get('message')}") continue z = hd["data"]["viewer"]["zones"][0] tot = (z["total"] or [{}])[0] c = tot.get("count") or 0 v = (tot.get("sum") or {}).get("visits") or 0 b = (tot.get("sum") or {}).get("edgeResponseBytes") or 0 print(f"{day.isoformat():<12} {c:>10} {v:>10} {fmt_bytes(b):>12}") host_days.append((day, z, c, v, b)) # Detail for the busiest recent day. pick = max(host_days, key=lambda x: x[2], default=None) if pick and pick[2] > 0: day, z, _, _, _ = pick print(f"\nDetail for busiest day {day.isoformat()} ({HOST})") print("Top paths:") for row in z["byPath"][:15]: path = row["dimensions"].get("clientRequestPath") or "/" v = (row.get("sum") or {}).get("visits") or 0 print(f" {row['count']:>8} req {v:>6} visits {path}") print("Top countries:") for row in z["byCountry"]: name = row["dimensions"].get("clientCountryName") or "?" v = (row.get("sum") or {}).get("visits") or 0 print(f" {row['count']:>8} req {v:>6} visits {name}") print("Status codes:") for row in z["byStatus"]: st = row["dimensions"].get("edgeResponseStatus") print(f" {st}: {row['count']}") print( "\nNote: edge requests/visits via Cloudflare proxy (not RUM pageviews). " "Includes assets/bots. Zone totals mix all refining.dev hosts; " "host table is dota2-only." ) if __name__ == "__main__": main()