# plugins/host_agent/scheduler.py from __future__ import annotations import logging from datetime import datetime, timezone from typing import Iterable from sqlalchemy import select from steward.core.alerts import MetricBatch from steward.core.scheduler import ScheduledTask from steward.models.hosts import Host from .models import HostAgentRegistration logger = logging.getLogger(__name__) def seconds_since_seen(regs: Iterable, host_names: dict[str, str], now: datetime) -> list[tuple[str, float]]: """(host name, seconds since its agent last reported) per registration. An agent that has never reported counts from when it was registered. A registration whose host is gone is skipped. """ out = [] for r in regs: name = host_names.get(r.host_id) seen = r.last_seen_at or r.created_at if name is None or seen is None: continue out.append((name, max(0.0, (now - seen).total_seconds()))) return out async def record_seconds_since_seen(app) -> None: """Record host_agent//seconds_since_seen for every registered agent, so "agent offline" is an ordinary alert rule (seconds_since_seen > 180).""" now = datetime.now(timezone.utc) async with app.db_sessionmaker() as session: async with session.begin(): regs = (await session.execute(select(HostAgentRegistration))).scalars().all() if not regs: return host_names = dict((await session.execute( select(Host.id, Host.name).where(Host.id.in_([r.host_id for r in regs])) )).all()) batch = MetricBatch() for name, seconds in seconds_since_seen(regs, host_names, now): batch.add(source_module="host_agent", resource_name=name, metric_name="seconds_since_seen", value=seconds, recorded_at=now) await batch.record(session) def make_task(app) -> ScheduledTask: async def _record(): try: await record_seconds_since_seen(app) except Exception: logger.exception("host_agent: recording seconds_since_seen failed") return ScheduledTask( name="host_agent_seconds_since_seen", coro_factory=_record, interval_seconds=60, run_on_startup=False, )