2025-10-30 11:14:43 +08:00
|
|
|
"""统一日志配置模块 - Uvicorn风格"""
|
|
|
|
|
import logging
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from logging.handlers import RotatingFileHandler
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UvicornFormatter(logging.Formatter):
|
|
|
|
|
"""Uvicorn风格的日志格式化器"""
|
|
|
|
|
|
|
|
|
|
# 日志级别颜色(ANSI转义码)
|
|
|
|
|
COLORS = {
|
|
|
|
|
'DEBUG': '\033[36m', # 青色
|
|
|
|
|
'INFO': '\033[32m', # 绿色
|
|
|
|
|
'WARNING': '\033[33m', # 黄色
|
|
|
|
|
'ERROR': '\033[31m', # 红色
|
|
|
|
|
'CRITICAL': '\033[35m', # 紫色
|
|
|
|
|
}
|
|
|
|
|
RESET = '\033[0m'
|
|
|
|
|
|
|
|
|
|
def __init__(self, use_colors: bool = True):
|
|
|
|
|
"""
|
|
|
|
|
初始化格式化器
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
use_colors: 是否使用颜色(控制台输出使用,文件输出不使用)
|
|
|
|
|
"""
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.use_colors = use_colors
|
|
|
|
|
|
|
|
|
|
def format(self, record):
|
|
|
|
|
"""格式化日志记录为 Uvicorn 风格"""
|
|
|
|
|
# 获取日志级别名称
|
|
|
|
|
levelname = record.levelname
|
|
|
|
|
|
|
|
|
|
# 添加颜色(如果启用且终端支持)
|
|
|
|
|
if self.use_colors and sys.stderr.isatty():
|
|
|
|
|
colored_level = f"{self.COLORS.get(levelname, '')}{levelname}{self.RESET}"
|
|
|
|
|
else:
|
|
|
|
|
colored_level = levelname
|
|
|
|
|
|
|
|
|
|
# 添加请求追踪ID(如果存在)
|
|
|
|
|
request_id = getattr(record, 'request_id', None)
|
|
|
|
|
request_id_str = f" [{request_id}]" if request_id else ""
|
|
|
|
|
|
2026-04-29 23:47:04 +08:00
|
|
|
# 格式化时间戳 (YYYY-MM-DD HH:MM:SS)
|
|
|
|
|
timestamp = self.formatTime(record, self.datefmt)
|
|
|
|
|
|
|
|
|
|
# Uvicorn风格格式: INFO: [2024-01-01 12:00:00] module_name - message [request_id]
|
2025-10-30 11:14:43 +08:00
|
|
|
# 注意:INFO后面有5个空格,保持对齐
|
2026-04-29 23:47:04 +08:00
|
|
|
return f"{colored_level}: [{timestamp}] {record.name}{request_id_str} - {record.getMessage()}"
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# 全局标志,防止重复初始化
|
|
|
|
|
_logging_configured = False
|
|
|
|
|
|
|
|
|
|
def setup_logging(
|
|
|
|
|
level: str = "INFO",
|
|
|
|
|
log_to_file: bool = False,
|
|
|
|
|
log_file_path: Optional[str] = None,
|
|
|
|
|
max_bytes: int = 10 * 1024 * 1024,
|
|
|
|
|
backup_count: int = 30
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
配置统一的 Uvicorn 风格日志系统
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
|
|
|
log_to_file: 是否输出到文件
|
|
|
|
|
log_file_path: 日志文件路径
|
|
|
|
|
max_bytes: 单个日志文件最大字节数(默认10MB)
|
|
|
|
|
backup_count: 保留的备份文件数量(默认30个)
|
|
|
|
|
"""
|
|
|
|
|
global _logging_configured
|
|
|
|
|
|
|
|
|
|
# 如果已经配置过,直接返回
|
|
|
|
|
if _logging_configured:
|
|
|
|
|
return logging.getLogger()
|
|
|
|
|
|
|
|
|
|
# 获取根日志器
|
|
|
|
|
root_logger = logging.getLogger()
|
|
|
|
|
root_logger.setLevel(getattr(logging, level.upper()))
|
|
|
|
|
|
|
|
|
|
# 清除已有的处理器,避免重复
|
|
|
|
|
root_logger.handlers.clear()
|
|
|
|
|
|
|
|
|
|
# 1. 创建控制台处理器(带颜色)
|
|
|
|
|
console_handler = logging.StreamHandler(sys.stderr)
|
|
|
|
|
console_handler.setLevel(getattr(logging, level.upper()))
|
|
|
|
|
console_formatter = UvicornFormatter(use_colors=True)
|
|
|
|
|
console_handler.setFormatter(console_formatter)
|
|
|
|
|
root_logger.addHandler(console_handler)
|
|
|
|
|
|
|
|
|
|
# 2. 创建文件处理器(如果启用)
|
|
|
|
|
if log_to_file and log_file_path:
|
|
|
|
|
# 确保日志目录存在
|
|
|
|
|
log_file = Path(log_file_path)
|
|
|
|
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# 使用RotatingFileHandler实现日志轮转
|
|
|
|
|
file_handler = RotatingFileHandler(
|
|
|
|
|
filename=log_file_path,
|
|
|
|
|
maxBytes=max_bytes,
|
|
|
|
|
backupCount=backup_count,
|
|
|
|
|
encoding='utf-8'
|
|
|
|
|
)
|
|
|
|
|
file_handler.setLevel(getattr(logging, level.upper()))
|
|
|
|
|
|
|
|
|
|
# 文件日志不使用颜色
|
|
|
|
|
file_formatter = UvicornFormatter(use_colors=False)
|
|
|
|
|
file_handler.setFormatter(file_formatter)
|
|
|
|
|
root_logger.addHandler(file_handler)
|
|
|
|
|
|
|
|
|
|
# 记录日志配置信息
|
|
|
|
|
root_logger.info(f"日志文件输出已启用: {log_file_path}")
|
|
|
|
|
root_logger.info(f"日志轮转配置: 单文件最大{max_bytes / 1024 / 1024:.1f}MB, 保留{backup_count}个备份")
|
|
|
|
|
|
|
|
|
|
# 配置第三方库的日志级别
|
|
|
|
|
_configure_third_party_loggers()
|
|
|
|
|
|
|
|
|
|
# 标记为已配置
|
|
|
|
|
_logging_configured = True
|
|
|
|
|
|
|
|
|
|
return root_logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _configure_third_party_loggers():
|
|
|
|
|
"""配置第三方库的日志级别"""
|
|
|
|
|
# SQLAlchemy - 禁用SQL日志
|
|
|
|
|
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
|
|
|
|
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
|
|
|
|
|
logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING)
|
|
|
|
|
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
|
|
|
|
|
|
2026-01-09 17:13:19 +08:00
|
|
|
# aiosqlite - 异步SQLite,禁用DEBUG日志
|
|
|
|
|
logging.getLogger('aiosqlite').setLevel(logging.WARNING)
|
|
|
|
|
|
2025-10-30 11:14:43 +08:00
|
|
|
# Watchfiles - 开发时的文件监控,降低级别
|
|
|
|
|
logging.getLogger('watchfiles').setLevel(logging.WARNING)
|
|
|
|
|
|
2026-01-09 17:13:19 +08:00
|
|
|
# httpx/httpcore - HTTP客户端,禁用DEBUG日志
|
2025-10-30 11:14:43 +08:00
|
|
|
logging.getLogger('httpx').setLevel(logging.WARNING)
|
2026-01-09 17:13:19 +08:00
|
|
|
logging.getLogger('httpcore').setLevel(logging.WARNING)
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
# openai/anthropic - AI客户端库
|
|
|
|
|
logging.getLogger('openai').setLevel(logging.WARNING)
|
|
|
|
|
logging.getLogger('anthropic').setLevel(logging.WARNING)
|
|
|
|
|
|
2026-03-18 12:35:13 +08:00
|
|
|
# 应用模块 - AI 统计日志需要保留 INFO 级别输出
|
|
|
|
|
logging.getLogger('app.services.ai_service').setLevel(logging.INFO)
|
2025-10-30 11:14:43 +08:00
|
|
|
logging.getLogger('app.api.wizard').setLevel(logging.WARNING)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
|
|
|
"""
|
|
|
|
|
获取指定名称的日志器
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
name: 日志器名称,通常使用 __name__
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
配置好的日志器实例
|
|
|
|
|
"""
|
|
|
|
|
return logging.getLogger(name)
|