2026-07-02 03:22:12 +08:00
|
|
|
|
"""StructuredLoggerAdapter:实现 LoggerPort,复用 loguru logger。
|
|
|
|
|
|
|
|
|
|
|
|
- 复用 yuxi.utils.logging_config.logger
|
|
|
|
|
|
- 日志必须携带 trace_id
|
|
|
|
|
|
- 敏感字段脱敏委托 MaskingPort(前 4 位 + ***,强敏感字段全量 ***)
|
|
|
|
|
|
- 日志故障不得阻断主流程,但通过 stderr 兜底输出,
|
|
|
|
|
|
避免日志系统故障完全无声
|
|
|
|
|
|
|
|
|
|
|
|
依赖边界:只依赖 yuxi.channels.contract(端口 + DTO)、
|
|
|
|
|
|
yuxi.utils.logging_config(logger)。脱敏字段表与算法不在此处维护,
|
|
|
|
|
|
统一由 ``MaskingPort`` 委托 ``yuxi.utils.crypto`` 维护(单一事实源)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from loguru import logger as loguru_logger
|
|
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.contract.dtos.logger import LogLevel
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.masking_port import MaskingPort
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
__all__ = ["StructuredLoggerAdapter"]
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
class StructuredLoggerAdapter(LoggerPort):
|
|
|
|
|
|
"""结构化日志被驱动适配器。
|
|
|
|
|
|
|
|
|
|
|
|
复用 loguru ``logger``,通过 ``logger.bind(trace_id=...)`` 注入链路追踪
|
2026-07-03 19:18:13 +08:00
|
|
|
|
上下文。敏感字段脱敏委托 ``MaskingPort.maskPartial``,递归处理
|
2026-07-02 03:22:12 +08:00
|
|
|
|
dict / list 嵌套(修复原自实现不递归导致 config={...} 整体泄露的漏洞)。
|
|
|
|
|
|
所有方法为 ``async def``,故障时捕获异常并通过 ``stderr`` 兜底输出,
|
|
|
|
|
|
不阻断主流程。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, masking_port: MaskingPort) -> None:
|
|
|
|
|
|
"""初始化结构化日志适配器。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
masking_port: 脱敏端口,用于日志 kwargs 的敏感字段脱敏。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._masking = masking_port
|
|
|
|
|
|
|
|
|
|
|
|
def _mask_sensitive_fields(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
2026-07-03 19:18:13 +08:00
|
|
|
|
"""脱敏 kwargs 中的敏感字段(委托 MaskingPort.maskPartial)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
递归处理嵌套 dict / list,强敏感字段(password / api_key /
|
|
|
|
|
|
webhook_url 等)全量遮蔽为 ``"***"``,普通敏感字段(token /
|
|
|
|
|
|
app_secret 等)保留前 4 位 + ``"***"``。
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
@pre
|
|
|
|
|
|
- kwargs 为 dict[str, Any]
|
|
|
|
|
|
|
|
|
|
|
|
@post
|
|
|
|
|
|
- 返回脱敏后的新 dict(不修改原对象)
|
|
|
|
|
|
|
|
|
|
|
|
@failure
|
|
|
|
|
|
- 无(脱敏为纯内存计算,故障由调用方兜底)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
@consistency
|
|
|
|
|
|
- 无状态(stateless):纯委托,不修改任何持久化状态
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
2026-07-03 19:18:13 +08:00
|
|
|
|
return self._masking.maskPartial(kwargs)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
def _emit(
|
|
|
|
|
|
self,
|
|
|
|
|
|
level: str,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None,
|
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""实际执行 loguru 日志写入,失败时 stderr 兜底。
|
|
|
|
|
|
|
|
|
|
|
|
集中处理 loguru 调用与异常兜底,避免每个日志方法重复 try/except。
|
|
|
|
|
|
loguru 故障时通过 ``sys.stderr.write`` 输出一行精简信息,确保日志
|
|
|
|
|
|
系统故障可被运维感知。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
masked = self._mask_sensitive_fields(kwargs)
|
|
|
|
|
|
bound = loguru_logger.bind(trace_id=trace_id) if trace_id else loguru_logger
|
|
|
|
|
|
bound.log(level, message, **masked)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
sys.stderr.write(f"[logger-failed] level={level} trace_id={trace_id} message={message} error={exc}\n")
|
|
|
|
|
|
|
|
|
|
|
|
async def log(
|
|
|
|
|
|
self,
|
|
|
|
|
|
level: LogLevel,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""按指定级别记录日志,携带 trace_id 与脱敏后的上下文。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
level: 日志级别。
|
|
|
|
|
|
message: 日志消息。
|
|
|
|
|
|
trace_id: 链路追踪 ID(可选)。
|
|
|
|
|
|
**kwargs: 附加上下文(敏感字段自动脱敏)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._emit(level.value.upper(), message, trace_id=trace_id, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
async def debug(
|
|
|
|
|
|
self,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""记录 DEBUG 级别日志。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
message: 日志消息。
|
|
|
|
|
|
trace_id: 链路追踪 ID(可选)。
|
|
|
|
|
|
**kwargs: 附加上下文(敏感字段自动脱敏)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._emit("DEBUG", message, trace_id=trace_id, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
async def info(
|
|
|
|
|
|
self,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""记录 INFO 级别日志。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
message: 日志消息。
|
|
|
|
|
|
trace_id: 链路追踪 ID(可选)。
|
|
|
|
|
|
**kwargs: 附加上下文(敏感字段自动脱敏)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._emit("INFO", message, trace_id=trace_id, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
async def warn(
|
|
|
|
|
|
self,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""记录 WARN 级别日志。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
message: 日志消息。
|
|
|
|
|
|
trace_id: 链路追踪 ID(可选)。
|
|
|
|
|
|
**kwargs: 附加上下文(敏感字段自动脱敏)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._emit("WARNING", message, trace_id=trace_id, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
async def error(
|
|
|
|
|
|
self,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""记录 ERROR 级别日志。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
message: 日志消息。
|
|
|
|
|
|
trace_id: 链路追踪 ID(可选)。
|
|
|
|
|
|
**kwargs: 附加上下文(敏感字段自动脱敏)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._emit("ERROR", message, trace_id=trace_id, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
async def exception(
|
|
|
|
|
|
self,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
|
|
|
|
|
exc_info: Exception | None = None,
|
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""记录 ERROR 级别日志,附带完整堆栈。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
masked = self._mask_sensitive_fields(kwargs)
|
|
|
|
|
|
bound = loguru_logger.bind(trace_id=trace_id) if trace_id else loguru_logger
|
|
|
|
|
|
bound.opt(exception=exc_info).log("ERROR", message, **masked)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
sys.stderr.write(f"[logger-failed] level=ERROR trace_id={trace_id} message={message} error={exc}\n")
|