feat: host heartbeat status and dashboard metrics (2.2)
Track agent.heartbeat and report.daily.ssh per host; show online/stale on Hosts UI; dashboard counters and SAC_HEARTBEAT_STALE_MINUTES config.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""Статус агентов по событиям 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"
|
||||
|
||||
|
||||
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 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"
|
||||
)
|
||||
Reference in New Issue
Block a user