CI 8551 integration: "integer out of range" for a 2099 timestamp. The SQL in the log showed a second problem: `CAST(epoch AS INTEGER) / 60` compiled to `/ CAST(60 AS NUMERIC)`, i.e. decimal division. Every scrape got its own "bucket", so the request chart and the Traefik page's sparklines never averaged anything. The int4 cast would also overflow in 2038. Both now use date_bin from the epoch, as host_agent/metrics_query.py does. The integration test adds a second scrape 30 s later in the same minute and checks that the bucket averages the totals and weights the 5xx share over both scrapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
3.2 KiB
Python
77 lines
3.2 KiB
Python
"""Integration: the newest Traefik row per router, read by walking the
|
|
(router_name, scraped_at) index rather than grouping every row (#5254).
|
|
Requires STEWARD_DATABASE_URL."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
if not os.environ.get("STEWARD_DATABASE_URL"):
|
|
pytest.skip("needs Postgres")
|
|
from steward.app import create_app
|
|
return create_app(testing=False)
|
|
|
|
|
|
def test_each_router_reads_as_its_newest_row(app):
|
|
from plugins.traefik.models import TraefikMetric
|
|
from plugins.traefik.routes import latest_per_router
|
|
|
|
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
tag = uuid.uuid4().hex[:8]
|
|
a, b = f"a-{tag}@docker", f"b-{tag}@docker"
|
|
|
|
def row(name, ago, rate):
|
|
return TraefikMetric(router_name=name, scraped_at=now - timedelta(minutes=ago), request_rate=rate,
|
|
error_rate_4xx_pct=0.0, error_rate_5xx_pct=0.0, latency_p50_ms=1.0,
|
|
latency_p95_ms=2.0, latency_p99_ms=3.0, response_bytes_rate=0.0)
|
|
|
|
async def go():
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
s.add_all([row(a, 3, 1.0), row(a, 1, 2.0), row(a, 2, 3.0), row(b, 5, 4.0)])
|
|
async with s.begin():
|
|
return {m.router_name: m for m in await latest_per_router(s)}
|
|
|
|
got = asyncio.run(go())
|
|
assert got[a].request_rate == 2.0 and got[a].scraped_at == now - timedelta(minutes=1)
|
|
assert got[b].request_rate == 4.0
|
|
|
|
|
|
def test_the_request_chart_sums_routers_and_weights_the_5xx_share(app):
|
|
"""#5256: two routers in one scrape are 4 req/s in total, not an average
|
|
of 2; a 5xx share of 50% on the 3 req/s router is 37.5% of that scrape's
|
|
traffic. A second scrape 30 s later shares the minute's bucket, so the
|
|
bucket averages the two totals and weights the share over both."""
|
|
from plugins.traefik.models import TraefikMetric
|
|
from plugins.traefik.routes import request_history
|
|
|
|
# Far ahead of every other test's rows, so only these fall in the window.
|
|
at = datetime(2099, 1, 1, 12, 0, tzinfo=timezone.utc) + timedelta(minutes=uuid.uuid4().int % 10_000)
|
|
|
|
def row(name, rate, e5, when=None):
|
|
return TraefikMetric(router_name=name, scraped_at=when or at, request_rate=rate, error_rate_4xx_pct=0.0,
|
|
error_rate_5xx_pct=e5, latency_p50_ms=1.0, latency_p95_ms=2.0,
|
|
latency_p99_ms=3.0, response_bytes_rate=0.0)
|
|
|
|
async def go():
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
later = at + timedelta(seconds=30)
|
|
s.add_all([row("quiet@docker", 1.0, 0.0), row("busy@docker", 3.0, 50.0),
|
|
row("quiet@docker", 2.0, 0.0, later), row("busy@docker", 4.0, 0.0, later)])
|
|
async with s.begin():
|
|
return await request_history(s, at, 60)
|
|
|
|
got = asyncio.run(go())
|
|
# One bucket: totals 4 and 6 average to 5; 150 weighted 5xx over 10 req/s is 15%.
|
|
assert got == [{"at": at, "req_rate": 5.0, "err_pct": 15.0}]
|