#5473 "Opening an alert rule takes up to 15 s". With the rule pickers now reading plugin_metric_latest, nine integration tests failed on rule_fields: "host_agent has no resource 'box-…'". They seeded a resource by adding a raw PluginMetric row, which no production path does: host-agent ingest and record_metric both go through record_metrics, which keeps the latest-value table. The seeds now go through record_metrics too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.7 KiB
Python
84 lines
3.7 KiB
Python
"""Integration: a host-agent ingest POST evaluates alert rules on the host's
|
|
metrics (#5060). Before, ingest wrote rows straight to plugin_metrics and no
|
|
host_agent rule could ever fire.
|
|
|
|
The agent registration is seeded with plain SQL: importing the plugin's
|
|
models again after the app loaded the plugin raises "Table already defined"
|
|
(see test_mcp_plugin_tools)."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
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_ingest_fires_a_host_rule_from_a_backlog(app):
|
|
from sqlalchemy import select, text
|
|
from steward.alerts import service as alert_svc
|
|
from steward.models.alerts import AlertEvent, AlertState, AlertStateEnum
|
|
from steward.models.hosts import Host
|
|
from steward.core.alerts import record_metrics
|
|
from steward.models.metrics import PluginMetric
|
|
from steward.models.users import User, UserRole
|
|
|
|
async def _go():
|
|
tag = uuid.uuid4().hex[:8]
|
|
hostname, token = f"agent-{tag}", f"tok-{tag}-{uuid.uuid4().hex}"
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
user = User(username=f"ha-{tag}", email=f"ha-{tag}@test", password_hash="x",
|
|
role=UserRole.admin)
|
|
host = Host(name=hostname, address="10.9.9.9")
|
|
s.add_all([user, host])
|
|
await s.flush()
|
|
await s.execute(text(
|
|
"INSERT INTO host_agent_registrations "
|
|
"(id, host_id, token_hash, token_created_at, created_at, updated_at) "
|
|
"VALUES (:id, :host, :hash, now(), now(), now())"),
|
|
{"id": str(uuid.uuid4()), "host": host.id,
|
|
"hash": hashlib.sha256(token.encode()).hexdigest()})
|
|
await record_metrics(s, [PluginMetric(source_module="host_agent", resource_name=hostname,
|
|
metric_name="cpu_pct", value=5.0)])
|
|
await s.flush()
|
|
fields = await alert_svc.rule_fields(
|
|
s, name=f"cpu {tag}", source_module="host_agent", resource_name=hostname,
|
|
metric_name="cpu_pct", operator=">", threshold=90,
|
|
consecutive_failures_required=2)
|
|
rule = await alert_svc.add_rule(s, fields, created_by=user.id)
|
|
rule_id = rule.id
|
|
|
|
now = datetime.now(timezone.utc)
|
|
# Two buffered samples in one POST, newest first: evaluated oldest
|
|
# first, they make the two breaching checks the rule needs.
|
|
samples = [{"ts": (now - timedelta(seconds=s)).isoformat(), "cpu_pct": v}
|
|
for s, v in ((0, 97.0), (30, 95.0))]
|
|
client = app.test_client()
|
|
resp = await client.post("/plugins/host_agent/ingest", json={"samples": samples},
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
assert resp.status_code == 200, await resp.get_data(as_text=True)
|
|
|
|
async with app.db_sessionmaker() as s:
|
|
st = (await s.execute(select(AlertState).where(
|
|
AlertState.rule_id == rule_id))).scalar_one()
|
|
events = (await s.execute(select(AlertEvent).where(
|
|
AlertEvent.rule_id == rule_id))).scalars().all()
|
|
assert st.state == AlertStateEnum.firing
|
|
assert [(e.from_state, e.to_state, e.metric_value) for e in events] == [
|
|
("pending", "firing", 97.0)]
|
|
|
|
asyncio.run(_go())
|