fix: dashboard and hosts count SSH and RDP daily reports
analytics-a4: daily_reports_24h uses report.daily.*; sac-deploy enables timers Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,7 +10,7 @@ from app.config import get_settings
|
|||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models import Event, Host, Problem
|
from app.models import Event, Host, Problem
|
||||||
from app.schemas.list_models import EventSummary
|
from app.schemas.list_models import EventSummary
|
||||||
from app.services.host_health import DAILY_REPORT_SSH, HEARTBEAT_TYPE, count_stale_hosts
|
from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts
|
||||||
|
|
||||||
router = APIRouter(prefix="/dashboards", tags=["dashboards"])
|
router = APIRouter(prefix="/dashboards", tags=["dashboards"])
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ def dashboard_summary(
|
|||||||
daily_reports_24h = db.scalar(
|
daily_reports_24h = db.scalar(
|
||||||
select(func.count())
|
select(func.count())
|
||||||
.select_from(Event)
|
.select_from(Event)
|
||||||
.where(Event.received_at >= since, Event.type == DAILY_REPORT_SSH)
|
.where(Event.received_at >= since, Event.type.in_(DAILY_REPORT_TYPES))
|
||||||
) or 0
|
) or 0
|
||||||
problems_open = (
|
problems_open = (
|
||||||
db.scalar(select(func.count()).select_from(Problem).where(Problem.status == "open")) or 0
|
db.scalar(select(func.count()).select_from(Problem).where(Problem.status == "open")) or 0
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ from app.database import get_db
|
|||||||
from app.models import Event, Host
|
from app.models import Event, Host
|
||||||
from app.schemas.list_models import HostListResponse, HostSummary
|
from app.schemas.list_models import HostListResponse, HostSummary
|
||||||
from app.services.host_health import (
|
from app.services.host_health import (
|
||||||
DAILY_REPORT_SSH,
|
|
||||||
HEARTBEAT_TYPE,
|
HEARTBEAT_TYPE,
|
||||||
agent_status,
|
agent_status,
|
||||||
|
max_daily_report_time_by_host,
|
||||||
max_event_time_by_host,
|
max_event_time_by_host,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ def list_hosts(
|
|||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE)
|
hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE)
|
||||||
report_map = max_event_time_by_host(db, DAILY_REPORT_SSH)
|
report_map = max_daily_report_time_by_host(db)
|
||||||
|
|
||||||
items: list[HostSummary] = []
|
items: list[HostSummary] = []
|
||||||
for host in rows:
|
for host in rows:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from app.auth.jwt_auth import verify_access_token
|
|||||||
from app.database import SessionLocal
|
from app.database import SessionLocal
|
||||||
from app.models import Event, Problem
|
from app.models import Event, Problem
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.services.host_health import DAILY_REPORT_SSH, HEARTBEAT_TYPE, count_stale_hosts
|
from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts
|
||||||
from app.version import APP_VERSION
|
from app.version import APP_VERSION
|
||||||
|
|
||||||
router = APIRouter(prefix="/stream", tags=["stream"])
|
router = APIRouter(prefix="/stream", tags=["stream"])
|
||||||
@@ -43,7 +43,7 @@ def _dashboard_snapshot(db: Session) -> dict:
|
|||||||
"daily_reports_24h": db.scalar(
|
"daily_reports_24h": db.scalar(
|
||||||
select(func.count())
|
select(func.count())
|
||||||
.select_from(Event)
|
.select_from(Event)
|
||||||
.where(Event.received_at >= since, Event.type == DAILY_REPORT_SSH)
|
.where(Event.received_at >= since, Event.type.in_(DAILY_REPORT_TYPES))
|
||||||
)
|
)
|
||||||
or 0,
|
or 0,
|
||||||
"problems_open": problems_open,
|
"problems_open": problems_open,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from app.models import Event, Host
|
|||||||
|
|
||||||
HEARTBEAT_TYPE = "agent.heartbeat"
|
HEARTBEAT_TYPE = "agent.heartbeat"
|
||||||
DAILY_REPORT_SSH = "report.daily.ssh"
|
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]:
|
def max_event_time_by_host(db: Session, event_type: str) -> dict[int, datetime]:
|
||||||
@@ -20,6 +22,15 @@ def max_event_time_by_host(db: Session, event_type: str) -> dict[int, datetime]:
|
|||||||
return {int(host_id): ts for host_id, ts in rows if ts is not None}
|
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(
|
def agent_status(
|
||||||
last_heartbeat_at: datetime | None,
|
last_heartbeat_at: datetime | None,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -92,6 +92,20 @@ def test_dashboard_summary_top_and_problems_24h(client, db_session, jwt_headers)
|
|||||||
assert types["agent.heartbeat"] == 2
|
assert types["agent.heartbeat"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_daily_reports_24h_includes_ssh_and_rdp(client, db_session, jwt_headers):
|
||||||
|
h_ssh = _host(db_session, "ssh-pilot")
|
||||||
|
h_rdp = _host(db_session, "rdp-pilot")
|
||||||
|
h_rdp.product = "rdp-login-monitor"
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
_event(db_session, h_ssh, "report.daily.ssh", received_at=now)
|
||||||
|
_event(db_session, h_rdp, "report.daily.rdp", received_at=now)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
r = client.get("/api/v1/dashboards/summary", headers=jwt_headers)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["daily_reports_24h"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_recent_events_ordered_by_received_at(client, db_session, jwt_headers):
|
def test_dashboard_recent_events_ordered_by_received_at(client, db_session, jwt_headers):
|
||||||
h = _host(db_session, "live-host")
|
h = _host(db_session, "live-host")
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|||||||
@@ -122,4 +122,22 @@ else
|
|||||||
die "${SERVICE_NAME} запущен, но /health не ответил — journalctl -u ${SERVICE_NAME} -n 80 --no-pager"
|
die "${SERVICE_NAME} запущен, но /health не ответил — journalctl -u ${SERVICE_NAME} -n 80 --no-pager"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
for aux in sac-retention sac-daily-report; do
|
||||||
|
for suffix in service timer; do
|
||||||
|
src="${APP_ROOT}/deploy/systemd/${aux}.${suffix}"
|
||||||
|
dst="/etc/systemd/system/${aux}.${suffix}"
|
||||||
|
if [ -f "${src}" ]; then
|
||||||
|
if [ ! -f "${dst}" ] || ! cmp -s "${src}" "${dst}"; then
|
||||||
|
log "Обновление ${dst}"
|
||||||
|
cp "${src}" "${dst}"
|
||||||
|
systemctl daemon-reload
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -f "/etc/systemd/system/${aux}.timer" ]; then
|
||||||
|
systemctl enable "${aux}.timer" 2>/dev/null || true
|
||||||
|
systemctl start "${aux}.timer" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
log "Готово."
|
log "Готово."
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
| analytics-a1 | `GET /api/v1/analytics/series` (`bucket=hour\|day\|week\|month`, `from`/`to`) | JSON series + тесты |
|
| analytics-a1 | `GET /api/v1/analytics/series` (`bucket=hour\|day\|week\|month`, `from`/`to`) | JSON series + тесты |
|
||||||
| analytics-a2 | `group_by=severity\|type\|product`, `exclude_types=agent.heartbeat` | heartbeat не в общем счётчике |
|
| analytics-a2 | `group_by=severity\|type\|product`, `exclude_types=agent.heartbeat` | heartbeat не в общем счётчике |
|
||||||
| analytics-a3 | Presets 24h / 7d / 30d / 90d | документация API |
|
| analytics-a3 | Presets 24h / 7d / 30d / 90d | документация API |
|
||||||
| analytics-a4 | Dashboard: `daily_reports_24h` учитывает `report.daily.rdp` | test_dashboard |
|
| analytics-a4 | Dashboard: `daily_reports_24h` учитывает `report.daily.rdp` | ✅ test_dashboard |
|
||||||
| analytics-a5 | Индексы под агрегации | EXPLAIN без full scan |
|
| analytics-a5 | Индексы под агрегации | EXPLAIN без full scan |
|
||||||
|
|
||||||
## Фаза B — UI
|
## Фаза B — UI
|
||||||
|
|||||||
Reference in New Issue
Block a user