ForcePilot/backend/package/yuxi/channels/adapters/structured_logger_adapter.py

185 lines
6.1 KiB
Python
Raw Normal View History

"""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
__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")