import logging import sys import json from datetime import datetime, timezone from typing import Optional from app.core.config import settings try: from fluent import handler as fluent_handler except Exception: # pragma: no cover - 运行时兜底 fluent_handler = None class JsonLogFormatter(logging.Formatter): """ 将日志格式化为 JSON(每行一条),便于 Fluentd/ELK 检索。 """ def format(self, record: logging.LogRecord) -> str: log_obj = { "timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds"), "level": record.levelname, "logger": record.name, "message": record.getMessage(), "module": record.module, "funcName": record.funcName, "lineNo": record.lineno, "app": settings.APP_NAME, "env": settings.APP_ENV } if record.exc_info: log_obj["exception"] = self.formatException(record.exc_info) return json.dumps(log_obj, ensure_ascii=False) def _build_fluent_handler() -> Optional[logging.Handler]: """ 创建 Fluentd Handler。 若 fluent-logger 未安装或初始化失败,返回 None。 """ if not settings.FLUENTD_ENABLED: return None if fluent_handler is None: logging.getLogger(__name__).warning("fluent-logger 未安装,已跳过 Fluentd 日志输出。") return None try: fh = fluent_handler.FluentHandler( tag=settings.FLUENTD_TAG, host=settings.FLUENTD_HOST, port=settings.FLUENTD_PORT, timeout=settings.FLUENTD_TIMEOUT_SEC ) # 注意:FluentHandler 不应设置普通字符串 formatter, # 否则发送到 Fluentd 的 record 会退化为 string,导致 record_transformer 报错。 return fh except Exception as e: logging.getLogger(__name__).error(f"初始化 Fluentd Handler 失败: {e}") return None def setup_logging(): """ 配置日志系统,使其同时输出到文件和控制台。 这个函数应该在应用程序启动时只调用一次。 """ # 获取根日志记录器 root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) # 设置根日志记录器的级别 # 如果已经有处理器,先清空,防止重复添加 if root_logger.hasHandlers(): root_logger.handlers.clear() # 创建 JSON 格式化器(用于控制台与本地文件) formatter = JsonLogFormatter() # 1. 创建控制台处理器 (StreamHandler) # - 将日志输出到标准输出(您的终端) console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) # 2. 创建文件处理器 (FileHandler) # - 将日志写入文件 app.log # - mode='w' 表示每次启动都覆盖旧日志 # - encoding='utf-8' 确保正确处理中文字符 file_handler = logging.FileHandler('app.log', mode='w', encoding='utf-8') file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) # 3. 可选:创建 Fluentd 处理器 (fluent-logger) fluentd_handler = _build_fluent_handler() if fluentd_handler: root_logger.addHandler(fluentd_handler) logging.info( "Fluentd日志输出已启用: host=%s port=%s tag=%s", settings.FLUENTD_HOST, settings.FLUENTD_PORT, settings.FLUENTD_TAG ) # 配置完成后,可以记录一条消息来确认 logging.info("日志系统已成功配置,将同时输出到控制台和 app.log 文件。") def get_logger(name: str) -> logging.Logger: """ 获取一个指定名称的日志记录器实例。 假设 setup_logging() 已经在此之前被调用。 """ return logging.getLogger(name)