"""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 __all__ = ["StructuredLoggerAdapter"] class StructuredLoggerAdapter(LoggerPort): """结构化日志被驱动适配器。 复用 loguru ``logger``,通过 ``logger.bind(trace_id=...)`` 注入链路追踪 上下文。敏感字段脱敏委托 ``MaskingPort.maskPartial``,递归处理 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]: """脱敏 kwargs 中的敏感字段(委托 MaskingPort.maskPartial)。 递归处理嵌套 dict / list,强敏感字段(password / api_key / webhook_url 等)全量遮蔽为 ``"***"``,普通敏感字段(token / app_secret 等)保留前 4 位 + ``"***"``。 @pre - kwargs 为 dict[str, Any] @post - 返回脱敏后的新 dict(不修改原对象) @failure - 无(脱敏为纯内存计算,故障由调用方兜底) @consistency - 无状态(stateless):纯委托,不修改任何持久化状态 """ return self._masking.maskPartial(kwargs) 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")