d5279b124f
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
152 lines
5.5 KiB
Python
152 lines
5.5 KiB
Python
"""监测数据留存清理任务。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any
|
|
|
|
from sqlalchemy import delete
|
|
|
|
from app.core.config import settings
|
|
from app.db.session import SessionLocal
|
|
from app.models.permission_access_log import PermissionAccessLog
|
|
from app.models.permission_metric_snapshot import PermissionMetricSnapshot
|
|
from app.models.security_access_log import SecurityAccessLog
|
|
from app.models.source_location_snapshot import SourceLocationSnapshot
|
|
from app.models.user_login_session import UserLoginSession
|
|
|
|
logger = logging.getLogger("ctms.monitoring_retention")
|
|
|
|
|
|
@dataclass
|
|
class MonitoringRetentionState:
|
|
running: bool = False
|
|
last_run_started_at: datetime | None = None
|
|
last_success_at: datetime | None = None
|
|
last_error_at: datetime | None = None
|
|
last_error_type: str | None = None
|
|
error_count: int = 0
|
|
last_deleted: dict[str, int] = field(default_factory=dict)
|
|
total_deleted: dict[str, int] = field(default_factory=dict)
|
|
|
|
def snapshot(self) -> dict[str, Any]:
|
|
return {
|
|
"running": self.running,
|
|
"access_log_retention_days": settings.MONITORING_ACCESS_LOG_RETENTION_DAYS,
|
|
"metric_retention_days": settings.MONITORING_METRIC_RETENTION_DAYS,
|
|
"login_activity_retention_days": settings.USER_LOGIN_ACTIVITY_RETENTION_DAYS,
|
|
"interval_seconds": settings.MONITORING_RETENTION_INTERVAL_SECONDS,
|
|
"last_run_started_at": (
|
|
self.last_run_started_at.isoformat() if self.last_run_started_at else None
|
|
),
|
|
"last_success_at": self.last_success_at.isoformat() if self.last_success_at else None,
|
|
"last_error_at": self.last_error_at.isoformat() if self.last_error_at else None,
|
|
"last_error_type": self.last_error_type,
|
|
"error_count": self.error_count,
|
|
"last_deleted": dict(self.last_deleted),
|
|
"total_deleted": dict(self.total_deleted),
|
|
}
|
|
|
|
|
|
_state = MonitoringRetentionState()
|
|
|
|
|
|
def get_monitoring_retention_status() -> dict[str, Any]:
|
|
return _state.snapshot()
|
|
|
|
|
|
def _deleted_count(result: Any) -> int:
|
|
rowcount = getattr(result, "rowcount", 0)
|
|
return max(0, int(rowcount or 0))
|
|
|
|
|
|
async def purge_expired_monitoring_data(
|
|
*, now: datetime | None = None
|
|
) -> dict[str, int]:
|
|
reference_time = now or datetime.now(timezone.utc)
|
|
access_cutoff = reference_time - timedelta(
|
|
days=settings.MONITORING_ACCESS_LOG_RETENTION_DAYS
|
|
)
|
|
metric_cutoff = reference_time - timedelta(
|
|
days=settings.MONITORING_METRIC_RETENTION_DAYS
|
|
)
|
|
login_activity_cutoff = reference_time - timedelta(
|
|
days=settings.USER_LOGIN_ACTIVITY_RETENTION_DAYS
|
|
)
|
|
|
|
async with SessionLocal() as session:
|
|
permission_result = await session.execute(
|
|
delete(PermissionAccessLog).where(PermissionAccessLog.created_at < access_cutoff)
|
|
)
|
|
security_result = await session.execute(
|
|
delete(SecurityAccessLog).where(SecurityAccessLog.created_at < access_cutoff)
|
|
)
|
|
metric_result = await session.execute(
|
|
delete(PermissionMetricSnapshot).where(
|
|
PermissionMetricSnapshot.bucket_time < metric_cutoff
|
|
)
|
|
)
|
|
source_location_result = await session.execute(
|
|
delete(SourceLocationSnapshot).where(
|
|
SourceLocationSnapshot.bucket_time < access_cutoff
|
|
)
|
|
)
|
|
login_session_result = await session.execute(
|
|
delete(UserLoginSession).where(UserLoginSession.last_seen_at < login_activity_cutoff)
|
|
)
|
|
await session.commit()
|
|
|
|
return {
|
|
"permission_access_logs": _deleted_count(permission_result),
|
|
"security_access_logs": _deleted_count(security_result),
|
|
"permission_metric_snapshots": _deleted_count(metric_result),
|
|
"source_location_snapshots": _deleted_count(source_location_result),
|
|
"user_login_sessions": _deleted_count(login_session_result),
|
|
}
|
|
|
|
|
|
async def run_monitoring_retention_once(
|
|
*, now: datetime | None = None
|
|
) -> dict[str, int]:
|
|
_state.last_run_started_at = now or datetime.now(timezone.utc)
|
|
try:
|
|
deleted = await purge_expired_monitoring_data(now=now)
|
|
except Exception as exc:
|
|
_state.last_error_at = datetime.now(timezone.utc)
|
|
_state.last_error_type = type(exc).__name__
|
|
_state.error_count += 1
|
|
raise
|
|
|
|
_state.last_success_at = datetime.now(timezone.utc)
|
|
_state.last_deleted = deleted
|
|
for key, count in deleted.items():
|
|
_state.total_deleted[key] = _state.total_deleted.get(key, 0) + count
|
|
return deleted
|
|
|
|
|
|
async def run_monitoring_retention(stop_event: asyncio.Event) -> None:
|
|
logger.info("Monitoring retention task started")
|
|
_state.running = True
|
|
try:
|
|
while not stop_event.is_set():
|
|
try:
|
|
deleted = await run_monitoring_retention_once()
|
|
if any(deleted.values()):
|
|
logger.info("Purged expired monitoring data: %s", deleted)
|
|
except Exception:
|
|
logger.exception("Failed to purge expired monitoring data")
|
|
|
|
try:
|
|
await asyncio.wait_for(
|
|
stop_event.wait(),
|
|
timeout=settings.MONITORING_RETENTION_INTERVAL_SECONDS,
|
|
)
|
|
except asyncio.TimeoutError:
|
|
continue
|
|
finally:
|
|
_state.running = False
|
|
logger.info("Monitoring retention task stopped")
|