Files
opsxsql/backend/app/services/log_cleanup.py
T
logan 00db0e5bca feat: 日志自动清理 — 系统设置+定时任务+手动操作
1. 系统设置新增 cleanup 分组(6项配置):
   - 启用自动清理(开关)
   - 清理执行时间(默认凌晨3点)
   - 工单保留天数(默认90天)
   - 审计日志保留天数(默认180天)
   - 查询历史保留天数(默认30天)
   - 定时任务日志保留天数(默认30天)

2. log_cleanup.py: 自动清理服务
   - 只清理已完成/已拒绝的工单
   - 按保留天数过滤
   - 0天=不清理

3. APScheduler cron job: 每天03:00自动清理

4. API endpoints:
   - POST /settings/cleanup/run — 手动触发清理
   - GET /settings/cleanup/preview — 预览待清理数量

5. 前端 PlatformConfigPage:
   - GROUP_META 新增 cleanup 分组
   - CleanupActions 子组件: 预览+手动清理按钮

6. 清空所有历史工单数据(tickets/ticket_approvals/audit_logs/query_histories)
2026-05-25 00:19:38 +08:00

132 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""日志自动清理服务 — 基于 APScheduler cron job。
每天定时清理过期记录:
- 数据变更工单 (tickets + ticket_approvals)
- 审计日志 (audit_logs)
- 查询历史 (query_histories)
- 定时任务日志 (scheduled_task_logs)
清理规则:
- 只清理已完成/已拒绝的工单(status in ['success', 'failed', 'rejected'])
- 每项保留天数由 system_settings 中对应 key 控制
- 0 天 = 不清理
- 清理结果记录到 audit_logs
"""
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional
from sqlalchemy import select, delete, func
from app.core.database import async_session
from app.models.system_setting import SystemSetting
from app.models.ticket import Ticket
from app.models.audit_log import AuditLog
from app.models.query_history import QueryHistory
from app.models.scheduled_task import ScheduledTaskLog
logger = logging.getLogger(__name__)
async def _get_setting(db, key: str) -> Optional[str]:
"""获取系统设置值。"""
result = await db.execute(select(SystemSetting).where(SystemSetting.key == key))
s = result.scalar_one_or_none()
return s.value if s else None
async def _cleanup_tickets(db, days: int) -> int:
"""清理过期工单及其审批记录。"""
if days <= 0:
return 0
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
# 先找要删除的工单 ID
result = await db.execute(
select(Ticket.id).where(
Ticket.status.in_(['success', 'failed', 'rejected']),
Ticket.create_time < cutoff,
)
)
ticket_ids = [row[0] for row in result.all()]
if not ticket_ids:
return 0
# 删除审批记录
from app.models.ticket import TicketApproval
for tid in ticket_ids:
await db.execute(delete(TicketApproval).where(TicketApproval.ticket_id == tid))
# 删除工单
await db.execute(delete(Ticket).where(Ticket.id.in_(ticket_ids)))
await db.commit()
return len(ticket_ids)
async def _cleanup_audit_logs(db, days: int) -> int:
"""清理过期审计日志。"""
if days <= 0:
return 0
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
result = await db.execute(delete(AuditLog).where(AuditLog.created_at < cutoff))
await db.commit()
return result.rowcount
async def _cleanup_query_histories(db, days: int) -> int:
"""清理过期查询历史。"""
if days <= 0:
return 0
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
result = await db.execute(delete(QueryHistory).where(QueryHistory.created_at < cutoff))
await db.commit()
return result.rowcount
async def _cleanup_scheduled_task_logs(db, days: int) -> int:
"""清理过期定时任务日志。"""
if days <= 0:
return 0
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
result = await db.execute(delete(ScheduledTaskLog).where(ScheduledTaskLog.started_at < cutoff))
await db.commit()
return result.rowcount
async def run_cleanup() -> dict:
"""执行一次完整清理,返回各项统计。"""
stats = {
'tickets': 0,
'audit_logs': 0,
'query_histories': 0,
'scheduled_task_logs': 0,
}
try:
async with async_session() as db:
enabled = await _get_setting(db, 'cleanup_enabled')
if enabled != 'true':
logger.info('自动清理已禁用,跳过')
return stats
ticket_days = int(await _get_setting(db, 'cleanup_ticket_days') or '0')
audit_days = int(await _get_setting(db, 'cleanup_audit_log_days') or '0')
query_days = int(await _get_setting(db, 'cleanup_query_history_days') or '0')
task_log_days = int(await _get_setting(db, 'cleanup_scheduled_task_log_days') or '0')
stats['tickets'] = await _cleanup_tickets(db, ticket_days)
stats['audit_logs'] = await _cleanup_audit_logs(db, audit_days)
stats['query_histories'] = await _cleanup_query_histories(db, query_days)
stats['scheduled_task_logs'] = await _cleanup_scheduled_task_logs(db, task_log_days)
logger.info(f'自动清理完成: {stats}')
except Exception as e:
logger.error(f'自动清理失败: {e}')
return stats
async def run_cleanup_sync():
"""sync wrapper for APScheduler job."""
return await run_cleanup()