Files
ctms/backend/app/services/security_access_log_writer.py
T
Cheng Zhou b491b6a146
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (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
发布候选:整合桌面端界面与发布稳定化里程碑
2026-07-01 10:53:24 +08:00

123 lines
4.0 KiB
Python

"""底层安全访问日志后台写入器"""
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"),
client_type=entry.get("client_type"),
client_version=entry.get("client_version"),
client_platform=entry.get("client_platform"),
build_channel=entry.get("build_channel"),
build_commit=entry.get("build_commit"),
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