- 为接收消息生成并透传trace_id到插件处理上下文 - 统一关键日志输出格式,支持按trace_id串联排障 - 将统计插件错误记录与执行日志补充trace_id关联信息 - 在工程优化文档中补充近期已完成治理项
178 lines
6.3 KiB
Python
178 lines
6.3 KiB
Python
from loguru import logger
|
||
from typing import Dict, Any, Tuple, Optional, List
|
||
|
||
from base.plugin_common.plugin_interface import PluginInterface, PluginStatus
|
||
from db.stats_db import StatsDBOperator
|
||
from db.connection import DBConnectionManager
|
||
|
||
|
||
class StatsCollectorPlugin(PluginInterface):
|
||
"""统计收集插件"""
|
||
|
||
@property
|
||
def name(self) -> str:
|
||
return "指令记录"
|
||
|
||
@property
|
||
def version(self) -> str:
|
||
return "1.0.0"
|
||
|
||
@property
|
||
def description(self) -> str:
|
||
return "群聊指令数据记录"
|
||
|
||
@property
|
||
def author(self) -> str:
|
||
return "Liuwei"
|
||
|
||
@property
|
||
def command_prefix(self) -> Optional[str]:
|
||
return "#"
|
||
|
||
@property
|
||
def commands(self) -> List[str]:
|
||
return []
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.LOG = logger
|
||
self.LOG.debug(f"正在初始化 {self.name} 插件...")
|
||
# 默认配置:
|
||
# 1. 这个插件现在不再依赖事件总线,而是由主消息分发链路直接回调;
|
||
# 2. 因此这里保留一份轻量配置,只控制“是否记录”和“排除哪些插件”;
|
||
# 3. 这样既能延续原有统计面板数据结构,也能避免事件系统带来的额外复杂度。
|
||
self.config = {
|
||
"enable": True,
|
||
"record_all_plugins": True, # 是否记录所有插件的调用
|
||
"excluded_plugins": [], # 排除的插件列表
|
||
}
|
||
self.db_manager = DBConnectionManager.get_instance()
|
||
self.stats_db = StatsDBOperator(self.db_manager)
|
||
|
||
def initialize(self, context: Dict[str, Any]) -> bool:
|
||
"""初始化插件"""
|
||
# 这里显式只读取插件自己的配置,不再把 system_context 整体 merge 进来:
|
||
# 1. 旧实现把 initialize 入参当成“插件配置”使用,但实际传入的是 system_context;
|
||
# 2. 结果会把 db_manager、redis_pool 等运行时对象误写到 self.config 中;
|
||
# 3. 改成只消费 load_config 后的 self._config,避免配置结构持续污染。
|
||
if isinstance(self._config, dict):
|
||
self.config.update(self._config)
|
||
|
||
# 若主系统已经初始化了 DB 管理器,则优先复用统一实例,避免插件侧再走兜底单例。
|
||
if isinstance(context, dict) and context.get("db_manager") is not None:
|
||
self.db_manager = context.get("db_manager")
|
||
self.stats_db = StatsDBOperator(self.db_manager)
|
||
|
||
if not self.config["enable"]:
|
||
self.LOG.info("统计收集插件已禁用")
|
||
return False
|
||
|
||
return True
|
||
|
||
def record_plugin_call(
|
||
self,
|
||
*,
|
||
plugin_name: str,
|
||
command: str,
|
||
user_id: str,
|
||
group_id: Optional[str],
|
||
is_group: bool,
|
||
process_result: bool,
|
||
process_time_ms: float,
|
||
trace_id: str = "",
|
||
) -> None:
|
||
"""由主链路直接调用,记录一次插件执行结果。"""
|
||
# 主链路可能会在高频场景下频繁回调这里,因此先做最便宜的开关判断,
|
||
# 避免对已关闭或被排除插件继续产生数据库写入成本。
|
||
if not self._should_record_plugin(plugin_name):
|
||
return
|
||
|
||
try:
|
||
self.stats_db.record_plugin_call(
|
||
plugin_name=plugin_name,
|
||
command=command,
|
||
user_id=user_id,
|
||
group_id=group_id if is_group else None,
|
||
success=process_result,
|
||
process_time_ms=process_time_ms,
|
||
)
|
||
self.LOG.debug(
|
||
f"记录插件调用结束: trace_id={trace_id or '-'} "
|
||
f"{plugin_name} - {command} - 成功: {process_result} - 处理时间: {process_time_ms}ms"
|
||
)
|
||
except Exception as e:
|
||
self.LOG.error(f"记录插件调用统计数据出错: {e}")
|
||
|
||
def record_plugin_error(
|
||
self,
|
||
*,
|
||
plugin_name: str,
|
||
command: str,
|
||
user_id: str,
|
||
group_id: Optional[str],
|
||
is_group: bool,
|
||
error_message: str,
|
||
trace_id: str = "",
|
||
stack_trace: Optional[str] = None,
|
||
) -> None:
|
||
"""由主链路直接调用,记录一次插件执行异常。"""
|
||
if not self._should_record_plugin(plugin_name):
|
||
return
|
||
|
||
try:
|
||
self.stats_db.record_error(
|
||
plugin_name=plugin_name,
|
||
command=command,
|
||
user_id=user_id,
|
||
group_id=group_id if is_group else None,
|
||
# 错误表当前没有独立 trace_id 字段,因此先把 trace_id 前缀写入文案,
|
||
# 这样后台查看错误列表时,仍然可以把数据库错误记录与运行日志串起来。
|
||
error_message=f"[trace_id={trace_id}] {error_message}" if trace_id else error_message,
|
||
stack_trace=(f"[trace_id={trace_id}]\n{stack_trace}" if trace_id and stack_trace else stack_trace),
|
||
)
|
||
self.LOG.debug(
|
||
f"记录插件调用错误: trace_id={trace_id or '-'} {plugin_name} - {command} - {error_message}"
|
||
)
|
||
except Exception as e:
|
||
self.LOG.error(f"记录插件错误信息出错: {e}")
|
||
|
||
def _should_record_plugin(self, plugin_name: str) -> bool:
|
||
"""检查是否应该记录该插件的调用"""
|
||
if not self.config["record_all_plugins"]:
|
||
return False
|
||
|
||
if plugin_name in self.config["excluded_plugins"]:
|
||
return False
|
||
|
||
# 不记录自身的调用
|
||
if plugin_name == self.name:
|
||
return False
|
||
|
||
return True
|
||
|
||
def match_command(self, content: str) -> bool:
|
||
"""匹配命令"""
|
||
# 该插件不处理用户消息
|
||
return False
|
||
|
||
def process_message(self, message: Dict[str, Any]) -> Tuple[bool, str]:
|
||
"""处理消息"""
|
||
# 该插件不处理用户消息
|
||
return False, ""
|
||
|
||
def shutdown(self) -> None:
|
||
"""关闭插件"""
|
||
self.LOG.info("统计收集插件已关闭")
|
||
|
||
def start(self) -> bool:
|
||
"""启动插件"""
|
||
self.LOG.debug(f"[{self.name}] 插件已启动")
|
||
self.status = PluginStatus.RUNNING
|
||
return True
|
||
|
||
def stop(self) -> bool:
|
||
"""停止插件"""
|
||
self.LOG.info(f"[{self.name}] 插件已停止")
|
||
self.status = PluginStatus.STOPPED
|
||
return True
|