#5066 "Alert history page". /alerts/history lists every alert transition in a range (1h–30d, the shared range picker), newest first, all rules or one (?rule=). Each row: from → to, the value against the rule's condition ("no data" for a silent one), email and webhook sent or failed with the error, "muted · <window>" (and "notified when the window ended") taken out of the event note, other notes, and a link to the Ansible run an action started. Range and rule changes swap only the rows (HTMX). Viewer role. The Alerts page links each rule's history and the whole history. The query moves to alert_svc.history, which MCP get_alert_history now uses too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
4.1 KiB
Python
84 lines
4.1 KiB
Python
"""Integration: /alerts/history (#5066) against real Postgres. A viewer can
|
|
read it; it shows the transition, value against the condition, delivery
|
|
outcome with its error, and a maintenance mute; ?rule= narrows it to one
|
|
rule, and an HTMX request gets only the rows."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import time
|
|
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_a_viewer_reads_the_alert_history(app):
|
|
from steward.core.alerts import MUTED
|
|
from steward.models.alerts import AlertEvent, AlertOperator, AlertRule
|
|
from steward.models.users import User, UserRole
|
|
|
|
async def _go():
|
|
tag = uuid.uuid4().hex[:8]
|
|
uid = str(uuid.uuid4())
|
|
now = datetime.now(timezone.utc)
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
s.add(User(id=uid, username=f"hist-{tag}", email=f"hist-{tag}@test",
|
|
password_hash="x", role=UserRole.viewer, is_active=True))
|
|
await s.flush()
|
|
hot = AlertRule(name=f"hot {tag}", source_module="host_agent", resource_name=f"box-{tag}",
|
|
metric_name="cpu_pct", operator=AlertOperator(">"), threshold=90,
|
|
created_by=uid)
|
|
cold = AlertRule(name=f"cold {tag}", source_module="host_agent", resource_name=f"box-{tag}",
|
|
metric_name="cpu_pct", operator=AlertOperator("<"), threshold=1,
|
|
created_by=uid)
|
|
s.add_all([hot, cold])
|
|
await s.flush()
|
|
s.add_all([
|
|
AlertEvent(rule_id=hot.id, from_state="inactive", to_state="firing", metric_value=97.5,
|
|
transitioned_at=now - timedelta(minutes=5), email_sent=False,
|
|
email_error=f"mx-{tag}: timed out", webhook_sent=True),
|
|
AlertEvent(rule_id=hot.id, from_state="firing", to_state="resolved", metric_value=None,
|
|
transitioned_at=now - timedelta(minutes=2), note=f"{MUTED} patch-{tag}"),
|
|
AlertEvent(rule_id=cold.id, from_state="inactive", to_state="firing", metric_value=0.5,
|
|
transitioned_at=now - timedelta(minutes=1), note=f"cold-note-{tag}"),
|
|
# Outside a 1h range.
|
|
AlertEvent(rule_id=hot.id, from_state="pending", to_state="firing", metric_value=95,
|
|
transitioned_at=now - timedelta(hours=3), note=f"old-{tag}"),
|
|
])
|
|
hot_id = hot.id
|
|
|
|
client = app.test_client()
|
|
async with client.session_transaction() as sess:
|
|
sess.update(user_id=uid, user_role="viewer", username=f"hist-{tag}", login_at=time.time())
|
|
|
|
resp = await client.get("/alerts/history?range=1h")
|
|
assert resp.status_code == 200
|
|
page = await resp.get_data(as_text=True)
|
|
assert "Alert History" in page and 'href="/alerts/"' in page # crumbs back to Alerts
|
|
assert f"hot {tag}" in page and f"cold-note-{tag}" in page and f"old-{tag}" not in page
|
|
assert "cpu_pct = 97.5" in page and "rule: > 90" in page
|
|
assert "email failed" in page and f"mx-{tag}: timed out" in page and "webhook sent" in page
|
|
assert f"muted · patch-{tag}" in page and "cpu_pct = no data" in page
|
|
|
|
resp = await client.get(f"/alerts/history?range=24h&rule={hot_id}", headers={"HX-Request": "true"})
|
|
rows = await resp.get_data(as_text=True)
|
|
assert "<html" not in rows and "<table" in rows
|
|
assert f"old-{tag}" in rows and f"cold-note-{tag}" not in rows
|
|
|
|
# The rules list links each rule to its history.
|
|
assert f'/alerts/history?rule={hot_id}' in await (await client.get("/alerts/")).get_data(as_text=True)
|
|
|
|
asyncio.run(_go())
|