"""监控数据自动清理服务 按配置的保留周期清理 metrics.db 中的历史数据,防止 SQLite 无限膨胀: - MetricPoint: 高频时序数据,默认保留 30 天 - SecurityCheck: 安全检查历史,默认保留 90 天 - EventLog: 事件日志,默认保留 180 天 提供两种触发方式: 1. 应用启动后由 asyncio 后台任务按 CLEANUP_INTERVAL_HOURS 周期执行 2. 通过 API 手动触发(POST /api/monitor/cleanup) """ import asyncio import logging from datetime import timedelta from sqlmodel import Session, delete, select from app.core.config import settings from app.core.timeutils import utcnow from app.database import metrics_engine from app.models.monitor import EventLog, MetricPoint, SecurityCheck logger = logging.getLogger("vps-manager.cleanup") # 单批删除行数上限:避免大表单条 DELETE 长时间持有写锁,阻塞 Agent 上报 _BATCH_SIZE = 1000 def _delete_in_batches(session: Session, model, cutoff, batch_size: int = _BATCH_SIZE) -> int: """分批删除指定模型的过期数据(ts < cutoff),返回总删除行数 SQLite 不支持 DELETE ... LIMIT,用子查询 SELECT id ... LIMIT 实现分批。 """ total = 0 while True: subq = select(model.id).where(model.ts < cutoff).limit(batch_size) ids = [row[0] if isinstance(row, tuple) else row for row in session.exec(subq).all()] if not ids: break session.exec(delete(model).where(model.id.in_(ids))) session.commit() # 每批独立提交,缩短写锁持有时间 total += len(ids) if len(ids) < batch_size: break return total def cleanup_metrics( metrics_days: int | None = None, security_days: int | None = None, event_log_days: int | None = None, ) -> dict: """按保留天数清理过期监控数据,返回各类删除条数""" metrics_days = metrics_days if metrics_days is not None else settings.METRICS_RETENTION_DAYS security_days = security_days if security_days is not None else settings.SECURITY_RETENTION_DAYS event_log_days = event_log_days if event_log_days is not None else settings.EVENT_LOG_RETENTION_DAYS now = utcnow() result = {"metric_points": 0, "security_checks": 0, "event_logs": 0} with Session(metrics_engine) as session: if metrics_days > 0: cutoff = now - timedelta(days=metrics_days) result["metric_points"] = _delete_in_batches(session, MetricPoint, cutoff) if security_days > 0: cutoff = now - timedelta(days=security_days) result["security_checks"] = _delete_in_batches(session, SecurityCheck, cutoff) if event_log_days > 0: cutoff = now - timedelta(days=event_log_days) result["event_logs"] = _delete_in_batches(session, EventLog, cutoff) logger.info( "监控数据清理完成:metric_points=%s, security_checks=%s, event_logs=%s", result["metric_points"], result["security_checks"], result["event_logs"], ) return result async def cleanup_loop() -> None: """后台周期清理任务(CLEANUP_INTERVAL_HOURS=0 时不启动)""" interval_hours = settings.CLEANUP_INTERVAL_HOURS if interval_hours <= 0: logger.info("监控数据自动清理已禁用(CLEANUP_INTERVAL_HOURS=0)") return interval_sec = interval_hours * 3600 logger.info("监控数据自动清理已启动,间隔 %s 小时", interval_hours) while True: try: # 在线程池执行同步 DB 操作,避免阻塞事件循环 await asyncio.to_thread(cleanup_metrics) except Exception: # noqa: BLE001 logger.exception("监控数据自动清理执行失败") await asyncio.sleep(interval_sec)