"""底层安全访问日志后台写入器""" from __future__ import annotations import asyncio import logging import uuid from datetime import datetime, timezone from app.db.session import SessionLocal from app.models.security_access_log import SecurityAccessLog logger = logging.getLogger("ctms.security_access_log_writer") BATCH_SIZE = 100 FLUSH_INTERVAL = 3.0 QUEUE_MAX_SIZE = 20000 class SecurityAccessLogWriter: def __init__(self) -> None: self._queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=QUEUE_MAX_SIZE) self._task: asyncio.Task | None = None def enqueue(self, entry: dict) -> None: try: self._queue.put_nowait(entry) except asyncio.QueueFull: logger.warning("Security access log queue full, dropping entry") async def start(self) -> None: self._task = asyncio.create_task(self._flush_loop()) logger.info("SecurityAccessLogWriter started") async def stop(self) -> None: if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass await self._drain() logger.info("SecurityAccessLogWriter stopped") async def _flush_loop(self) -> None: while True: batch = await self._collect_batch() if batch: await self._write_batch(batch) async def _collect_batch(self) -> list[dict]: batch: list[dict] = [] try: first = await asyncio.wait_for(self._queue.get(), timeout=FLUSH_INTERVAL) batch.append(first) except asyncio.TimeoutError: return batch while len(batch) < BATCH_SIZE: try: batch.append(self._queue.get_nowait()) except asyncio.QueueEmpty: break return batch async def _write_batch(self, batch: list[dict]) -> None: try: async with SessionLocal() as session: for entry in batch: session.add( SecurityAccessLog( id=uuid.uuid4(), method=entry["method"], path=entry["path"], status_code=entry["status_code"], elapsed_ms=entry["elapsed_ms"], client_ip=entry.get("client_ip"), user_agent=entry.get("user_agent"), auth_status=entry["auth_status"], user_identifier=entry.get("user_identifier"), created_at=entry.get("created_at", datetime.now(timezone.utc)), ) ) await session.commit() except Exception: logger.exception("Failed to write security access log batch (%d entries)", len(batch)) async def _drain(self) -> None: batch: list[dict] = [] while not self._queue.empty(): try: batch.append(self._queue.get_nowait()) except asyncio.QueueEmpty: break if batch: await self._write_batch(batch) _writer: SecurityAccessLogWriter | None = None def get_security_log_writer() -> SecurityAccessLogWriter | None: return _writer async def start_security_log_writer() -> SecurityAccessLogWriter: global _writer _writer = SecurityAccessLogWriter() await _writer.start() return _writer async def stop_security_log_writer() -> None: global _writer if _writer: await _writer.stop() _writer = None