mirror of
https://git.oceanpay.cc/danial/kami_apple_exchage.git
synced 2025-12-18 22:29:09 +00:00
- 新增 CODEBUDDY.md、GEMINI.md、GEMINI_CN.md 等项目文档 - 更新 Dockerfile 和其他配置文件 - 优化部分代码结构,如 orders.py、tasks.py 等 - 新增 .dockerignore 文件
342 lines
10 KiB
Python
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",
|
|
]
|