release(main): 同步 dev 最新候选改动
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
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
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""监测数据留存清理任务。"""
|
||||
|
||||
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")
|
||||
Reference in New Issue
Block a user