"""
增强的日志系统模块
本模块提供企业级日志功能,解决 MCP 服务器环境中的日志问题,包括重复输出、级别错误、
安全风险和性能问题。
核心功能
--------
1. **单例日志管理器**: 防止 logger 重复初始化导致的日志重复输出
2. **多流输出策略**: 基于日志级别智能路由到不同输出流(全部输出到 stderr)
3. **日志脱敏处理**: 自动检测和脱敏密码、API 密钥等敏感信息
4. **注入攻击防护**: 转义危险字符,防止日志分割和注入攻击
5. **日志去重机制**: 在时间窗口内去除重复日志,减少噪声
6. **智能级别映射**: 根据消息内容自动调整日志级别
主要组件
--------
- SingletonLogManager: 单例日志管理器,确保每个 logger 只初始化一次
- LevelBasedStreamHandler: 基于级别的多流输出处理器(所有输出到 stderr)
- LogSanitizer: 日志脱敏处理器,保护敏感信息
- SecureLogFormatter: 安全日志格式化器,集成脱敏功能
- AntiInjectionFilter: 防注入过滤器,转义危险字符
- LogDeduplicator: 日志去重器,在时间窗口内去除重复日志
- EnhancedLogger: 增强日志记录器,集成所有功能的高级接口
设计原则
--------
1. **MCP 友好**: 所有日志输出到 stderr,避免污染 MCP stdio 通信通道
2. **线程安全**: 使用锁保护共享状态,支持多线程环境
3. **性能优化**: 去重缓存自动清理,限制最大缓存大小
4. **安全第一**: 默认启用脱敏和注入防护
5. **易用性**: 提供与标准 logging 兼容的 API
使用场景
--------
- MCP 服务器日志记录(避免污染 stdio)
- 多线程环境的安全日志
- 需要脱敏的敏感信息日志
- 高频日志场景(需要去重)
- 防止日志注入攻击的安全日志
典型用法
--------
创建增强日志记录器并使用标准 API。
脱敏规则
--------
自动脱敏以下类型的敏感信息:
- 密码字段(password、passwd)
- 密钥字段(secret_key、private_key)
- OpenAI API key(sk-xxx)
- Slack Bot Token(xoxb-xxx)
- GitHub Personal Access Token(ghp_xxx)
去重规则
--------
- 时间窗口: 5 秒(可配置)
- 缓存大小: 1000 条(可配置)
- 重复计数: 自动附加 "(重复 N 次)" 信息
线程安全
--------
- SingletonLogManager 使用双重检查锁实现线程安全的单例
- LogDeduplicator 使用线程锁保护缓存操作
- 所有共享状态都使用 threading.Lock 保护
注意事项
--------
- 日志级别映射基于消息内容关键词匹配(可扩展)
- 脱敏处理使用正则表达式,可能影响性能(已优化)
- 去重缓存会占用内存,自动清理旧条目
- 所有日志输出到 stderr,确保 stdout 用于 MCP 通信
依赖
----
- logging: Python 标准库日志模块
- threading: 线程安全保护
- re: 正则表达式(用于脱敏和注入防护)
- 【性能优化】使用内置 hash() 代替 hashlib.md5,无需额外依赖
"""
import json # noqa: F401
import logging
import os # noqa: F401
import re
import sys
import threading
import time
from typing import Any, Dict, Optional, Set, Tuple # noqa: F401
class SingletonLogManager:
"""
单例日志管理器
功能概述
--------
确保每个 logger 只被初始化一次,防止重复注册 handler 导致的日志重复输出问题。
使用双重检查锁实现线程安全的单例模式。
核心特性
--------
1. **单例模式**: 全局唯一实例,防止多次初始化
2. **Logger 去重**: 跟踪已初始化的 logger 名称
3. **自动配置**: 为每个 logger 自动配置多流输出和安全过滤器
4. **线程安全**: 使用锁保护初始化过程
内部状态
--------
- _instance: 单例实例(类变量)
- _lock: 线程锁(类变量)
- _initialized_loggers: 已初始化的 logger 名称集合(类变量)
单例实现
----------
使用双重检查锁(Double-Checked Locking):
- 第一次检查: 快速路径,避免不必要的锁竞争
- 加锁: 确保只有一个线程创建实例
- 第二次检查: 防止多个线程同时通过第一次检查
使用场景
--------
- 创建应用级日志记录器
- 确保日志不重复输出
- 多模块共享日志配置
注意事项
--------
- 一旦 logger 初始化,无法更改其配置
- 所有 logger 共享相同的 handler 配置
- 默认日志级别为 WARNING
"""
_instance = None
_lock = threading.Lock()
_initialized_loggers: Set[str] = set()
def __new__(cls):
"""
创建或返回单例实例(双重检查锁)
返回
----
SingletonLogManager
全局唯一的管理器实例
线程安全
--------
使用双重检查锁确保多线程环境下只创建一个实例
"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def setup_logger(self, name: str, level=logging.WARNING):
"""
设置并返回已配置的 logger 实例
参数
----
name : str
logger 名称(通常使用 __name__)
level : int, optional
日志级别,默认 logging.WARNING
返回
----
logging.Logger
配置好的 logger 实例
功能
----
1. 检查 logger 是否已初始化(快速路径)
2. 如果未初始化,加锁并配置:
- 清除现有 handler(避免重复)
- 创建并附加 LevelBasedStreamHandler
- 设置日志级别
- 禁用传播到父 logger(防止重复)
- 标记为已初始化
3. 返回 logger 实例
线程安全
--------
使用 _lock 保护初始化过程,确保每个 logger 只被配置一次。
始终在锁内检查并返回,避免快速路径的竞态条件。
注意事项
--------
- 重复调用返回相同的 logger 实例(已配置)
- handler 配置无法更改(一次性初始化)
- propagate=False 防止日志向上传播到 root logger
- 移除快速路径,所有访问都在锁内进行,确保线程安全
"""
# 始终加锁检查,避免快速路径的竞态条件
# 原逻辑的快速路径可能导致返回未完全初始化的 logger
with self._lock:
if name not in self._initialized_loggers:
logger = logging.getLogger(name)
# 清除现有处理器
logger.handlers.clear()
# 使用多流输出策略
stream_handler = LevelBasedStreamHandler()
stream_handler.attach_to_logger(logger)
logger.setLevel(level)
logger.propagate = False # 防止向父logger传播
self._initialized_loggers.add(name)
return logging.getLogger(name)
class LevelBasedStreamHandler:
"""
基于日志级别的多流输出处理器
功能概述
--------
创建两个 StreamHandler,根据日志级别将日志路由到不同的 handler:
- DEBUG/INFO: 通过第一个 handler
- WARNING/ERROR/CRITICAL: 通过第二个 handler
所有 handler 都输出到 stderr,避免污染 MCP 的 stdio 通信通道。
设计原因
--------
在 MCP 环境中,stdout 用于协议通信,必须保持纯净。所有日志输出
(包括 INFO 级别)都应该输出到 stderr。
内部结构
----------
- stdout_handler: 处理 DEBUG 和 INFO 级别(实际输出到 stderr)
- stderr_handler: 处理 WARNING、ERROR、CRITICAL 级别(输出到 stderr)
- formatter: SecureLogFormatter(集成脱敏功能)
- anti_injection_filter: AntiInjectionFilter(防注入防护)
日志格式
--------
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
安全特性
--------
- 所有 handler 都添加了 AntiInjectionFilter(防注入攻击)
- 使用 SecureLogFormatter(自动脱敏)
使用场景
--------
- 被 SingletonLogManager 自动创建和配置
- 不应该手动创建或配置
注意事项
--------
- 虽然名为 stdout_handler,但实际输出到 stderr
- 两个 handler 的日志会按时间顺序交织输出
- 所有输出流都是 sys.stderr
"""
def __init__(self):
"""
初始化多流输出处理器
初始化流程
----------
1. 创建两个 StreamHandler(都输出到 stderr)
2. 设置日志级别和过滤器
3. 配置 SecureLogFormatter(脱敏功能)
4. 添加 AntiInjectionFilter(防注入功能)
"""
self.stdout_handler = logging.StreamHandler(sys.stderr)
self.stdout_handler.setLevel(logging.DEBUG)
self.stdout_handler.addFilter(self._stdout_filter)
# WARNING和ERROR使用stderr
self.stderr_handler = logging.StreamHandler(sys.stderr)
self.stderr_handler.setLevel(logging.WARNING)
# 设置安全格式化器
formatter = SecureLogFormatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
self.stdout_handler.setFormatter(formatter)
self.stderr_handler.setFormatter(formatter)
# 添加注入防护过滤器
anti_injection_filter = AntiInjectionFilter()
self.stdout_handler.addFilter(anti_injection_filter)
self.stderr_handler.addFilter(anti_injection_filter)
def _stdout_filter(self, record):
"""
过滤器:只允许 DEBUG 和 INFO 级别通过
参数
----
record : logging.LogRecord
日志记录对象
返回
----
bool
True: 允许通过(DEBUG 或 INFO)
False: 拒绝通过(WARNING 或更高级别)
功能
----
确保此 handler 只处理 DEBUG 和 INFO 级别的日志,
WARNING 及以上级别由 stderr_handler 处理。
"""
return record.levelno <= logging.INFO
def attach_to_logger(self, logger):
"""
将两个处理器附加到指定 logger
参数
----
logger : logging.Logger
要配置的 logger 实例
功能
----
将 stdout_handler 和 stderr_handler 同时添加到 logger,
实现基于级别的多流输出。
注意事项
--------
- 两个 handler 会同时处理所有日志
- 过滤器确保每条日志只被一个 handler 输出
"""
logger.addHandler(self.stdout_handler)
logger.addHandler(self.stderr_handler)
class LogSanitizer:
"""
日志脱敏处理器
功能概述
--------
自动检测并脱敏日志中的敏感信息,只处理真正的密码、密钥和 API Token,
避免过度脱敏导致日志可读性下降。
支持的敏感信息类型
------------------
1. **密码字段**: password, passwd(至少 6 字符)
2. **密钥字段**: secret_key, private_key(至少 16 字符)
3. **知名 API Token**:
- OpenAI API key (sk-xxx,至少 32 字符)
- Slack Bot Token (xoxb-xxx,至少 50 字符)
- GitHub Personal Access Token (ghp_xxx,36 字符)
脱敏规则
--------
- 匹配的敏感信息替换为 "***REDACTED***"
- 使用长度限制减少误判(如 password 至少 6 字符)
- 使用边界匹配(\b)确保精确匹配 Token 格式
实现方式
--------
使用预编译的正则表达式列表,在初始化时编译所有模式,提高性能。
设计原则
--------
- **精确匹配**: 避免过度脱敏(如 "timeout" 不会被误判为 "password")
- **性能优化**: 编译正则表达式,避免重复编译
- **可扩展**: 可通过添加新模式支持更多敏感信息类型
使用场景
--------
- 被 SecureLogFormatter 自动调用
- 也可独立使用:sanitizer = LogSanitizer(); sanitizer.sanitize(message)
注意事项
--------
- 脱敏是不可逆的
- 可能无法检测复杂编码或混淆的敏感信息
- 正则匹配可能影响高频日志性能
"""
def __init__(self):
"""
初始化脱敏处理器
初始化流程
----------
预编译所有正则表达式模式,提高后续匹配性能。
编译的模式
----------
- 密码字段(至少 6 字符)
- 密钥字段(至少 16 字符)
- OpenAI、Slack、GitHub 等知名服务的 API Token
性能优化
--------
使用 re.compile() 预编译正则表达式,避免每次匹配都重新编译。
"""
# 只保护真正的密码和密钥,避免过度脱敏
self.sensitive_patterns = [
# 明确的密码字段
re.compile(r'password["\']?\s*[:=]\s*["\']?[^\s"\']{6,}["\']?'),
re.compile(r'passwd["\']?\s*[:=]\s*["\']?[^\s"\']{6,}["\']?'),
# 明确的密钥字段
re.compile(
r'secret[_-]?key["\']?\s*[:=]\s*["\']?[A-Za-z0-9._-]{16,}["\']?'
),
re.compile(
r'private[_-]?key["\']?\s*[:=]\s*["\']?[A-Za-z0-9._-]{16,}["\']?'
),
# 知名API密钥格式(精确匹配)
re.compile(r"\bsk-[A-Za-z0-9]{32,}\b"), # OpenAI API key
re.compile(r"\bxoxb-[A-Za-z0-9-]{50,}\b"), # Slack Bot Token
re.compile(r"\bghp_[A-Za-z0-9]{36}\b"), # GitHub Personal Access Token
]
def sanitize(self, message: str) -> str:
"""
脱敏处理日志消息
参数
----
message : str
原始日志消息
返回
----
str
脱敏后的日志消息
处理流程
--------
1. 遍历所有预编译的正则模式
2. 对每个模式执行替换
3. 返回脱敏后的消息
替换策略
--------
所有匹配的敏感信息替换为 "***REDACTED***"
性能
----
- 时间复杂度: O(n * m),n 为消息长度,m 为模式数量
- 使用预编译正则表达式,减少编译开销
注意事项
--------
- 不会修改原始消息(返回新字符串)
- 脱敏是不可逆的
- 可能存在误判或漏判
"""
for pattern in self.sensitive_patterns:
message = pattern.sub("***REDACTED***", message)
return message
class SecureLogFormatter(logging.Formatter):
"""
安全的日志格式化器
功能概述
--------
继承自标准 logging.Formatter,在格式化后自动脱敏敏感信息。
工作流程
--------
1. 使用父类的 format() 方法进行标准格式化
2. 调用 LogSanitizer 脱敏敏感信息
3. 返回脱敏后的日志字符串
使用场景
--------
- 被 LevelBasedStreamHandler 自动使用
- 可用于任何需要脱敏的 logging.Handler
优势
----
- 无缝集成到标准 logging 系统
- 对用户代码透明(自动脱敏)
- 不影响日志的其他功能(级别、时间戳、模块名等)
注意事项
--------
- 脱敏发生在格式化之后,不影响日志记录的原始数据
- 脱敏会略微影响性能(正则匹配)
"""
def __init__(self, *args, **kwargs):
"""
初始化安全日志格式化器
参数
----
*args, **kwargs
传递给父类 logging.Formatter 的参数
初始化流程
----------
1. 调用父类构造函数
2. 创建 LogSanitizer 实例
"""
super().__init__(*args, **kwargs)
self.sanitizer = LogSanitizer()
def format(self, record):
"""
格式化并脱敏日志记录
参数
----
record : logging.LogRecord
日志记录对象
返回
----
str
格式化并脱敏后的日志字符串
处理流程
--------
1. 调用父类的 format() 进行标准格式化
2. 调用 sanitizer.sanitize() 脱敏
3. 返回脱敏后的字符串
性能
----
- 增加的开销主要来自脱敏正则匹配
- 对于不含敏感信息的日志,影响较小
"""
# 先进行标准格式化
formatted = super().format(record)
# 然后进行脱敏处理
return self.sanitizer.sanitize(formatted)
class AntiInjectionFilter(logging.Filter):
"""
防止日志注入攻击的过滤器
功能概述
--------
转义日志消息中的危险字符,防止日志分割攻击和日志伪造攻击。
攻击场景
--------
攻击者可能通过在输入中插入换行符或控制字符,伪造日志条目或分割日志:
- 插入 "\\n" 可以伪造多行日志
- 插入 "\\x00"(空字节)可能导致日志处理器异常
- 插入 "\\r" 可能覆盖同一行的日志
防护策略
--------
- 转义空字节(\\x00)为 "\\\\x00"
- 转义换行符(\\n)为 "\\\\n"
- 转义回车符(\\r)为 "\\\\r"
- 不转义 HTML 字符(保持可读性)
处理范围
--------
- record.msg: 日志消息模板
- record.args: 日志消息参数(仅字符串类型)
设计原则
--------
- **安全优先**: 转义所有潜在危险字符
- **最小影响**: 只转义必要的字符,保持可读性
- **不阻止日志**: 始终返回 True,允许日志记录
使用场景
--------
- 被 LevelBasedStreamHandler 自动添加
- 适用于所有 logging.Handler
注意事项
--------
- 转义后日志可读性略有下降("\\n" 显示为 "\\\\n")
- 不处理非字符串类型的参数
- 不修改日志记录的其他属性
"""
def filter(self, record):
"""
过滤并转义日志记录中的危险字符
参数
----
record : logging.LogRecord
日志记录对象
返回
----
bool
始终返回 True(允许日志通过)
处理流程
--------
1. 转义 record.msg 中的危险字符(如果是字符串)
2. 转义 record.args 中所有字符串参数的危险字符
3. 返回 True
转义规则
--------
- \\x00 → \\\\x00(空字节)
- \\n → \\\\n(换行符)
- \\r → \\\\r(回车符)
副作用
------
直接修改 record.msg 和 record.args,不会创建新对象。
性能
----
- 只处理字符串类型
- 只转义 3 种字符,性能影响很小
线程安全
--------
每个 LogRecord 只被一个线程处理,无需加锁。
注意事项(修复)
--------------
record.msg和record.args都需要转义危险字符,确保一致性
"""
# 转义record.msg中的危险字符(换行符、回车符、空字节)
if hasattr(record, "msg") and isinstance(record.msg, str):
record.msg = (
record.msg.replace("\x00", "\\x00") # 空字节
.replace("\n", "\\n") # 换行符
.replace("\r", "\\r") # 回车符
)
# 转义 record.args 中的危险字符
if hasattr(record, "args"):
escaped_args = []
for arg in record.args:
if isinstance(arg, str):
# 转义换行符、回车符和空字节,保持可读性
escaped_arg = (
arg.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\x00", "\\x00")
)
escaped_args.append(escaped_arg)
else:
escaped_args.append(arg)
record.args = tuple(escaped_args)
return True
class LogDeduplicator:
"""
日志去重器
功能概述
--------
在指定时间窗口内去除重复日志,减少日志噪声,提高日志可读性。
去重策略
--------
- **时间窗口**: 默认 5 秒,在此期间的重复日志被抑制
- **哈希匹配**: 【性能优化】使用 Python 内置 hash() 函数判断消息是否重复(比 MD5 快 5-10 倍)
- **计数累加**: 重复日志的计数会累加,可附加到最终日志
缓存管理
----------
- **过期清理**: 超出时间窗口的缓存条目自动删除
- **大小限制**: 缓存超过 max_cache_size 时,删除最旧的 25% 条目
- **线程安全**: 使用 threading.Lock 保护缓存操作
内部结构
----------
- cache: {log_hash: (timestamp, count)}
- log_hash: 消息的 MD5 哈希
- timestamp: 最后一次出现的时间戳
- count: 累计重复次数
使用场景
--------
- 高频日志场景(如轮询、心跳)
- 错误重试场景(避免相同错误刷屏)
- EnhancedLogger 自动集成
性能考虑
--------
- MD5 哈希计算成本较低
- 缓存查找为 O(1)
- 定期清理防止内存泄漏
注意事项
--------
- 不同的日志内容(如时间戳)会导致哈希不同
- 去重可能隐藏重要信息(需谨慎配置时间窗口)
- 缓存会占用内存(由 max_cache_size 限制)
"""
def __init__(self, time_window=5.0, max_cache_size=1000):
"""
初始化日志去重器
参数
----
time_window : float, optional
时间窗口(秒),默认 5.0
在此时间内的重复日志将被抑制
max_cache_size : int, optional
最大缓存大小,默认 1000
超过此大小时自动清理最旧的 25% 条目
初始化流程
----------
1. 设置时间窗口和缓存大小
2. 初始化空缓存字典
3. 创建线程锁
内部状态
--------
- time_window: 时间窗口(秒)
- max_cache_size: 最大缓存大小
- cache: 日志哈希 -> (时间戳, 计数) 的映射
- lock: 线程锁
"""
self.time_window = time_window # 时间窗口(秒)
self.max_cache_size = max_cache_size
# 使用内置 hash(message)(int)作为 key
self.cache: Dict[int, Tuple[float, int]] = {} # {msg_hash: (timestamp, count)}
self.lock = threading.Lock()
def should_log(self, message: str) -> Tuple[bool, Optional[str]]:
"""
检查是否应该记录日志
参数
----
message : str
日志消息
返回
----
Tuple[bool, Optional[str]]
- bool: 是否应该记录日志
- Optional[str]: 重复信息(如 "重复 3 次"),无重复则为 None
处理流程
--------
1. 生成消息的 MD5 哈希
2. 检查哈希是否在缓存中:
- 存在且在时间窗口内: 增加计数,不记录,返回 (False, "重复 N 次")
- 存在但超出时间窗口: 重置计数,记录,返回 (True, None)
- 不存在: 添加到缓存,记录,返回 (True, None)
3. 定期清理过期缓存
线程安全
--------
使用 self.lock 保护整个操作,确保缓存一致性。
性能
----
- 时间复杂度: O(1)(哈希查找)
- 【性能优化】使用 Python 内置 hash(),比 MD5 快 5-10 倍
- 清理操作: O(m)(m 为缓存大小)
注意事项
--------
- 相同内容的消息会被去重
- 不同时间戳的消息会被视为不同(需在格式化前去重)
- 使用内置 hash() 而非加密哈希,因为日志去重不需要加密安全性
"""
with self.lock:
current_time = time.time()
# 【性能优化】使用 Python 内置 hash(),比 MD5 快 5-10 倍
# 对于日志去重场景,不需要加密安全性,只需要高效的哈希区分
msg_hash = hash(message)
if msg_hash in self.cache:
last_time, count = self.cache[msg_hash]
if current_time - last_time <= self.time_window:
# 在时间窗口内,增加计数但不记录
self.cache[msg_hash] = (current_time, count + 1)
return False, f"重复 {count + 1} 次"
else:
# 超出时间窗口,重新记录
self.cache[msg_hash] = (current_time, 1)
return True, None
else:
# 新消息,记录
self.cache[msg_hash] = (current_time, 1)
self._cleanup_cache(current_time)
return True, None
def _cleanup_cache(self, current_time: float):
"""
清理过期缓存
参数
----
current_time : float
当前时间戳
清理策略
--------
1. **过期清理**: 删除超出时间窗口的所有条目
2. **大小限制**: 如果缓存超过 max_cache_size,删除最旧的 25% 条目
处理流程
--------
1. 找出所有过期的键(超出时间窗口)
2. 删除过期键
3. 检查缓存大小,如果超过限制:
- 按时间戳排序
- 删除最旧的 25% 条目
性能
----
- 过期清理: O(m)(m 为缓存大小)
- 大小限制: O(m log m)(排序)
线程安全
--------
调用者(should_log)已持有 self.lock,无需再次加锁。
注意事项
--------
- 清理是惰性的(不是定时任务)
- 只在添加新消息时触发
- 删除 25% 的策略是启发式的,可根据需要调整
"""
expired_keys = [
key
for key, (timestamp, _) in self.cache.items()
if current_time - timestamp > self.time_window
]
for key in expired_keys:
del self.cache[key]
# 限制缓存大小
if len(self.cache) > self.max_cache_size:
# 删除最旧的条目
sorted_items = sorted(self.cache.items(), key=lambda x: x[1][0])
for key, _ in sorted_items[: len(sorted_items) // 4]:
del self.cache[key]
class EnhancedLogger:
"""
增强的日志记录器
功能概述
--------
提供企业级日志功能的高级接口,集成所有底层优化:
- 单例管理(防止日志重复)
- 日志去重(减少噪声)
- 自动脱敏(保护敏感信息)
- 注入防护(防止日志攻击)
- 智能级别映射(动态调整日志级别)
核心特性
--------
1. **单例 Logger**: 通过 SingletonLogManager 确保 logger 不重复初始化
2. **自动去重**: 5 秒时间窗口内的重复日志自动抑制
3. **透明脱敏**: 自动检测并脱敏密码、API key 等敏感信息
4. **防注入攻击**: 自动转义换行符和控制字符
5. **智能级别**: 根据消息内容关键词自动调整日志级别
使用场景
--------
- 替代标准 logging.Logger 使用
- 需要高级日志功能的场景
- MCP 服务器日志记录
API 兼容性
----------
提供与标准 logging.Logger 兼容的方法:
- debug(message, *args, **kwargs)
- info(message, *args, **kwargs)
- warning(message, *args, **kwargs)
- error(message, *args, **kwargs)
级别映射规则
------------
根据消息中的关键词自动调整日志级别(可扩展):
- "收到反馈请求", "Web UI 配置加载成功" → DEBUG
- "等待用户反馈", "收到用户反馈" → INFO
- "服务启动失败", "配置加载失败" → ERROR
性能
----
- 去重增加少量开销(MD5 哈希 + 缓存查找)
- 级别映射使用简单字符串匹配(O(m),m 为映射数量)
- 脱敏和注入防护在底层 Handler 处理
注意事项
--------
- 去重可能隐藏重要信息(时间窗口 5 秒)
- 级别映射基于字符串匹配,可能误判
- 不支持动态修改 level_mapping(需重新创建实例)
"""
def __init__(self, name: str):
"""
初始化增强日志记录器
参数
----
name : str
logger 名称(通常使用 __name__)
初始化流程
----------
1. 创建 SingletonLogManager 并获取 logger
2. 创建 LogDeduplicator(5 秒窗口,1000 条缓存)
3. 配置级别映射规则
内部组件
--------
- log_manager: SingletonLogManager 实例
- logger: 配置好的 logging.Logger
- deduplicator: LogDeduplicator 实例
- level_mapping: 消息关键词 -> 日志级别的映射
"""
self.log_manager = SingletonLogManager()
self.logger = self.log_manager.setup_logger(name)
self.deduplicator = LogDeduplicator(
time_window=5.0,
max_cache_size=1000,
)
self.level_mapping = {
"收到反馈请求": logging.DEBUG,
"Web UI 配置加载成功": logging.DEBUG,
"启动反馈界面": logging.DEBUG,
"Web 服务已在运行": logging.DEBUG,
"内容已更新": logging.INFO,
"等待用户反馈": logging.INFO,
"收到用户反馈": logging.INFO,
"服务启动失败": logging.ERROR,
"配置加载失败": logging.ERROR,
}
def _get_effective_level(self, message: str, default_level: int) -> int:
"""
根据消息内容获取有效的日志级别
参数
----
message : str
日志消息
default_level : int
默认日志级别(如果没有匹配的映射)
返回
----
int
有效的日志级别
匹配规则
--------
遍历 level_mapping,如果消息中包含关键词,返回对应级别。
如果没有匹配,返回 default_level。
性能
----
- 时间复杂度: O(m * n)
- m: level_mapping 的大小
- n: 消息长度
- 使用简单字符串匹配(str.contains)
扩展性
------
可通过修改 level_mapping 添加新的映射规则,或在初始化后动态修改。
注意事项
--------
- 匹配是子串匹配,可能存在误判
- 第一个匹配的规则生效(顺序敏感)
"""
for pattern, level in self.level_mapping.items():
if pattern in message:
return level
return default_level
def log(self, level: int, message: str, *args, **kwargs):
"""
记录日志(带去重和级别优化)
参数
----
level : int
日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL)
message : str
日志消息
*args
传递给 logger.log() 的额外参数
**kwargs
传递给 logger.log() 的关键字参数
处理流程
--------
1. 根据消息内容获取有效日志级别(可能与 level 不同)
2. 检查是否应该记录(去重)
3. 如果应该记录:
- 如果有重复信息,附加到消息末尾
- 调用底层 logger.log()
去重机制
--------
在 5 秒时间窗口内,相同消息只记录一次,重复计数附加到消息。
级别映射
--------
即使调用 debug(),如果消息匹配到 ERROR 映射,也会以 ERROR 级别记录。
注意事项
--------
- 去重基于消息内容(不包括参数)
- 级别映射可能覆盖传入的 level
- 底层 handler 会自动脱敏和防注入
"""
effective_level = self._get_effective_level(message, level)
should_log, duplicate_info = self.deduplicator.should_log(message)
if should_log:
if duplicate_info:
message += f" ({duplicate_info})"
self.logger.log(effective_level, message, *args, **kwargs)
def setLevel(self, level: int) -> None:
"""兼容标准 logging.Logger API:设置底层 logger 的级别。"""
self.logger.setLevel(level)
def debug(self, message: str, *args, **kwargs):
"""
记录 DEBUG 级别日志
参数
----
message : str
日志消息
*args
额外参数
**kwargs
关键字参数
功能
----
调用 self.log(logging.DEBUG, message, *args, **kwargs)
注意
----
实际日志级别可能被 level_mapping 覆盖
"""
self.log(logging.DEBUG, message, *args, **kwargs)
def info(self, message: str, *args, **kwargs):
"""
记录 INFO 级别日志
参数
----
message : str
日志消息
*args
额外参数
**kwargs
关键字参数
功能
----
调用 self.log(logging.INFO, message, *args, **kwargs)
注意
----
实际日志级别可能被 level_mapping 覆盖
"""
self.log(logging.INFO, message, *args, **kwargs)
def warning(self, message: str, *args, **kwargs):
"""
记录 WARNING 级别日志
参数
----
message : str
日志消息
*args
额外参数
**kwargs
关键字参数
功能
----
调用 self.log(logging.WARNING, message, *args, **kwargs)
注意
----
实际日志级别可能被 level_mapping 覆盖
"""
self.log(logging.WARNING, message, *args, **kwargs)
def error(self, message: str, *args, **kwargs):
"""
记录 ERROR 级别日志
参数
----
message : str
日志消息
*args
额外参数
**kwargs
关键字参数
功能
----
调用 self.log(logging.ERROR, message, *args, **kwargs)
注意
----
实际日志级别可能被 level_mapping 覆盖
"""
self.log(logging.ERROR, message, *args, **kwargs)
enhanced_logger = EnhancedLogger(__name__)
# ========================================================================
# 日志级别配置工具
# ========================================================================
# 日志级别映射
LOG_LEVEL_MAP = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
}
# 有效的日志级别名称
VALID_LOG_LEVELS = tuple(LOG_LEVEL_MAP.keys())
def get_log_level_from_config() -> int:
"""
从配置文件读取日志级别
返回
----
int
logging 模块的日志级别常量
处理逻辑
--------
1. 尝试从 config_manager 读取 web_ui.log_level 配置
2. 如果配置无效或读取失败,使用默认级别 WARNING
3. 忽略大小写(如 "warning" 等同于 "WARNING")
示例
----
>>> # config.jsonc: {"web_ui": {"log_level": "DEBUG"}}
>>> get_log_level_from_config()
10 # logging.DEBUG
"""
try:
from config_manager import config_manager
web_ui_config = config_manager.get("web_ui", {})
log_level_str = web_ui_config.get("log_level", "WARNING")
# 标准化为大写
log_level_upper = str(log_level_str).upper()
if log_level_upper in LOG_LEVEL_MAP:
return LOG_LEVEL_MAP[log_level_upper]
else:
logging.warning(
f"无效的日志级别 '{log_level_str}',"
f"有效值: {VALID_LOG_LEVELS},使用默认值 WARNING"
)
return logging.WARNING
except Exception as e:
# 配置读取失败时使用默认级别
logging.debug(f"读取日志级别配置失败: {e},使用默认值 WARNING")
return logging.WARNING
def configure_logging_from_config() -> None:
"""
根据配置文件设置全局日志级别
功能
----
1. 从配置读取日志级别
2. 设置 root logger 级别
3. 更新所有已存在的 handler 级别
使用场景
--------
在应用启动时调用,确保日志级别与配置一致
示例
----
>>> configure_logging_from_config()
>>> # 现在所有日志都使用配置中的级别
"""
log_level = get_log_level_from_config()
# 设置 root logger
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# 更新所有 handler
for handler in root_logger.handlers:
handler.setLevel(log_level)
logging.info(f"日志级别已设置为: {logging.getLevelName(log_level)}")