Files
abot/plugins/stats_collector/main.py
liuwei ce38f66b7b 为主消息链路接入trace_id追踪
- 为接收消息生成并透传trace_id到插件处理上下文
- 统一关键日志输出格式,支持按trace_id串联排障
- 将统计插件错误记录与执行日志补充trace_id关联信息
- 在工程优化文档中补充近期已完成治理项
2026-04-30 15:00:29 +08:00

178 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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