"""#5844: one metrics URL can reach several Traefik processes (swarm replicas, pods, a keepalived pair, a restart). Each is counted only against its own last scrape, so one replica's lifetime totals never read as another's new traffic.""" from plugins.traefik.scraper import BASELINE_TTL_SECONDS, Baselines, compute_router_metrics, instance_of REQ = "traefik_service_requests_total" def _scrape(started: float | None, **counts): """A scrape from the process started at `started`, with {code: total} for qbittorrent@file.""" out = {REQ: {frozenset({"service": "qbittorrent@file", "code": c, "method": "GET"}.items()): v for c, v in counts.items()}} if started is not None: out["process_start_time_seconds"] = {frozenset(): started} return out def test_each_process_is_compared_only_with_itself(): b = Baselines() a1, b1 = _scrape(1000.0, **{"200": 10}), _scrape(2000.0, **{"502": 336}) assert b.swap(a1, 0.0) == (None, 0.0) # The other replica answers: it has no baseline yet, so nothing is counted. assert b.swap(b1, 60.0) == (None, 0.0) # Back to the first: compared with its own scrape two minutes ago. a2 = _scrape(1000.0, **{"200": 22}) assert b.swap(a2, 120.0) == (a1, 120.0) assert b.swap(_scrape(2000.0, **{"502": 336}), 180.0) == (b1, 120.0) def test_landing_on_the_other_replica_no_longer_reads_its_totals_as_traffic(): """The live symptom: an idle retired router flashing 336 502s, 100% 5xx.""" b = Baselines() b.swap(_scrape(1000.0, **{"200": 10}), 0.0) previous, elapsed = b.swap(_scrape(2000.0, **{"502": 336}), 60.0) got = compute_router_metrics(_scrape(2000.0, **{"502": 336}), previous, elapsed) assert got["qbittorrent@file"]["request_rate"] == 0.0 assert got["qbittorrent@file"]["error_rate_5xx_pct"] == 0.0 def test_a_restarted_traefik_starts_a_new_baseline(): b = Baselines() b.swap(_scrape(1000.0, **{"200": 5000}), 0.0) assert b.swap(_scrape(3000.0, **{"200": 3}), 60.0) == (None, 0.0) def test_without_a_start_time_every_scrape_is_one_process_as_before(): b = Baselines() first = _scrape(None, **{"200": 1}) assert instance_of(first) == "" b.swap(first, 0.0) assert b.swap(_scrape(None, **{"200": 2}), 60.0) == (first, 60.0) def test_a_baseline_older_than_the_ttl_is_dropped_not_compared(): b = Baselines() b.swap(_scrape(1000.0, **{"200": 1}), 0.0) assert b.swap(_scrape(1000.0, **{"200": 900}), BASELINE_TTL_SECONDS + 1) == (None, 0.0)