初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,235 @@
|
||||
"""loguru 日志配置(后端日志系统的唯一出口)。
|
||||
|
||||
提供全局日志配置、标准 logging 接管、上下文管理和便捷的日志记录函数。
|
||||
所有日志(含 uvicorn / SQLAlchemy 等第三方库)统一汇入 loguru,
|
||||
文件统一存放在项目根目录的 logs 文件夹下。
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from contextvars import ContextVar
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# ==================== 上下文变量 ====================
|
||||
# 用于在异步请求中传递日志上下文
|
||||
|
||||
_request_id_var: ContextVar[str | None] = ContextVar("request_id", default=None)
|
||||
"""请求 ID 上下文变量,用于追踪单个请求的完整链路。"""
|
||||
|
||||
_user_id_var: ContextVar[str | None] = ContextVar("user_id", default=None)
|
||||
"""用户 ID 上下文变量,用于记录当前操作用户。"""
|
||||
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
"""标准 ``logging`` → loguru 转发器。
|
||||
|
||||
uvicorn / SQLAlchemy / httpx / langchain 等第三方库一律使用标准 ``logging`` 模块;
|
||||
把本 handler 装到 root logger 后,它们的日志会统一汇入 loguru,与应用自有日志
|
||||
走同一 sink、格式与落盘策略,实现整个后端日志系统的单一出口。
|
||||
|
||||
注:此处 ``import logging`` 与「避免使用 logging 模块」规范并不冲突——那条规范
|
||||
针对的是「用 logging 记业务日志」,而本 handler 的职责恰是把 logging 的输出
|
||||
导入 loguru,是统一日志出口的必要接管机制。
|
||||
"""
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
# 将标准 logging 的级别名映射为 loguru 级别
|
||||
try:
|
||||
level: str | int = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
# 沿调用栈回溯到真正的日志发出处,让 loguru 记录正确的文件名 / 行号
|
||||
frame, depth = logging.currentframe(), 2
|
||||
while frame and frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
"""配置全局日志系统。
|
||||
|
||||
配置内容:
|
||||
1. 创建日志目录
|
||||
2. 移除 loguru 默认处理器
|
||||
3. 添加控制台输出(彩色,级别跟随 debug 开关与 LOG_LEVEL)
|
||||
4. 添加主日志文件(按大小轮转、压缩)
|
||||
5. 添加错误日志单独文件(保留更久)
|
||||
6. 添加访问日志(可选)
|
||||
7. 接管标准 logging,使 uvicorn / SQLAlchemy 等第三方日志汇入 loguru
|
||||
|
||||
日志文件位置:
|
||||
- {project_root}/logs/app.log # 主日志
|
||||
- {project_root}/logs/errors.log # 错误日志
|
||||
- {project_root}/logs/access.log # 访问日志(可选)
|
||||
"""
|
||||
# 获取并创建日志目录
|
||||
log_dir: Path = settings.resolved_log_dir
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 移除 loguru 默认的 stderr 处理器
|
||||
logger.remove()
|
||||
|
||||
# 判断是否为开发环境。开发 reload 进程不使用异步队列,避免退出时残留 multiprocessing semaphore。
|
||||
is_dev = settings.is_development
|
||||
enqueue_logs = not is_dev
|
||||
|
||||
# 控制台输出格式(带颜色,便于阅读)
|
||||
console_format = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan>"
|
||||
"{extra} - <level>{message}</level>\n{exception}"
|
||||
)
|
||||
|
||||
# 文件输出格式(纯文本,便于解析)
|
||||
file_format = (
|
||||
"{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | "
|
||||
"{name}:{function}:{line}{extra} | {message}\n{exception}"
|
||||
)
|
||||
|
||||
# ----- 控制台输出 -----
|
||||
# 控制台级别跟随 debug 开关与 LOG_LEVEL:debug=true → DEBUG,否则用 LOG_LEVEL。
|
||||
console_level = "DEBUG" if settings.debug else settings.log_level
|
||||
logger.add(
|
||||
sys.stderr, # 输出到标准错误
|
||||
level=console_level, # 日志级别
|
||||
format=console_format, # 彩色格式
|
||||
colorize=is_dev, # 开发环境启用颜色
|
||||
enqueue=enqueue_logs, # 生产环境异步写入,提高性能
|
||||
backtrace=is_dev, # 开发环境显示完整回溯
|
||||
diagnose=is_dev, # 开发环境显示诊断信息
|
||||
)
|
||||
|
||||
# ----- 主日志文件 -----
|
||||
# 记录所有级别日志,按大小轮转,自动压缩
|
||||
logger.add(
|
||||
log_dir / "app.log", # 日志文件路径
|
||||
level=settings.log_level, # 日志级别
|
||||
format=file_format, # 纯文本格式
|
||||
rotation=settings.log_rotation_size, # 轮转大小:10 MB
|
||||
retention=f"{settings.log_retention_days} days", # 保留天数:14天
|
||||
compression="gz", # 压缩旧日志
|
||||
encoding="utf-8", # 编码
|
||||
enqueue=enqueue_logs, # 生产环境异步写入
|
||||
backtrace=False, # 不保存完整回溯
|
||||
diagnose=False, # 不保存诊断信息
|
||||
)
|
||||
|
||||
# ----- 错误日志单独文件 -----
|
||||
# 只记录 ERROR 及以上级别,保留更久,便于故障排查
|
||||
logger.add(
|
||||
log_dir / "errors.log", # 错误日志文件路径
|
||||
level="ERROR", # 只记录 ERROR 及以上
|
||||
format=file_format, # 纯文本格式
|
||||
rotation="50 MB", # 轮转大小:50 MB
|
||||
retention="30 days", # 保留天数:30天
|
||||
compression="gz", # 压缩旧日志
|
||||
encoding="utf-8", # 编码
|
||||
enqueue=enqueue_logs, # 生产环境异步写入
|
||||
backtrace=True, # 错误时保留完整堆栈
|
||||
diagnose=True, # 错误时保留诊断信息
|
||||
)
|
||||
|
||||
# ----- 访问日志(可选) -----
|
||||
# 通过 filter 只记录标记为 access 类型的日志
|
||||
if settings.log_access:
|
||||
logger.add(
|
||||
log_dir / "access.log", # 访问日志文件路径
|
||||
level="INFO", # INFO 及以上
|
||||
format=file_format, # 纯文本格式
|
||||
rotation="100 MB", # 轮转大小:100 MB
|
||||
retention="7 days", # 保留天数:7天
|
||||
compression="gz", # 压缩旧日志
|
||||
encoding="utf-8", # 编码
|
||||
enqueue=enqueue_logs, # 生产环境异步写入
|
||||
filter=lambda r: r["extra"].get("type") == "access", # 只记录访问日志
|
||||
)
|
||||
|
||||
# ----- 接管标准 logging -----
|
||||
# force=True 清掉第三方库自带的 handler(含 uvicorn 默认 dictConfig);
|
||||
# level=0 让记录全量进入,最终级别过滤交由上方 loguru sink 完成。
|
||||
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
||||
|
||||
# 应用已有自研访问日志中间件(trace_middleware + log_access,含 trace_id /
|
||||
# method / path / status / duration 等结构化字段),关闭 uvicorn.access 以
|
||||
# 避免访问日志重复落盘。
|
||||
logging.getLogger("uvicorn.access").disabled = True
|
||||
|
||||
|
||||
# ==================== 日志上下文管理 ====================
|
||||
|
||||
|
||||
def set_request_id(request_id: str) -> None:
|
||||
"""设置当前请求的 ID。
|
||||
|
||||
Args:
|
||||
request_id: 请求唯一标识,用于链路追踪
|
||||
"""
|
||||
_request_id_var.set(request_id)
|
||||
|
||||
|
||||
def set_user_id(user_id: str) -> None:
|
||||
"""设置当前操作用户的 ID。
|
||||
|
||||
Args:
|
||||
user_id: 用户唯一标识
|
||||
"""
|
||||
_user_id_var.set(user_id)
|
||||
|
||||
|
||||
def clear_log_context() -> None:
|
||||
"""清除当前日志上下文。
|
||||
|
||||
在请求结束后调用,避免上下文污染。
|
||||
"""
|
||||
_request_id_var.set(None)
|
||||
_user_id_var.set(None)
|
||||
|
||||
|
||||
def get_log_context() -> dict[str, Any]:
|
||||
"""获取当前日志上下文。
|
||||
|
||||
Returns:
|
||||
包含 request_id 和 user_id 的字典(如果已设置)
|
||||
"""
|
||||
context = {}
|
||||
if rid := _request_id_var.get():
|
||||
context["request_id"] = rid
|
||||
if uid := _user_id_var.get():
|
||||
context["user_id"] = uid
|
||||
return context
|
||||
|
||||
|
||||
def log_with_context(message: str, level: str = "INFO", **kwargs: Any) -> None:
|
||||
"""带上下文记录日志。
|
||||
|
||||
自动附加当前请求的 request_id 和 user_id。
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别(DEBUG/INFO/WARNING/ERROR)
|
||||
**kwargs: 额外的上下文字段
|
||||
"""
|
||||
context = get_log_context()
|
||||
context.update(kwargs)
|
||||
logger.bind(**context).log(level, message)
|
||||
|
||||
|
||||
def log_access(message: str, **kwargs: Any) -> None:
|
||||
"""记录访问日志。
|
||||
|
||||
自动添加 type="access" 标记,会被 access.log 过滤器捕获。
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
**kwargs: 额外的上下文字段(如 method, path, status, duration 等)
|
||||
"""
|
||||
logger.bind(type="access", **kwargs).info(message)
|
||||
Reference in new issue
Block a user