1051 lines
39 KiB
Python
1051 lines
39 KiB
Python
# 使用基于时间戳的文件处理器,简单的轮转份数限制
|
||
|
||
import logging
|
||
import orjson
|
||
import threading
|
||
import time
|
||
import structlog
|
||
import tomlkit
|
||
|
||
from pathlib import Path
|
||
from typing import Callable, Optional
|
||
from datetime import datetime, timedelta
|
||
|
||
# 创建logs目录
|
||
LOG_DIR = Path("logs")
|
||
LOG_DIR.mkdir(exist_ok=True)
|
||
|
||
# 全局handler实例,避免重复创建
|
||
_file_handler = None
|
||
_console_handler = None
|
||
|
||
|
||
def get_file_handler():
|
||
"""获取文件handler单例"""
|
||
global _file_handler
|
||
if _file_handler is None:
|
||
# 确保日志目录存在
|
||
LOG_DIR.mkdir(exist_ok=True)
|
||
|
||
# 检查现有handler,避免重复创建
|
||
root_logger = logging.getLogger()
|
||
for handler in root_logger.handlers:
|
||
if isinstance(handler, TimestampedFileHandler):
|
||
_file_handler = handler
|
||
return _file_handler
|
||
|
||
# 使用基于时间戳的handler,简单的轮转份数限制
|
||
_file_handler = TimestampedFileHandler(
|
||
log_dir=LOG_DIR,
|
||
max_bytes=5 * 1024 * 1024, # 5MB
|
||
backup_count=30,
|
||
encoding="utf-8",
|
||
)
|
||
# 设置文件handler的日志级别
|
||
file_level = LOG_CONFIG.get("file_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||
_file_handler.setLevel(getattr(logging, file_level.upper(), logging.INFO))
|
||
return _file_handler
|
||
|
||
|
||
def get_console_handler():
|
||
"""获取控制台handler单例"""
|
||
global _console_handler
|
||
if _console_handler is None:
|
||
_console_handler = logging.StreamHandler()
|
||
# 设置控制台handler的日志级别
|
||
console_level = LOG_CONFIG.get("console_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||
_console_handler.setLevel(getattr(logging, console_level.upper(), logging.INFO))
|
||
return _console_handler
|
||
|
||
|
||
class TimestampedFileHandler(logging.Handler):
|
||
"""基于时间戳的文件处理器,简单的轮转份数限制"""
|
||
|
||
def __init__(self, log_dir, max_bytes=5 * 1024 * 1024, backup_count=30, encoding="utf-8"):
|
||
super().__init__()
|
||
self.log_dir = Path(log_dir)
|
||
self.log_dir.mkdir(exist_ok=True)
|
||
self.max_bytes = max_bytes
|
||
self.backup_count = backup_count
|
||
self.encoding = encoding
|
||
self._lock = threading.Lock()
|
||
|
||
# 当前活跃的日志文件
|
||
self.current_file = None
|
||
self.current_stream = None
|
||
self._init_current_file()
|
||
|
||
def _init_current_file(self):
|
||
"""初始化当前日志文件"""
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
self.current_file = self.log_dir / f"app_{timestamp}.log.jsonl"
|
||
self.current_stream = open(self.current_file, "a", encoding=self.encoding)
|
||
|
||
def _should_rollover(self):
|
||
"""检查是否需要轮转"""
|
||
if self.current_file and self.current_file.exists():
|
||
return self.current_file.stat().st_size >= self.max_bytes
|
||
return False
|
||
|
||
def _do_rollover(self):
|
||
"""执行轮转:关闭当前文件,创建新文件"""
|
||
if self.current_stream:
|
||
self.current_stream.close()
|
||
|
||
# 清理旧文件
|
||
self._cleanup_old_files()
|
||
|
||
# 创建新文件
|
||
self._init_current_file()
|
||
|
||
def _cleanup_old_files(self):
|
||
"""清理旧的日志文件,保留指定数量"""
|
||
try:
|
||
# 获取所有日志文件
|
||
log_files = list(self.log_dir.glob("app_*.log.jsonl"))
|
||
|
||
# 按修改时间排序
|
||
log_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
|
||
|
||
# 删除超出数量限制的文件
|
||
for old_file in log_files[self.backup_count :]:
|
||
try:
|
||
old_file.unlink()
|
||
print(f"[日志清理] 删除旧文件: {old_file.name}")
|
||
except Exception as e:
|
||
print(f"[日志清理] 删除失败 {old_file}: {e}")
|
||
|
||
except Exception as e:
|
||
print(f"[日志清理] 清理过程出错: {e}")
|
||
|
||
def emit(self, record):
|
||
"""发出日志记录"""
|
||
try:
|
||
with self._lock:
|
||
# 检查是否需要轮转
|
||
if self._should_rollover():
|
||
self._do_rollover()
|
||
|
||
# 写入日志
|
||
if self.current_stream:
|
||
msg = self.format(record)
|
||
self.current_stream.write(msg + "\n")
|
||
self.current_stream.flush()
|
||
|
||
except Exception:
|
||
self.handleError(record)
|
||
|
||
def close(self):
|
||
"""关闭处理器"""
|
||
with self._lock:
|
||
if self.current_stream:
|
||
self.current_stream.close()
|
||
self.current_stream = None
|
||
super().close()
|
||
|
||
|
||
# 旧的轮转文件处理器已移除,现在使用基于时间戳的处理器
|
||
|
||
|
||
def close_handlers():
|
||
"""安全关闭所有handler"""
|
||
global _file_handler, _console_handler
|
||
|
||
if _file_handler:
|
||
_file_handler.close()
|
||
_file_handler = None
|
||
|
||
if _console_handler:
|
||
_console_handler.close()
|
||
_console_handler = None
|
||
|
||
|
||
def remove_duplicate_handlers(): # sourcery skip: for-append-to-extend, list-comprehension
|
||
"""移除重复的handler,特别是文件handler"""
|
||
root_logger = logging.getLogger()
|
||
|
||
# 收集所有时间戳文件handler
|
||
file_handlers = []
|
||
for handler in root_logger.handlers[:]:
|
||
if isinstance(handler, TimestampedFileHandler):
|
||
file_handlers.append(handler)
|
||
|
||
# 如果有多个文件handler,保留第一个,关闭其他的
|
||
if len(file_handlers) > 1:
|
||
print(f"[日志系统] 检测到 {len(file_handlers)} 个重复的文件handler,正在清理...")
|
||
for i, handler in enumerate(file_handlers[1:], 1):
|
||
print(f"[日志系统] 关闭重复的文件handler {i}")
|
||
root_logger.removeHandler(handler)
|
||
handler.close()
|
||
|
||
# 更新全局引用
|
||
global _file_handler
|
||
_file_handler = file_handlers[0]
|
||
|
||
|
||
# 读取日志配置
|
||
def load_log_config(): # sourcery skip: use-contextlib-suppress
|
||
"""从配置文件加载日志设置"""
|
||
config_path = Path("config/bot_config.toml")
|
||
default_config = {
|
||
"date_style": "m-d H:i:s",
|
||
"log_level_style": "lite",
|
||
"color_text": "full",
|
||
"log_level": "INFO", # 全局日志级别(向下兼容)
|
||
"console_log_level": "INFO", # 控制台日志级别
|
||
"file_log_level": "DEBUG", # 文件日志级别
|
||
"suppress_libraries": [
|
||
"faiss",
|
||
"httpx",
|
||
"urllib3",
|
||
"asyncio",
|
||
"websockets",
|
||
"httpcore",
|
||
"requests",
|
||
"peewee",
|
||
"openai",
|
||
"uvicorn",
|
||
"jieba",
|
||
],
|
||
"library_log_levels": {"aiohttp": "WARNING"},
|
||
}
|
||
|
||
try:
|
||
if config_path.exists():
|
||
with open(config_path, "r", encoding="utf-8") as f:
|
||
config = tomlkit.load(f)
|
||
return config.get("log", default_config)
|
||
except Exception as e:
|
||
print(f"[日志系统] 加载日志配置失败: {e}")
|
||
pass
|
||
|
||
return default_config
|
||
|
||
|
||
LOG_CONFIG = load_log_config()
|
||
|
||
|
||
def get_timestamp_format():
|
||
"""将配置中的日期格式转换为Python格式"""
|
||
date_style = LOG_CONFIG.get("date_style", "Y-m-d H:i:s")
|
||
# 转换PHP风格的日期格式到Python格式
|
||
format_map = {
|
||
"Y": "%Y", # 4位年份
|
||
"m": "%m", # 月份(01-12)
|
||
"d": "%d", # 日期(01-31)
|
||
"H": "%H", # 小时(00-23)
|
||
"i": "%M", # 分钟(00-59)
|
||
"s": "%S", # 秒数(00-59)
|
||
}
|
||
|
||
python_format = date_style
|
||
for php_char, python_char in format_map.items():
|
||
python_format = python_format.replace(php_char, python_char)
|
||
|
||
return python_format
|
||
|
||
|
||
def configure_third_party_loggers():
|
||
"""配置第三方库的日志级别"""
|
||
# 设置根logger级别为所有handler中最低的级别,确保所有日志都能被捕获
|
||
console_level = LOG_CONFIG.get("console_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||
file_level = LOG_CONFIG.get("file_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||
|
||
# 获取最低级别(DEBUG < INFO < WARNING < ERROR < CRITICAL)
|
||
console_level_num = getattr(logging, console_level.upper(), logging.INFO)
|
||
file_level_num = getattr(logging, file_level.upper(), logging.INFO)
|
||
min_level = min(console_level_num, file_level_num)
|
||
|
||
root_logger = logging.getLogger()
|
||
root_logger.setLevel(min_level)
|
||
|
||
# 完全屏蔽的库
|
||
suppress_libraries = LOG_CONFIG.get("suppress_libraries", [])
|
||
for lib_name in suppress_libraries:
|
||
lib_logger = logging.getLogger(lib_name)
|
||
lib_logger.setLevel(logging.CRITICAL + 1) # 设置为比CRITICAL更高的级别,基本屏蔽所有日志
|
||
lib_logger.propagate = False # 阻止向上传播
|
||
|
||
# 设置特定级别的库
|
||
library_log_levels = LOG_CONFIG.get("library_log_levels", {})
|
||
for lib_name, level_name in library_log_levels.items():
|
||
lib_logger = logging.getLogger(lib_name)
|
||
level = getattr(logging, level_name.upper(), logging.WARNING)
|
||
lib_logger.setLevel(level)
|
||
|
||
|
||
def reconfigure_existing_loggers():
|
||
"""重新配置所有已存在的logger,解决加载顺序问题"""
|
||
# 获取根logger
|
||
root_logger = logging.getLogger()
|
||
|
||
# 重新设置根logger的所有handler的格式化器
|
||
for handler in root_logger.handlers:
|
||
if isinstance(handler, TimestampedFileHandler):
|
||
handler.setFormatter(file_formatter)
|
||
elif isinstance(handler, logging.StreamHandler):
|
||
handler.setFormatter(console_formatter)
|
||
|
||
# 遍历所有已存在的logger并重新配置
|
||
logger_dict = logging.getLogger().manager.loggerDict
|
||
for name, logger_obj in logger_dict.items():
|
||
if isinstance(logger_obj, logging.Logger):
|
||
# 检查是否是第三方库logger
|
||
suppress_libraries = LOG_CONFIG.get("suppress_libraries", [])
|
||
library_log_levels = LOG_CONFIG.get("library_log_levels", {})
|
||
|
||
# 如果在屏蔽列表中
|
||
if any(name.startswith(lib) for lib in suppress_libraries):
|
||
logger_obj.setLevel(logging.CRITICAL + 1)
|
||
logger_obj.propagate = False
|
||
continue
|
||
|
||
# 如果在特定级别设置中
|
||
for lib_name, level_name in library_log_levels.items():
|
||
if name.startswith(lib_name):
|
||
level = getattr(logging, level_name.upper(), logging.WARNING)
|
||
logger_obj.setLevel(level)
|
||
break
|
||
|
||
# 强制清除并重新设置所有handler
|
||
original_handlers = logger_obj.handlers[:]
|
||
for handler in original_handlers:
|
||
# 安全关闭handler
|
||
if hasattr(handler, "close"):
|
||
handler.close()
|
||
logger_obj.removeHandler(handler)
|
||
|
||
# 如果logger没有handler,让它使用根logger的handler(propagate=True)
|
||
if not logger_obj.handlers:
|
||
logger_obj.propagate = True
|
||
|
||
# 如果logger有自己的handler,重新配置它们(避免重复创建文件handler)
|
||
for handler in original_handlers:
|
||
if isinstance(handler, TimestampedFileHandler):
|
||
# 不重新添加,让它使用根logger的文件handler
|
||
continue
|
||
elif isinstance(handler, logging.StreamHandler):
|
||
handler.setFormatter(console_formatter)
|
||
logger_obj.addHandler(handler)
|
||
|
||
|
||
# 定义模块颜色映射
|
||
MODULE_COLORS = {
|
||
# 核心模块
|
||
"main": "\033[1;97m", # 亮白色+粗体 (主程序)
|
||
"api": "\033[92m", # 亮绿色
|
||
"emoji": "\033[38;5;214m", # 橙黄色,偏向橙色但与replyer和action_manager不同
|
||
"chat": "\033[92m", # 亮蓝色
|
||
"config": "\033[93m", # 亮黄色
|
||
"common": "\033[95m", # 亮紫色
|
||
"tools": "\033[96m", # 亮青色
|
||
"lpmm": "\033[96m",
|
||
"plugin_system": "\033[91m", # 亮红色
|
||
"person_info": "\033[32m", # 绿色
|
||
"individuality": "\033[94m", # 显眼的亮蓝色
|
||
"manager": "\033[35m", # 紫色
|
||
"llm_models": "\033[36m", # 青色
|
||
"remote": "\033[38;5;242m", # 深灰色,更不显眼
|
||
"planner": "\033[36m",
|
||
"memory": "\033[38;5;117m", # 天蓝色
|
||
"hfc": "\033[38;5;81m", # 稍微暗一些的青色,保持可读
|
||
"action_manager": "\033[38;5;208m", # 橙色,不与replyer重复
|
||
"message_manager": "\033[38;5;27m", # 深蓝色,消息管理器
|
||
"chatter_manager": "\033[38;5;129m", # 紫色,聊天管理器
|
||
"chatter_interest_scoring": "\033[38;5;214m", # 橙黄色,兴趣评分
|
||
"plan_executor": "\033[38;5;172m", # 橙褐色,计划执行器
|
||
# 关系系统
|
||
"relation": "\033[38;5;139m", # 柔和的紫色,不刺眼
|
||
# 聊天相关模块
|
||
"normal_chat": "\033[38;5;81m", # 亮蓝绿色
|
||
"heartflow": "\033[38;5;175m", # 柔和的粉色,不显眼但保持粉色系
|
||
"sub_heartflow": "\033[38;5;207m", # 粉紫色
|
||
"subheartflow_manager": "\033[38;5;201m", # 深粉色
|
||
"background_tasks": "\033[38;5;240m", # 灰色
|
||
"chat_message": "\033[38;5;45m", # 青色
|
||
"chat_stream": "\033[38;5;51m", # 亮青色
|
||
"sender": "\033[38;5;67m", # 稍微暗一些的蓝色,不显眼
|
||
"message_storage": "\033[38;5;33m", # 深蓝色
|
||
"expressor": "\033[38;5;166m", # 橙色
|
||
# 专注聊天模块
|
||
"replyer": "\033[38;5;166m", # 橙色
|
||
"memory_activator": "\033[38;5;117m", # 天蓝色
|
||
# 插件系统
|
||
"plugins": "\033[31m", # 红色
|
||
"plugin_api": "\033[33m", # 黄色
|
||
"plugin_manager": "\033[38;5;208m", # 红色
|
||
"base_plugin": "\033[38;5;202m", # 橙红色
|
||
"send_api": "\033[38;5;208m", # 橙色
|
||
"base_command": "\033[38;5;208m", # 橙色
|
||
"component_registry": "\033[38;5;214m", # 橙黄色
|
||
"stream_api": "\033[38;5;220m", # 黄色
|
||
"plugin_hot_reload": "\033[38;5;226m", # 品红色
|
||
"config_api": "\033[38;5;226m", # 亮黄色
|
||
"heartflow_api": "\033[38;5;154m", # 黄绿色
|
||
"action_apis": "\033[38;5;118m", # 绿色
|
||
"independent_apis": "\033[38;5;82m", # 绿色
|
||
"llm_api": "\033[38;5;46m", # 亮绿色
|
||
"database_api": "\033[38;5;10m", # 绿色
|
||
"utils_api": "\033[38;5;14m", # 青色
|
||
"message_api": "\033[38;5;6m", # 青色
|
||
# 管理器模块
|
||
"async_task_manager": "\033[38;5;129m", # 紫色
|
||
"mood": "\033[38;5;135m", # 紫红色
|
||
"local_storage": "\033[38;5;141m", # 紫色
|
||
"willing": "\033[38;5;147m", # 浅紫色
|
||
# 工具模块
|
||
"tool_use": "\033[38;5;172m", # 橙褐色
|
||
"tool_executor": "\033[38;5;172m", # 橙褐色
|
||
"base_tool": "\033[38;5;178m", # 金黄色
|
||
# 工具和实用模块
|
||
"prompt_build": "\033[38;5;105m", # 紫色
|
||
"chat_utils": "\033[38;5;111m", # 蓝色
|
||
"chat_image": "\033[38;5;117m", # 浅蓝色
|
||
"maibot_statistic": "\033[38;5;129m", # 紫色
|
||
# 特殊功能插件
|
||
"mute_plugin": "\033[38;5;240m", # 灰色
|
||
"core_actions": "\033[38;5;117m", # 深红色
|
||
"tts_action": "\033[38;5;58m", # 深黄色
|
||
"doubao_pic_plugin": "\033[38;5;64m", # 深绿色
|
||
# Action组件
|
||
"no_reply_action": "\033[38;5;214m", # 亮橙色,显眼但不像警告
|
||
"reply_action": "\033[38;5;46m", # 亮绿色
|
||
"base_action": "\033[38;5;250m", # 浅灰色
|
||
# 数据库和消息
|
||
"database_model": "\033[38;5;94m", # 橙褐色
|
||
"database": "\033[38;5;46m", # 橙褐色
|
||
"maim_message": "\033[38;5;140m", # 紫褐色
|
||
# 日志系统
|
||
"logger": "\033[38;5;8m", # 深灰色
|
||
"confirm": "\033[1;93m", # 黄色+粗体
|
||
# 模型相关
|
||
"model_utils": "\033[38;5;164m", # 紫红色
|
||
"relationship_fetcher": "\033[38;5;170m", # 浅紫色
|
||
"relationship_builder": "\033[38;5;93m", # 浅蓝色
|
||
"sqlalchemy_init": "\033[38;5;105m", #
|
||
"sqlalchemy_models": "\033[38;5;105m",
|
||
"sqlalchemy_database_api": "\033[38;5;105m",
|
||
# s4u
|
||
"context_web_api": "\033[38;5;240m", # 深灰色
|
||
"S4U_chat": "\033[92m", # 亮绿色
|
||
# API相关扩展
|
||
"chat_api": "\033[38;5;34m", # 深绿色
|
||
"emoji_api": "\033[38;5;40m", # 亮绿色
|
||
"generator_api": "\033[38;5;28m", # 森林绿
|
||
"person_api": "\033[38;5;22m", # 深绿色
|
||
"tool_api": "\033[38;5;76m", # 绿色
|
||
"OpenAI客户端": "\033[38;5;81m",
|
||
"Gemini客户端": "\033[38;5;81m",
|
||
# 插件系统扩展
|
||
"plugin_base": "\033[38;5;196m", # 红色
|
||
"base_event_handler": "\033[38;5;203m", # 粉红色
|
||
"events_manager": "\033[38;5;209m", # 橙红色
|
||
"global_announcement_manager": "\033[38;5;215m", # 浅橙色
|
||
# 工具和依赖管理
|
||
"dependency_config": "\033[38;5;24m", # 深蓝色
|
||
"dependency_manager": "\033[38;5;30m", # 深青色
|
||
"manifest_utils": "\033[38;5;39m", # 蓝色
|
||
"schedule_manager": "\033[38;5;27m", # 深蓝色
|
||
"monthly_plan_manager": "\033[38;5;171m",
|
||
"plan_manager": "\033[38;5;171m",
|
||
"llm_generator": "\033[38;5;171m",
|
||
"schedule_bridge": "\033[38;5;171m",
|
||
"sleep_manager": "\033[38;5;171m",
|
||
"official_configs": "\033[38;5;171m",
|
||
"mmc_com_layer": "\033[38;5;67m",
|
||
# 聊天和多媒体扩展
|
||
"chat_voice": "\033[38;5;87m", # 浅青色
|
||
"typo_gen": "\033[38;5;123m", # 天蓝色
|
||
"utils_video": "\033[38;5;75m", # 亮蓝色
|
||
"ReplyerManager": "\033[38;5;173m", # 浅橙色
|
||
"relationship_builder_manager": "\033[38;5;176m", # 浅紫色
|
||
"expression_selector": "\033[38;5;176m",
|
||
"chat_message_builder": "\033[38;5;176m",
|
||
# MaiZone QQ空间相关
|
||
"MaiZone": "\033[38;5;98m", # 紫色
|
||
"MaiZone-Monitor": "\033[38;5;104m", # 深紫色
|
||
"MaiZone.ConfigLoader": "\033[38;5;110m", # 蓝紫色
|
||
"MaiZone-Scheduler": "\033[38;5;134m", # 紫红色
|
||
"MaiZone-Utils": "\033[38;5;140m", # 浅紫色
|
||
# MaiZone Refactored
|
||
"MaiZone.HistoryUtils": "\033[38;5;140m",
|
||
"MaiZone.SchedulerService": "\033[38;5;134m",
|
||
"MaiZone.QZoneService": "\033[38;5;98m",
|
||
"MaiZone.MonitorService": "\033[38;5;104m",
|
||
"MaiZone.ImageService": "\033[38;5;110m",
|
||
"MaiZone.CookieService": "\033[38;5;140m",
|
||
"MaiZone.ContentService": "\033[38;5;110m",
|
||
"MaiZone.Plugin": "\033[38;5;98m",
|
||
"MaiZone.SendFeedCommand": "\033[38;5;134m",
|
||
"MaiZone.SendFeedAction": "\033[38;5;134m",
|
||
"MaiZone.ReadFeedAction": "\033[38;5;134m",
|
||
# 网络工具
|
||
"web_surfing_tool": "\033[38;5;130m", # 棕色
|
||
"tts": "\033[38;5;136m", # 浅棕色
|
||
"poke_plugin": "\033[38;5;136m",
|
||
"set_emoji_like_plugin": "\033[38;5;136m",
|
||
# mais4u系统扩展
|
||
"s4u_config": "\033[38;5;18m", # 深蓝色
|
||
"action": "\033[38;5;52m", # 深红色(mais4u的action)
|
||
"context_web": "\033[38;5;58m", # 深黄色
|
||
"gift_manager": "\033[38;5;161m", # 粉红色
|
||
"prompt": "\033[38;5;99m", # 紫色(mais4u的prompt)
|
||
"super_chat_manager": "\033[38;5;125m", # 紫红色
|
||
"watching": "\033[38;5;131m", # 深橙色
|
||
"offline_llm": "\033[38;5;236m", # 深灰色
|
||
"s4u_stream_generator": "\033[38;5;60m", # 深紫色
|
||
# 其他工具
|
||
"消息压缩工具": "\033[38;5;244m", # 灰色
|
||
"lpmm_get_knowledge_tool": "\033[38;5;102m", # 绿色
|
||
"message_chunker": "\033[38;5;244m",
|
||
"plan_generator": "\033[38;5;171m",
|
||
"Permission": "\033[38;5;196m",
|
||
"web_search_plugin": "\033[38;5;130m",
|
||
"url_parser_tool": "\033[38;5;130m",
|
||
"api_key_manager": "\033[38;5;130m",
|
||
"tavily_engine": "\033[38;5;130m",
|
||
"exa_engine": "\033[38;5;130m",
|
||
"ddg_engine": "\033[38;5;130m",
|
||
"bing_engine": "\033[38;5;130m",
|
||
"vector_instant_memory_v2": "\033[38;5;117m",
|
||
"async_memory_optimizer": "\033[38;5;117m",
|
||
"async_instant_memory_wrapper": "\033[38;5;117m",
|
||
"action_diagnostics": "\033[38;5;214m",
|
||
"anti_injector.message_processor": "\033[38;5;196m",
|
||
"anti_injector.user_ban": "\033[38;5;196m",
|
||
"anti_injector.statistics": "\033[38;5;196m",
|
||
"anti_injector.decision_maker": "\033[38;5;196m",
|
||
"anti_injector.counter_attack": "\033[38;5;196m",
|
||
"hfc.processor": "\033[38;5;81m",
|
||
"hfc.normal_mode": "\033[38;5;81m",
|
||
"wakeup": "\033[38;5;81m",
|
||
"cache_manager": "\033[38;5;244m",
|
||
"monthly_plan_db": "\033[38;5;94m",
|
||
"db_migration": "\033[38;5;94m",
|
||
"小彩蛋": "\033[38;5;214m",
|
||
"AioHTTP-Gemini客户端": "\033[38;5;81m",
|
||
"napcat_adapter": "\033[38;5;67m", # 柔和的灰蓝色,不刺眼且低调
|
||
"event_manager": "\033[38;5;79m", # 柔和的蓝绿色,稍微醒目但不刺眼
|
||
}
|
||
|
||
# 定义模块别名映射 - 将真实的logger名称映射到显示的别名
|
||
MODULE_ALIASES = {
|
||
# 核心模块
|
||
"individuality": "人格特质",
|
||
"emoji": "表情包",
|
||
"no_reply_action": "摸鱼",
|
||
"reply_action": "回复",
|
||
"action_manager": "动作",
|
||
"memory_activator": "记忆",
|
||
"tool_use": "工具",
|
||
"expressor": "表达方式",
|
||
"plugin_hot_reload": "热重载",
|
||
"database": "数据库",
|
||
"database_model": "数据库",
|
||
"mood": "情绪",
|
||
"memory": "记忆",
|
||
"tool_executor": "工具",
|
||
"hfc": "聊天节奏",
|
||
"chat": "所见",
|
||
"anti_injector": "反注入",
|
||
"anti_injector.detector": "反注入检测",
|
||
"anti_injector.shield": "反注入加盾",
|
||
"plugin_manager": "插件",
|
||
"relationship_builder": "关系",
|
||
"llm_models": "模型",
|
||
"person_info": "人物",
|
||
"chat_stream": "聊天流",
|
||
"message_manager": "消息管理",
|
||
"chatter_manager": "聊天管理",
|
||
"chatter_interest_scoring": "兴趣评分",
|
||
"plan_executor": "计划执行",
|
||
"planner": "规划器",
|
||
"replyer": "言语",
|
||
"config": "配置",
|
||
"main": "主程序",
|
||
# API相关扩展
|
||
"chat_api": "聊天接口",
|
||
"emoji_api": "表情接口",
|
||
"generator_api": "生成接口",
|
||
"person_api": "人物接口",
|
||
"tool_api": "工具接口",
|
||
# 插件系统扩展
|
||
"plugin_base": "插件基类",
|
||
"base_event_handler": "事件处理",
|
||
"event_manager": "事件管理器",
|
||
"global_announcement_manager": "全局通知",
|
||
# 工具和依赖管理
|
||
"dependency_config": "依赖配置",
|
||
"dependency_manager": "依赖管理",
|
||
"manifest_utils": "清单工具",
|
||
"schedule_manager": "规划系统-日程表管理",
|
||
"monthly_plan_manager": "规划系统-月度计划",
|
||
"plan_manager": "规划系统-计划管理",
|
||
"llm_generator": "规划系统-LLM生成",
|
||
"schedule_bridge": "计划桥接",
|
||
"sleep_manager": "睡眠管理",
|
||
"official_configs": "官方配置",
|
||
"mmc_com_layer": "MMC通信层",
|
||
# 聊天和多媒体扩展
|
||
"chat_voice": "语音处理",
|
||
"typo_gen": "错字生成",
|
||
"src.chat.utils.utils_video": "视频分析",
|
||
"ReplyerManager": "回复管理",
|
||
"relationship_builder_manager": "关系管理",
|
||
# MaiZone QQ空间相关
|
||
"MaiZone": "Mai空间",
|
||
"MaiZone-Monitor": "Mai空间监控",
|
||
"MaiZone.ConfigLoader": "Mai空间配置",
|
||
"MaiZone-Scheduler": "Mai空间调度",
|
||
"MaiZone-Utils": "Mai空间工具",
|
||
# MaiZone Refactored
|
||
"MaiZone.HistoryUtils": "Mai空间历史",
|
||
"MaiZone.SchedulerService": "Mai空间调度",
|
||
"MaiZone.QZoneService": "Mai空间服务",
|
||
"MaiZone.MonitorService": "Mai空间监控",
|
||
"MaiZone.ImageService": "Mai空间图片",
|
||
"MaiZone.CookieService": "Mai空间饼干",
|
||
"MaiZone.ContentService": "Mai空间内容",
|
||
"MaiZone.Plugin": "Mai空间插件",
|
||
"MaiZone.SendFeedCommand": "Mai空间发说说",
|
||
"MaiZone.SendFeedAction": "Mai空间发说说",
|
||
"MaiZone.ReadFeedAction": "Mai空间读说说",
|
||
# 网络工具
|
||
"web_surfing_tool": "网络搜索",
|
||
# napcat ada
|
||
"napcat_adapter": "Napcat 适配器",
|
||
"tts": "语音合成",
|
||
# mais4u系统扩展
|
||
"s4u_config": "直播配置",
|
||
"action": "直播动作",
|
||
"context_web": "网络上下文",
|
||
"gift_manager": "礼物管理",
|
||
"prompt": "直播提示",
|
||
"super_chat_manager": "醒目留言",
|
||
"watching": "观看状态",
|
||
"offline_llm": "离线模型",
|
||
"s4u_stream_generator": "直播生成",
|
||
# 其他工具
|
||
"消息压缩工具": "消息压缩",
|
||
"lpmm_get_knowledge_tool": "知识获取",
|
||
"message_chunker": "消息分块",
|
||
"plan_generator": "计划生成",
|
||
"Permission": "权限管理",
|
||
"web_search_plugin": "网页搜索插件",
|
||
"url_parser_tool": "URL解析工具",
|
||
"api_key_manager": "API密钥管理",
|
||
"tavily_engine": "Tavily引擎",
|
||
"exa_engine": "Exa引擎",
|
||
"ddg_engine": "DDG引擎",
|
||
"bing_engine": "Bing引擎",
|
||
"vector_instant_memory_v2": "向量瞬时记忆",
|
||
"async_memory_optimizer": "异步记忆优化器",
|
||
"async_instant_memory_wrapper": "异步瞬时记忆包装器",
|
||
"action_diagnostics": "动作诊断",
|
||
"anti_injector.message_processor": "反注入消息处理器",
|
||
"anti_injector.user_ban": "反注入用户封禁",
|
||
"anti_injector.statistics": "反注入统计",
|
||
"anti_injector.decision_maker": "反注入决策者",
|
||
"anti_injector.counter_attack": "反注入反击",
|
||
"hfc.processor": "聊天节奏处理器",
|
||
"hfc.normal_mode": "聊天节奏普通模式",
|
||
"wakeup": "唤醒",
|
||
"cache_manager": "缓存管理",
|
||
"monthly_plan_db": "月度计划数据库",
|
||
"db_migration": "数据库迁移",
|
||
"小彩蛋": "小彩蛋",
|
||
"AioHTTP-Gemini客户端": "AioHTTP-Gemini客户端",
|
||
}
|
||
|
||
RESET_COLOR = "\033[0m"
|
||
|
||
|
||
class ModuleColoredConsoleRenderer:
|
||
"""自定义控制台渲染器,为不同模块提供不同颜色"""
|
||
|
||
def __init__(self, colors=True):
|
||
# sourcery skip: merge-duplicate-blocks, remove-redundant-if
|
||
self._colors = colors
|
||
self._config = LOG_CONFIG
|
||
|
||
# 日志级别颜色
|
||
self._level_colors = {
|
||
"debug": "\033[38;5;208m", # 橙色
|
||
"info": "\033[38;5;117m", # 天蓝色
|
||
"success": "\033[32m", # 绿色
|
||
"warning": "\033[33m", # 黄色
|
||
"error": "\033[31m", # 红色
|
||
"critical": "\033[35m", # 紫色
|
||
}
|
||
|
||
# 根据配置决定是否启用颜色
|
||
color_text = self._config.get("color_text", "title")
|
||
if color_text == "none":
|
||
self._colors = False
|
||
elif color_text == "title":
|
||
self._enable_module_colors = True
|
||
self._enable_level_colors = False
|
||
self._enable_full_content_colors = False
|
||
elif color_text == "full":
|
||
self._enable_module_colors = True
|
||
self._enable_level_colors = True
|
||
self._enable_full_content_colors = True
|
||
else:
|
||
self._enable_module_colors = True
|
||
self._enable_level_colors = False
|
||
self._enable_full_content_colors = False
|
||
|
||
def __call__(self, logger, method_name, event_dict):
|
||
# sourcery skip: merge-duplicate-blocks
|
||
"""渲染日志消息"""
|
||
# 获取基本信息
|
||
timestamp = event_dict.get("timestamp", "")
|
||
level = event_dict.get("level", "info")
|
||
logger_name = event_dict.get("logger_name", "")
|
||
event = event_dict.get("event", "")
|
||
|
||
# 构建输出
|
||
parts = []
|
||
|
||
# 日志级别样式配置
|
||
log_level_style = self._config.get("log_level_style", "lite")
|
||
level_color = self._level_colors.get(level.lower(), "") if self._colors else ""
|
||
|
||
# 时间戳(lite模式下按级别着色)
|
||
if timestamp:
|
||
if log_level_style == "lite" and level_color:
|
||
timestamp_part = f"{level_color}{timestamp}{RESET_COLOR}"
|
||
else:
|
||
timestamp_part = timestamp
|
||
parts.append(timestamp_part)
|
||
|
||
# 日志级别显示(根据配置样式)
|
||
if log_level_style == "full":
|
||
# 显示完整级别名并着色
|
||
level_text = level.upper()
|
||
if level_color:
|
||
level_part = f"{level_color}[{level_text:>8}]{RESET_COLOR}"
|
||
else:
|
||
level_part = f"[{level_text:>8}]"
|
||
parts.append(level_part)
|
||
|
||
elif log_level_style == "compact":
|
||
# 只显示首字母并着色
|
||
level_text = level.upper()[0]
|
||
if level_color:
|
||
level_part = f"{level_color}[{level_text:>8}]{RESET_COLOR}"
|
||
else:
|
||
level_part = f"[{level_text:>8}]"
|
||
parts.append(level_part)
|
||
|
||
# lite模式不显示级别,只给时间戳着色
|
||
|
||
# 获取模块颜色,用于full模式下的整体着色
|
||
module_color = ""
|
||
if self._colors and self._enable_module_colors and logger_name:
|
||
module_color = MODULE_COLORS.get(logger_name, "")
|
||
|
||
# 模块名称(带颜色和别名支持)
|
||
if logger_name:
|
||
# 获取别名,如果没有别名则使用原名称
|
||
display_name = MODULE_ALIASES.get(logger_name, logger_name)
|
||
|
||
if self._colors and self._enable_module_colors:
|
||
if module_color:
|
||
module_part = f"{module_color}[{display_name}]{RESET_COLOR}"
|
||
else:
|
||
module_part = f"[{display_name}]"
|
||
else:
|
||
module_part = f"[{display_name}]"
|
||
parts.append(module_part)
|
||
|
||
# 消息内容(确保转换为字符串)
|
||
event_content = ""
|
||
if isinstance(event, str):
|
||
event_content = event
|
||
elif isinstance(event, dict):
|
||
# 如果是字典,格式化为可读字符串
|
||
try:
|
||
event_content = orjson.dumps(event).decode("utf-8")
|
||
except (TypeError, ValueError):
|
||
event_content = str(event)
|
||
else:
|
||
# 其他类型直接转换为字符串
|
||
event_content = str(event)
|
||
|
||
# 在full模式下为消息内容着色
|
||
if self._colors and self._enable_full_content_colors:
|
||
# 检查是否包含“内心思考:”
|
||
if "内心思考:" in event_content:
|
||
# 使用明亮的粉色
|
||
thought_color = "\033[38;5;218m"
|
||
# 分割消息内容
|
||
prefix, thought = event_content.split("内心思考:", 1)
|
||
|
||
# 前缀部分(“决定进行回复,”)使用模块颜色
|
||
if module_color:
|
||
prefix_colored = f"{module_color}{prefix.strip()}{RESET_COLOR}"
|
||
else:
|
||
prefix_colored = prefix.strip()
|
||
|
||
# “内心思考”部分换行并使用专属颜色
|
||
thought_colored = f"\n\n{thought_color}内心思考:{thought.strip()}{RESET_COLOR}\n"
|
||
|
||
# 重新组合
|
||
# parts.append(prefix_colored + thought_colored)
|
||
# 将前缀和思考内容作为独立的part添加,避免它们之间出现多余的空格
|
||
if prefix_colored:
|
||
parts.append(prefix_colored)
|
||
parts.append(thought_colored)
|
||
|
||
elif module_color:
|
||
event_content = f"{module_color}{event_content}{RESET_COLOR}"
|
||
parts.append(event_content)
|
||
else:
|
||
parts.append(event_content)
|
||
else:
|
||
parts.append(event_content)
|
||
|
||
# 处理其他字段
|
||
extras = []
|
||
for key, value in event_dict.items():
|
||
if key not in ("timestamp", "level", "logger_name", "event"):
|
||
# 确保值也转换为字符串
|
||
if isinstance(value, (dict, list)):
|
||
try:
|
||
value_str = orjson.dumps(value).decode("utf-8")
|
||
except (TypeError, ValueError):
|
||
value_str = str(value)
|
||
else:
|
||
value_str = str(value)
|
||
|
||
# 在full模式下为额外字段着色
|
||
extra_field = f"{key}={value_str}"
|
||
if self._colors and self._enable_full_content_colors and module_color:
|
||
extra_field = f"{module_color}{extra_field}{RESET_COLOR}"
|
||
|
||
extras.append(extra_field)
|
||
|
||
if extras:
|
||
parts.append(" ".join(extras))
|
||
|
||
return " ".join(parts)
|
||
|
||
|
||
# 配置标准logging以支持文件输出和压缩
|
||
# 使用单例handler避免重复创建
|
||
file_handler = get_file_handler()
|
||
console_handler = get_console_handler()
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(message)s",
|
||
handlers=[file_handler, console_handler],
|
||
)
|
||
|
||
|
||
def configure_structlog():
|
||
"""配置structlog"""
|
||
structlog.configure(
|
||
processors=[
|
||
structlog.contextvars.merge_contextvars,
|
||
structlog.processors.add_log_level,
|
||
structlog.processors.StackInfoRenderer(),
|
||
structlog.dev.set_exc_info,
|
||
structlog.processors.TimeStamper(fmt=get_timestamp_format(), utc=False),
|
||
# 根据输出类型选择不同的渲染器
|
||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||
],
|
||
wrapper_class=structlog.stdlib.BoundLogger,
|
||
context_class=dict,
|
||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||
cache_logger_on_first_use=True,
|
||
)
|
||
|
||
|
||
# 配置structlog
|
||
configure_structlog()
|
||
|
||
# 为文件输出配置JSON格式
|
||
file_formatter = structlog.stdlib.ProcessorFormatter(
|
||
processor=structlog.processors.JSONRenderer(ensure_ascii=False),
|
||
foreign_pre_chain=[
|
||
structlog.stdlib.add_logger_name,
|
||
structlog.stdlib.add_log_level,
|
||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||
structlog.processors.TimeStamper(fmt="iso"),
|
||
structlog.processors.StackInfoRenderer(),
|
||
structlog.processors.format_exc_info,
|
||
],
|
||
)
|
||
|
||
# 为控制台输出配置可读格式
|
||
console_formatter = structlog.stdlib.ProcessorFormatter(
|
||
processor=ModuleColoredConsoleRenderer(colors=True),
|
||
foreign_pre_chain=[
|
||
structlog.stdlib.add_logger_name,
|
||
structlog.stdlib.add_log_level,
|
||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||
structlog.processors.TimeStamper(fmt=get_timestamp_format(), utc=False),
|
||
structlog.processors.StackInfoRenderer(),
|
||
structlog.processors.format_exc_info,
|
||
],
|
||
)
|
||
|
||
# 获取根logger并配置格式化器
|
||
root_logger = logging.getLogger()
|
||
for handler in root_logger.handlers:
|
||
if isinstance(handler, TimestampedFileHandler):
|
||
handler.setFormatter(file_formatter)
|
||
else:
|
||
handler.setFormatter(console_formatter)
|
||
|
||
|
||
# 立即配置日志系统,确保最早期的日志也使用正确格式
|
||
def _immediate_setup():
|
||
"""立即设置日志系统,在模块导入时就生效"""
|
||
# 重新配置structlog
|
||
configure_structlog()
|
||
|
||
# 清除所有已有的handler,重新配置
|
||
root_logger = logging.getLogger()
|
||
for handler in root_logger.handlers[:]:
|
||
root_logger.removeHandler(handler)
|
||
|
||
# 使用单例handler避免重复创建
|
||
file_handler = get_file_handler()
|
||
console_handler = get_console_handler()
|
||
|
||
# 重新添加配置好的handler
|
||
root_logger.addHandler(file_handler)
|
||
root_logger.addHandler(console_handler)
|
||
|
||
# 设置格式化器
|
||
file_handler.setFormatter(file_formatter)
|
||
console_handler.setFormatter(console_formatter)
|
||
|
||
# 清理重复的handler
|
||
remove_duplicate_handlers()
|
||
|
||
# 配置第三方库日志
|
||
configure_third_party_loggers()
|
||
|
||
# 重新配置所有已存在的logger
|
||
reconfigure_existing_loggers()
|
||
|
||
|
||
# 立即执行配置
|
||
_immediate_setup()
|
||
|
||
raw_logger: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||
|
||
binds: dict[str, Callable] = {}
|
||
|
||
|
||
def get_logger(name: Optional[str]) -> structlog.stdlib.BoundLogger:
|
||
"""获取logger实例,支持按名称绑定"""
|
||
if name is None:
|
||
return raw_logger
|
||
logger = binds.get(name) # type: ignore
|
||
if logger is None:
|
||
logger: structlog.stdlib.BoundLogger = structlog.get_logger(name).bind(logger_name=name)
|
||
binds[name] = logger
|
||
return logger
|
||
|
||
|
||
def initialize_logging():
|
||
"""手动初始化日志系统,确保所有logger都使用正确的配置
|
||
|
||
在应用程序的早期调用此函数,确保所有模块都使用统一的日志配置
|
||
"""
|
||
global LOG_CONFIG
|
||
LOG_CONFIG = load_log_config()
|
||
# print(LOG_CONFIG)
|
||
configure_third_party_loggers()
|
||
reconfigure_existing_loggers()
|
||
|
||
# 启动日志清理任务
|
||
start_log_cleanup_task()
|
||
|
||
# 输出初始化信息
|
||
logger = get_logger("logger")
|
||
console_level = LOG_CONFIG.get("console_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||
file_level = LOG_CONFIG.get("file_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||
|
||
logger.info("日志系统已初始化:")
|
||
logger.info(f" - 控制台级别: {console_level}")
|
||
logger.info(f" - 文件级别: {file_level}")
|
||
logger.info(" - 轮转份数: 30个文件|自动清理: 30天前的日志")
|
||
|
||
|
||
def cleanup_old_logs():
|
||
"""清理过期的日志文件"""
|
||
try:
|
||
cleanup_days = 30 # 硬编码30天
|
||
cutoff_date = datetime.now() - timedelta(days=cleanup_days)
|
||
deleted_count = 0
|
||
deleted_size = 0
|
||
|
||
# 遍历日志目录
|
||
for log_file in LOG_DIR.glob("*.log*"):
|
||
try:
|
||
file_time = datetime.fromtimestamp(log_file.stat().st_mtime)
|
||
if file_time < cutoff_date:
|
||
file_size = log_file.stat().st_size
|
||
log_file.unlink()
|
||
deleted_count += 1
|
||
deleted_size += file_size
|
||
except Exception as e:
|
||
logger = get_logger("logger")
|
||
logger.warning(f"清理日志文件 {log_file} 时出错: {e}")
|
||
|
||
if deleted_count > 0:
|
||
logger = get_logger("logger")
|
||
logger.info(f"清理了 {deleted_count} 个过期日志文件,释放空间 {deleted_size / 1024 / 1024:.2f} MB")
|
||
|
||
except Exception as e:
|
||
logger = get_logger("logger")
|
||
logger.error(f"清理旧日志文件时出错: {e}")
|
||
|
||
|
||
def start_log_cleanup_task():
|
||
"""启动日志清理任务"""
|
||
|
||
def cleanup_task():
|
||
while True:
|
||
time.sleep(24 * 60 * 60) # 每24小时执行一次
|
||
cleanup_old_logs()
|
||
|
||
cleanup_thread = threading.Thread(target=cleanup_task, daemon=True)
|
||
cleanup_thread.start()
|
||
|
||
logger = get_logger("logger")
|
||
logger.info("已启动日志清理任务,将自动清理30天前的日志文件(轮转份数限制: 30个文件)")
|
||
|
||
|
||
def shutdown_logging():
|
||
"""优雅关闭日志系统,释放所有文件句柄"""
|
||
logger = get_logger("logger")
|
||
logger.info("正在关闭日志系统...")
|
||
|
||
# 关闭所有handler
|
||
root_logger = logging.getLogger()
|
||
for handler in root_logger.handlers[:]:
|
||
if hasattr(handler, "close"):
|
||
handler.close()
|
||
root_logger.removeHandler(handler)
|
||
|
||
# 关闭全局handler
|
||
close_handlers()
|
||
|
||
# 关闭所有其他logger的handler
|
||
logger_dict = logging.getLogger().manager.loggerDict
|
||
for _name, logger_obj in logger_dict.items():
|
||
if isinstance(logger_obj, logging.Logger):
|
||
for handler in logger_obj.handlers[:]:
|
||
if hasattr(handler, "close"):
|
||
handler.close()
|
||
logger_obj.removeHandler(handler)
|
||
|
||
logger.info("日志系统已关闭")
|