- agent/session_stats.py: SessionDB context/compression metrics - agent/skill_stats.py: curator usage.json reader + prune history - agent/system_health.py: gateway uptime, version, cron activity - agent/stats_dashboard.py: Telegram-friendly bullet renderer - cli.py: /stats dispatch + _handle_stats_command method - gateway/run.py: /stats dispatch + _handle_stats_command for messaging platforms - hermes_cli/commands.py: /stats CommandDef registration
91 lines
2.9 KiB
Python
91 lines
2.9 KiB
Python
"""System health and cron telemetry collectors for /stats."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any, Dict
|
|
|
|
|
|
def _parse_dt(value: Any):
|
|
if not value:
|
|
return None
|
|
try:
|
|
dt = datetime.fromisoformat(str(value))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
return dt
|
|
|
|
|
|
def format_duration(seconds: int | float | None) -> str:
|
|
if seconds is None:
|
|
return "unknown"
|
|
seconds = max(0, int(seconds))
|
|
days, rem = divmod(seconds, 86400)
|
|
hours, rem = divmod(rem, 3600)
|
|
minutes, _ = divmod(rem, 60)
|
|
if days:
|
|
return f"{days}d {hours}h"
|
|
if hours:
|
|
return f"{hours}h {minutes}m"
|
|
return f"{minutes}m"
|
|
|
|
|
|
def collect_cron_activity(hours: int = 24) -> Dict[str, Any]:
|
|
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
try:
|
|
from cron.jobs import list_jobs
|
|
jobs = list_jobs(include_disabled=True)
|
|
except Exception:
|
|
jobs = []
|
|
|
|
recent = []
|
|
ok = error = health_checks = 0
|
|
for job in jobs:
|
|
if not isinstance(job, dict):
|
|
continue
|
|
dt = _parse_dt(job.get("last_run_at"))
|
|
if dt is None or dt < cutoff:
|
|
continue
|
|
status = str(job.get("last_status") or "unknown")
|
|
if status == "ok":
|
|
ok += 1
|
|
elif status == "error":
|
|
error += 1
|
|
haystack = " ".join(str(job.get(k) or "") for k in ("name", "prompt", "id")).lower()
|
|
if "health" in haystack or "doctor" in haystack:
|
|
health_checks += 1
|
|
recent.append({"id": job.get("id"), "name": job.get("name"), "status": status, "last_run_at": job.get("last_run_at")})
|
|
recent.sort(key=lambda r: str(r.get("last_run_at") or ""), reverse=True)
|
|
return {"hours": hours, "runs": len(recent), "ok": ok, "error": error, "health_checks": health_checks, "recent": recent[:5]}
|
|
|
|
|
|
def collect_system_health(*, started_at: Any = None, start_monotonic: float | None = None) -> Dict[str, Any]:
|
|
try:
|
|
from hermes_cli import __version__ as version
|
|
except Exception:
|
|
version = "unknown"
|
|
|
|
uptime_seconds = None
|
|
if start_monotonic is not None:
|
|
uptime_seconds = time.monotonic() - float(start_monotonic)
|
|
else:
|
|
dt = started_at
|
|
if isinstance(dt, (int, float)):
|
|
uptime_seconds = time.time() - float(dt)
|
|
elif isinstance(dt, datetime):
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
uptime_seconds = (datetime.now(timezone.utc) - dt).total_seconds()
|
|
|
|
return {
|
|
"version": version,
|
|
"pid": os.getpid(),
|
|
"uptime_seconds": int(uptime_seconds) if uptime_seconds is not None else None,
|
|
"uptime": format_duration(uptime_seconds),
|
|
"cron": collect_cron_activity(hours=24),
|
|
}
|