""" JSON 结构化日志配置 — 用于 ELK 日志聚合。 用法: 在 main.py 启动时调用 setup_json_logging() 即可。 会在 logs/ 目录下并行输出 app.json.log(JSON 格式)。 现有文本格式日志不受影响。 """ from __future__ import annotations import json import logging import os from datetime import datetime, timezone from logging.handlers import RotatingFileHandler from pathlib import Path from typing import Any from app.core.config import settings class JsonFormatter(logging.Formatter): """将日志记录格式化为单行 JSON,便于 Filebeat → Elasticsearch 采集。""" def format(self, record: logging.LogRecord) -> str: log_entry: dict[str, Any] = { "timestamp": datetime.fromtimestamp( record.created, tz=timezone.utc ).isoformat(), "level": record.levelname, "logger": record.name, "message": record.getMessage(), "module": record.module, "function": record.funcName, "line": record.lineno, } # 异常信息 if record.exc_info and record.exc_info[1]: log_entry["exception"] = self.formatException(record.exc_info) # 上下文字段(如 request_id / user_id) for key in ("request_id", "user_id", "workspace_id", "client_ip", "method", "path", "status_code", "duration_ms"): val = getattr(record, key, None) if val is not None: log_entry[key] = val return json.dumps(log_entry, ensure_ascii=False, default=str) def setup_json_logging() -> None: """为 root logger 添加 JSON 格式的 RotatingFileHandler。 日志写入 LOG_DIR/app.json.log,大小达到 LOG_MAX_BYTES 时轮转。 """ log_dir = Path(settings.LOG_DIR) log_dir.mkdir(parents=True, exist_ok=True) json_log_path = log_dir / "app.json.log" # 避免重复添加(uvicorn reload 时会重新执行 startup) root = logging.getLogger() for h in root.handlers: if isinstance(h, RotatingFileHandler) and str(json_log_path) in getattr(h, 'baseFilename', ''): return handler = RotatingFileHandler( json_log_path, maxBytes=settings.LOG_MAX_BYTES, backupCount=settings.LOG_BACKUP_COUNT, encoding="utf-8", ) handler.setFormatter(JsonFormatter()) handler.setLevel(getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)) root.addHandler(handler) logging.getLogger(__name__).info("JSON 日志已启用 → %s", json_log_path)