ForcePilot/backend/package/yuxi/channels/adapters/structured_logger_adapter.py
Kris b88c0ae29e feat(channels): 批量新增多渠道网关限界上下文基础代码与契约
新增完整的 channels 限界上下文模块,包含契约层、领域核心层、应用服务、管道编排、插件体系、基础设施组合根等全层级代码,新增飞书与微信 iLink 渠道插件基础结构,补充各类 DTO、端口协议与领域服务实现。
2026-07-02 03:22:12 +08:00

177 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""StructuredLoggerAdapter实现 LoggerPort复用 loguru logger。
- 复用 yuxi.utils.logging_config.logger
- 日志必须携带 trace_id
- 敏感字段脱敏委托 MaskingPort前 4 位 + ***,强敏感字段全量 ***
- 日志故障不得阻断主流程,但通过 stderr 兜底输出,
避免日志系统故障完全无声
依赖边界:只依赖 yuxi.channels.contract端口 + DTO
yuxi.utils.logging_configlogger。脱敏字段表与算法不在此处维护
统一由 ``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
class StructuredLoggerAdapter(LoggerPort):
"""结构化日志被驱动适配器。
复用 loguru ``logger``,通过 ``logger.bind(trace_id=...)`` 注入链路追踪
上下文。敏感字段脱敏委托 ``MaskingPort.mask_partial``,递归处理
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.mask_partial
递归处理嵌套 dict / list强敏感字段password / api_key /
webhook_url 等)全量遮蔽为 ``"***"``普通敏感字段token /
app_secret 等)保留前 4 位 + ``"***"``。
Args:
kwargs: 原始关键字参数。
Returns:
脱敏后的关键字参数(新 dict
"""
return self._masking.mask_partial(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")