Files
kami_apple_exchage/backend/app/core/log.py
danial 5c486e34d3 docs(项目): 添加项目文档并进行代码调整
- 新增 CODEBUDDY.md、GEMINI.md、GEMINI_CN.md 等项目文档
- 更新 Dockerfile 和其他配置文件
- 优化部分代码结构,如 orders.py、tasks.py 等
- 新增 .dockerignore 文件
2025-09-12 19:38:24 +08:00

342 lines
10 KiB
Python

"""
统一日志模块 - 集成 OpenTelemetry 追踪功能
基于 loguru + structlog 提供现代化日志解决方案
"""
import sys
from pathlib import Path
from typing import Any, Dict, Optional
from contextvars import ContextVar
# 核心日志库
from loguru import logger
import structlog
# OpenTelemetry 集成
from opentelemetry import trace
from app.core.config import get_settings
# 全局配置
settings = get_settings()
# 上下文变量 - 用于跨请求追踪
request_id_var: ContextVar[Optional[str]] = ContextVar("request_id", default=None)
trace_id_var: ContextVar[Optional[str]] = ContextVar("trace_id", default=None)
user_id_var: ContextVar[Optional[str]] = ContextVar("user_id", default=None)
# 全局状态
_logger_initialized = False
class LogContext:
"""日志上下文管理器 - 自动集成 OpenTelemetry 信息"""
def __init__(self, **context: Any) -> None:
self.context = context
self.tokens: Dict[str, Any] = {}
def __enter__(self) -> "LogContext":
"""进入上下文并设置上下文变量"""
# 设置显式上下文
if "request_id" in self.context:
self.tokens["request_id"] = request_id_var.set(self.context["request_id"])
if "trace_id" in self.context:
self.tokens["trace_id"] = trace_id_var.set(self.context["trace_id"])
if "user_id" in self.context:
self.tokens["user_id"] = user_id_var.set(self.context["user_id"])
# 自动获取 OpenTelemetry 信息
current_span = trace.get_current_span()
if current_span.is_recording():
span_context = current_span.get_span_context()
if "otel_trace_id" not in self.context:
self.context["otel_trace_id"] = format(span_context.trace_id, "032x")
if "otel_span_id" not in self.context:
self.context["otel_span_id"] = format(span_context.span_id, "016x")
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""退出上下文并重置变量"""
# 重置上下文变量
for var_name, token in self.tokens.items():
if var_name == "request_id":
request_id_var.reset(token)
elif var_name == "trace_id":
trace_id_var.reset(token)
elif var_name == "user_id":
user_id_var.reset(token)
# 记录异常(如果有)
if exc_type:
get_logger().bind(**self.context).exception(
f"Context exception: {exc_val}",
)
def get_enriched_context(self) -> Dict[str, Any]:
"""获取丰富的上下文信息"""
enriched = self.context.copy()
# 添加上下文变量
if request_id := request_id_var.get():
enriched["request_id"] = request_id
if trace_id := trace_id_var.get():
enriched["trace_id"] = trace_id
if user_id := user_id_var.get():
enriched["user_id"] = user_id
return enriched
def setup_structlog() -> None:
"""配置 structlog 结构化日志"""
processors = [
# 添加上下文变量
structlog.contextvars.merge_contextvars,
# 添加 OpenTelemetry 信息
_add_opentelemetry_context,
# 添加日志级别
structlog.processors.add_log_level,
# 添加时间戳
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
# 堆栈信息
structlog.processors.StackInfoRenderer(),
# 异常信息
structlog.dev.set_exc_info,
]
# 根据环境选择渲染器
if settings.DEBUG:
processors.append(structlog.dev.ConsoleRenderer(colors=True))
else:
processors.append(structlog.processors.JSONRenderer())
structlog.configure(
processors=processors,
wrapper_class=structlog.make_filtering_bound_logger(
getattr(settings, "LOG_LEVEL", "INFO").upper()
),
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
def _add_opentelemetry_context(_, __, event_dict: Dict[str, Any]) -> Dict[str, Any]:
"""添加 OpenTelemetry 上下文信息到日志"""
current_span = trace.get_current_span()
if current_span.is_recording():
span_context = current_span.get_span_context()
event_dict["otel_trace_id"] = format(span_context.trace_id, "032x")
event_dict["otel_span_id"] = format(span_context.span_id, "016x")
return event_dict
def setup_logging() -> None:
"""配置 loguru 日志系统"""
global _logger_initialized
if _logger_initialized:
return
# 移除默认处理器
logger.remove()
# 获取配置
log_level = getattr(settings, "LOG_LEVEL", "INFO").upper()
log_dir = Path(getattr(settings, "LOG_DIR", "logs"))
log_dir.mkdir(parents=True, exist_ok=True)
# 控制台输出
if settings.DEBUG:
# 开发环境 - 彩色输出
logger.add(
sys.stdout,
level=log_level,
format=(
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
"<level>{message}</level>"
),
colorize=True,
backtrace=True,
diagnose=True,
filter=_log_filter,
)
else:
# 生产环境 - JSON 格式
logger.add(
sys.stdout,
level=log_level,
serialize=True, # 使用内置JSON序列化
filter=_log_filter,
)
# 文件输出 - 主日志
logger.add(
log_dir / "app.log",
level=log_level,
serialize=True, # 使用内置JSON序列化
rotation="100 MB",
retention="30 days",
compression="zip",
backtrace=True,
diagnose=True,
filter=_log_filter,
)
# 文件输出 - 错误日志
logger.add(
log_dir / "error.log",
level="ERROR",
serialize=True, # 使用内置JSON序列化
rotation="50 MB",
retention="60 days",
compression="zip",
backtrace=True,
diagnose=True,
filter=_log_filter,
)
# 配置结构化日志
setup_structlog()
_logger_initialized = True
logger.info(f"日志系统初始化完成 - 级别: {log_level}, 调试模式: {settings.DEBUG}")
def _log_filter(record) -> bool:
"""日志过滤器 - 添加 OpenTelemetry 信息"""
try:
# 确保 extra 字典存在
if "extra" not in record:
record["extra"] = {}
# 添加 OpenTelemetry 追踪信息
current_span = trace.get_current_span()
if current_span.is_recording():
span_context = current_span.get_span_context()
record["extra"]["otel_trace_id"] = format(span_context.trace_id, "032x")
record["extra"]["otel_span_id"] = format(span_context.span_id, "016x")
# 添加上下文变量
if request_id := request_id_var.get():
record["extra"]["request_id"] = request_id
if trace_id := trace_id_var.get():
record["extra"]["trace_id"] = trace_id
if user_id := user_id_var.get():
record["extra"]["user_id"] = user_id
return True
except Exception:
# 如果过滤器失败,仍然允许日志记录
return True
def _json_formatter(record):
"""自定义 JSON 格式化器 - 直接返回格式化字符串"""
import json
from datetime import datetime
# 安全地提取记录信息,避免格式化错误
try:
log_entry = {
"timestamp": record["time"].isoformat(),
"level": record["level"].name,
"logger": record["name"],
"module": record["module"],
"function": record["function"],
"line": record["line"],
"message": record["message"],
}
# 安全地添加额外信息
if record.get("extra"):
log_entry.update(record["extra"])
# 安全地添加异常信息
if record.get("exception"):
log_entry["exception"] = record["exception"]
return json.dumps(log_entry, ensure_ascii=False)
except Exception as e:
# 如果格式化失败,返回基本的错误信息
fallback_entry = {
"timestamp": datetime.now().isoformat(),
"level": "ERROR",
"logger": "log_formatter",
"message": f"Log formatting error: {str(e)}",
"original_message": str(record.get("message", "N/A")),
}
return json.dumps(fallback_entry, ensure_ascii=False)
def get_logger(name: Optional[str] = None):
"""获取日志器实例并自动添加上下文信息"""
if not _logger_initialized:
setup_logging()
# 获取当前上下文
context = {}
if request_id := request_id_var.get():
context["request_id"] = request_id
if trace_id := trace_id_var.get():
context["trace_id"] = trace_id
if user_id := user_id_var.get():
context["user_id"] = user_id
# 绑定名称和上下文
bound_logger = logger.bind(**context)
if name:
bound_logger = bound_logger.bind(logger_name=name)
return bound_logger
def get_structured_logger(name: Optional[str] = None):
"""获取结构化日志器"""
if not _logger_initialized:
setup_logging()
return structlog.get_logger(name or __name__)
def log_business_event(event_type: str, **kwargs):
"""记录业务事件日志"""
get_logger().info(f"Business event: {event_type}", event_type=event_type, **kwargs)
def log_performance(operation: str, duration_ms: float, **kwargs):
"""记录性能日志"""
get_logger().info(
f"Performance: {operation} completed in {duration_ms:.2f}ms",
operation=operation,
duration_ms=duration_ms,
**kwargs,
)
def log_security_event(event: str, severity: str = "INFO", **kwargs):
"""记录安全事件日志"""
log_func = getattr(get_logger(), severity.lower(), get_logger().info)
log_func(
f"Security event: {event}", security_event=event, severity=severity, **kwargs
)
# 导出的公共 API
__all__ = [
"setup_logging",
"get_logger",
"get_structured_logger",
"log_business_event",
"log_performance",
"log_security_event",
"LogContext",
"request_id_var",
"trace_id_var",
"user_id_var",
]