"""Статус агентов по событиям agent.heartbeat / report.daily.* в БД.""" from datetime import datetime, timezone from sqlalchemy import func, select from sqlalchemy.orm import Session from app.models import Event, Host HEARTBEAT_TYPE = "agent.heartbeat" DAILY_REPORT_SSH = "report.daily.ssh" DAILY_REPORT_RDP = "report.daily.rdp" DAILY_REPORT_TYPES = (DAILY_REPORT_SSH, DAILY_REPORT_RDP) def max_event_time_by_host(db: Session, event_type: str) -> dict[int, datetime]: rows = db.execute( select(Event.host_id, func.max(Event.received_at)) .where(Event.type == event_type) .group_by(Event.host_id) ).all() return {int(host_id): ts for host_id, ts in rows if ts is not None} def max_daily_report_time_by_host(db: Session) -> dict[int, datetime]: rows = db.execute( select(Event.host_id, func.max(Event.received_at)) .where(Event.type.in_(DAILY_REPORT_TYPES)) .group_by(Event.host_id) ).all() return {int(host_id): ts for host_id, ts in rows if ts is not None} def agent_status( last_heartbeat_at: datetime | None, *, stale_minutes: int, now: datetime | None = None, ) -> str: """ online — heartbeat не старше порога; stale — был хост, но heartbeat устарел или отсутствует; unknown — heartbeat ещё не приходил. """ if last_heartbeat_at is None: return "unknown" ref = now or datetime.now(timezone.utc) hb = last_heartbeat_at if hb.tzinfo is None: hb = hb.replace(tzinfo=timezone.utc) age_sec = (ref - hb).total_seconds() if age_sec > stale_minutes * 60: return "stale" return "online" def count_stale_hosts(db: Session, stale_minutes: int) -> int: hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE) host_ids = db.scalars(select(Host.id)).all() return sum( 1 for host_id in host_ids if agent_status(hb_map.get(host_id), stale_minutes=stale_minutes) == "stale" ) def apply_agent_status_host_filter( stmt, count_stmt, agent_status_filter: str, *, stale_minutes: int, ): """Ограничить выборку Host по online / stale / unknown (как в agent_status).""" from datetime import timedelta hb_subq = ( select(Event.host_id, func.max(Event.received_at).label("last_hb")) .where(Event.type == HEARTBEAT_TYPE) .group_by(Event.host_id) .subquery() ) cutoff = datetime.now(timezone.utc) - timedelta(minutes=stale_minutes) if agent_status_filter == "online": join = stmt.join(hb_subq, Host.id == hb_subq.c.host_id) count_join = count_stmt.join(hb_subq, Host.id == hb_subq.c.host_id) cond = hb_subq.c.last_hb >= cutoff return join.where(cond), count_join.where(cond) if agent_status_filter == "stale": join = stmt.join(hb_subq, Host.id == hb_subq.c.host_id) count_join = count_stmt.join(hb_subq, Host.id == hb_subq.c.host_id) cond = hb_subq.c.last_hb < cutoff return join.where(cond), count_join.where(cond) if agent_status_filter == "unknown": join = stmt.outerjoin(hb_subq, Host.id == hb_subq.c.host_id) count_join = count_stmt.outerjoin(hb_subq, Host.id == hb_subq.c.host_id) cond = hb_subq.c.last_hb.is_(None) return join.where(cond), count_join.where(cond) return stmt, count_stmt