f9d56621f6
Co-authored-by: Cursor <cursoragent@cursor.com>
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""Статус агентов по событиям 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
|