本次提交包含多项代码优化与规范修正: 1. 文档与注释优化:修正注释术语、补充注解与FR编号 2. 代码格式调整:统一空格、换行与缩进规范 3. 类型与接口完善:补充__all__导出、修正返回类型注解 4. 错误处理增强:新增领域错误类与校验逻辑 5. 依赖与导入调整:修复路径引用、统一时区导入 6. 协议与契约更新:完善接口文档与一致性注解
185 lines
6.1 KiB
Python
185 lines
6.1 KiB
Python
"""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")
|