thermograph/tests/test_metrics.py
Emi Griffith 19f4965258 Metrics: don't count the dashboard's own /api/v2/metrics polling (#135)
The ops dashboard polls the metrics endpoint every few seconds; counting those hits
just shows the monitor watching itself and inflates the inbound totals. Skip the
'metrics' category in record_inbound (so it's excluded from inbound_total too), and
hide it in the dashboard's inbound list defensively.
2026-07-16 21:28:04 +00:00

92 lines
3.7 KiB
Python

"""Unit tests for the in-process traffic counters (metrics.py)."""
import importlib
import metrics
def _fresh():
# Counters are module-level; reload for an isolated starting state per test.
return importlib.reload(metrics)
def test_phase_source_map_covers_every_source():
m = _fresh()
assert m.source_for_phase("history_fetch") == "open-meteo-archive"
assert m.source_for_phase("history_topup") == "open-meteo-archive"
assert m.source_for_phase("recent_forecast_fetch") == "open-meteo-forecast"
assert m.source_for_phase("geocode") == "open-meteo-geocoding"
assert m.source_for_phase("history_nasa") == "nasa-power"
assert m.source_for_phase("forecast_metno") == "met-norway"
assert m.source_for_phase("reverse_geocode") == "nominatim"
assert m.source_for_phase("unknown-phase") == "other"
def test_classify_inbound_categories():
m = _fresh()
c = m.classify_inbound
# BASE stripped, API namespaces
assert c("/thermograph/api/v2/place", "/thermograph") == "api:place"
assert c("/thermograph/api/v2/grade", "/thermograph") == "api:grade"
assert c("/thermograph/api/v1/geocode", "/thermograph") == "api:geocode"
assert c("/thermograph/api/v2/auth/login", "/thermograph") == "auth"
assert c("/thermograph/api/v2/users/me", "/thermograph") == "auth"
assert c("/thermograph/api/v2/subscriptions", "/thermograph") == "accounts"
assert c("/thermograph/api/v2/notifications", "/thermograph") == "accounts"
assert c("/thermograph/api/v2/push/subscribe", "/thermograph") == "accounts"
assert c("/thermograph/api/v2/metrics", "/thermograph") == "metrics"
# pages, static, seo
assert c("/thermograph/", "/thermograph") == "page"
assert c("/thermograph/calendar", "/thermograph") == "page"
assert c("/thermograph/climate/tokyo", "/thermograph") == "page"
assert c("/thermograph/app.js", "/thermograph") == "static"
assert c("/thermograph/style.css", "/thermograph") == "static"
assert c("/thermograph/robots.txt", "/thermograph") == "seo"
# root-mounted (BASE == "")
assert c("/api/v2/place", "") == "api:place"
assert c("/", "") == "page"
def test_record_inbound_buckets_by_status():
m = _fresh()
m.record_inbound("api:place", 200)
m.record_inbound("api:place", 200)
m.record_inbound("api:place", 503)
m.record_inbound("page", 304)
snap = m.snapshot()
assert snap["inbound"]["api:place"]["total"] == 3
assert snap["inbound"]["api:place"]["2xx"] == 2
assert snap["inbound"]["api:place"]["5xx"] == 1
assert snap["inbound"]["page"]["3xx"] == 1
assert snap["inbound_total"] == 4
def test_metrics_endpoint_polling_is_not_counted():
# The dashboard polls the metrics endpoint; that self-traffic must be excluded.
m = _fresh()
m.record_inbound("metrics", 200)
m.record_inbound("metrics", 404)
m.record_inbound("api:place", 200)
snap = m.snapshot()
assert "metrics" not in snap["inbound"]
assert snap["inbound_total"] == 1
def test_record_outbound_by_source_and_outcome():
m = _fresh()
m.record_outbound("history_fetch", "ok")
m.record_outbound("history_topup", "ok") # same source (archive)
m.record_outbound("reverse_geocode", "error")
m.record_outbound("recent_forecast_fetch", "rate_limited")
snap = m.snapshot()
assert snap["outbound"]["open-meteo-archive"]["ok"] == 2
assert snap["outbound"]["nominatim"]["error"] == 1
assert snap["outbound"]["open-meteo-forecast"]["rate_limited"] == 1
assert snap["outbound_total"] == 4
def test_snapshot_has_process_facts():
m = _fresh()
snap = m.snapshot()
assert isinstance(snap["pid"], int)
assert snap["uptime_s"] >= 0
assert "open-meteo-archive" in snap["outbound"] # stable source rows exist