mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
9.1 KiB
9.1 KiB
TraceID 实现代码示例
这个文件展示如何在插件中实现 contextvars + logging.Filter 方案。
文件结构
astrbot_plugin_qq_group_daily_analysis/
├── src/
│ ├── utils/
│ │ ├── trace.py # ← 新增:TraceID 相关
│ │ └── helpers.py
│ ├── core/
│ │ └── config.py
│ └── ...
├── main.py # ← 需要修改:注册 Filter
└── ...
1. 新增文件:src/utils/trace.py
"""
TraceID 追踪工具模块
提供分布式追踪的 trace_id 上下文管理
"""
import contextvars
import logging
import time
from astrbot.api import logger
# ============ ContextVar 定义 ============
_trace_id: contextvars.ContextVar[str] = contextvars.ContextVar(
'trace_id',
default=''
)
# ============ Filter 实现 ============
class TraceIDFilter(logging.Filter):
"""
将 trace_id 自动注入到日志记录
使用方式:
logger.addFilter(TraceIDFilter())
# 之后所有日志的 record 对象都会有 trace_id 属性
"""
def filter(self, record):
"""添加 trace_id 到日志记录"""
trace_id = _trace_id.get('')
record.trace_id = trace_id if trace_id else 'no-trace'
return True
# ============ 接口函数 ============
def set_trace_id(group_id: str, timestamp: int = None) -> str:
"""
设置当前协程的 trace_id
Args:
group_id: 群 ID
timestamp: 时间戳(如果为 None 则使用当前时间)
Returns:
生成的 trace_id 字符串
Example:
>>> trace_id = set_trace_id("123456789")
>>> logger.info("分析开始") # 日志自动包含 trace_id
"""
if timestamp is None:
timestamp = int(time.time())
trace_id = f"{group_id}-{timestamp}"
_trace_id.set(trace_id)
return trace_id
def get_trace_id() -> str:
"""获取当前协程的 trace_id"""
return _trace_id.get('no-trace')
def clear_trace_id():
"""清理当前协程的 trace_id"""
_trace_id.set('')
def with_trace_id(group_id: str):
"""
上下文管理器:自动管理 trace_id 的生命周期
Example:
>>> from src.utils.trace import with_trace_id
>>>
>>> async def analyze_group(group_id: str):
... with with_trace_id(group_id):
... logger.info("开始分析") # 自动包含 trace_id
... # ...
... logger.info("分析完成")
... # 退出时自动清理 trace_id
"""
class TraceContextManager:
def __init__(self, group_id):
self.group_id = group_id
self.trace_id = None
def __enter__(self):
self.trace_id = set_trace_id(self.group_id)
return self.trace_id
def __exit__(self, exc_type, exc_val, exc_tb):
clear_trace_id()
return False
return TraceContextManager(group_id)
# ============ 初始化函数 ============
def setup_trace_logging():
"""
初始化 TraceID 日志追踪
在插件启动时调用一次即可:
from src.utils.trace import setup_trace_logging
setup_trace_logging()
"""
# 注册 Filter 到 AstrBot logger
trace_filter = TraceIDFilter()
# 检查是否已注册(避免重复注册)
for existing_filter in logger.filters:
if isinstance(existing_filter, TraceIDFilter):
return # 已经注册过了
logger.addFilter(trace_filter)
logger.info("[Trace] TraceID 日志追踪已启用")
2. 修改文件:main.py(插件主文件)
在 __init__ 方法中添加初始化:
# main.py
from src.utils.trace import setup_trace_logging
class QQGroupDailyAnalysis(Star):
def __init__(self, context: Context, config: AstrBotConfig):
super().__init__(context)
# ... 其他初始化代码 ...
# ← 新增:初始化 TraceID 追踪
setup_trace_logging()
# ... 其他初始化代码 ...
3. 修改文件:src/scheduler/auto_scheduler.py
在群分析方法中使用 TraceID:
# auto_scheduler.py
from src.utils.trace import set_trace_id, clear_trace_id, with_trace_id
class AutoScheduler:
# ... 其他方法 ...
async def _perform_auto_analysis_for_group(self, group_id: str):
"""为指定群执行自动分析"""
# ← 方案 1:使用上下文管理器(推荐)
with with_trace_id(group_id):
try:
logger.info(f"开始为群 {group_id} 执行自动分析")
# ... 分析逻辑,所有 logger 调用都自动包含 trace_id ...
except Exception as e:
logger.error(f"群 {group_id} 自动分析失败: {e}")
# ← 方案 2:手动设置/清理(传统方式)
# set_trace_id(group_id)
# try:
# logger.info(f"开始为群 {group_id} 执行自动分析")
# # ... 分析逻辑 ...
# finally:
# clear_trace_id()
4. 修改文件:src/utils/helpers.py
在 MessageAnalyzer 中使用 TraceID:
# helpers.py
from src.utils.trace import with_trace_id
class MessageAnalyzer:
async def analyze_messages(
self,
messages: list[dict],
group_id: str,
unified_msg_origin: str = None
) -> dict:
"""完整的消息分析流程"""
with with_trace_id(group_id):
try:
logger.info("开始消息分析")
# 基础统计
statistics = await asyncio.to_thread(...)
logger.info(f"统计完成:{len(messages)} 条消息")
# LLM 分析
topics, user_titles, golden_quotes, token_usage = \
await self.llm_analyzer.analyze_all_concurrent(...)
logger.info(f"LLM 分析完成")
return {
"statistics": statistics,
"topics": topics,
"user_titles": user_titles,
}
except Exception as e:
logger.error(f"消息分析失败: {e}")
return None
5. 日志输出效果
启动后,日志会自动包含 trace_id:
$ python main.py
[10:30:45] [Plug] [INFO ] [auto_scheduler:100]: [123456789-1707292800] 开始为群 123456789 执行自动分析
[10:30:45] [Plug] [INFO ] [message_handler:50]: [123456789-1707292800] 开始获取消息
[10:30:46] [Plug] [INFO ] [message_handler:100]: [123456789-1707292800] 获取成功,共 256 条消息
[10:30:47] [Plug] [INFO ] [helpers:200]: [123456789-1707292800] 开始消息分析
[10:30:48] [Plug] [INFO ] [llm_analyzer:300]: [123456789-1707292800] 开始 LLM 分析
[10:30:50] [Plug] [INFO ] [llm_analyzer:350]: [123456789-1707292800] LLM 分析完成
[10:30:51] [Plug] [INFO ] [report_generator:400]: [123456789-1707292800] 报告生成中
[10:30:53] [Plug] [INFO ] [auto_scheduler:450]: [123456789-1707292800] 分析完成,耗时 8s
6. 查看日志
终端实时查看
# 看所有日志
tail -f logs/astrbot.log
# 看特定群的日志
tail -f logs/astrbot.log | grep "123456789"
# 或使用 grep 搜索
grep "123456789-1707292800" logs/astrbot.log
按 TraceID 追踪完整链路
# 获取某个 trace_id 的所有日志
grep "123456789-1707292800" logs/astrbot.log
# 按时间戳统计分析耗时
# trace_id 格式 {group_id}-{start_timestamp}
# 可从日志时间戳对比,计算耗时
7. 成本统计
| 项目 | 工作量 | 难度 |
|---|---|---|
| 新增 trace.py | ~80 行 | 低 |
| 修改 main.py | 3 行 | 低 |
| 修改 auto_scheduler.py | ~10 行 | 低 |
| 修改 helpers.py | ~5 行 | 低 |
| 修改其他文件 | 可选(向后兼容) | 低 |
| 总计 | ~100 行 | 低 |
8. 向后兼容性
✅ 完全向后兼容
- 现有代码无需改动
- 新增代码仅在初始化时注册 Filter
- 所有日志输出不变,仅在 record 对象中添加 trace_id 字段
- 如果日志格式未修改,trace_id 不会显示,但在代码中可以访问
9. 扩展应用
应用 1:性能分析
from src.utils.trace import get_trace_id
import time
start = time.time()
# ... 某个操作 ...
elapsed = time.time() - start
logger.info(f"操作耗时 {elapsed:.2f}s")
# 日志自动包含 trace_id,可后续统计所有群的平均耗时
应用 2:错误统计
# 找出所有失败的 trace_id
grep "ERROR" logs/astrbot.log | awk -F'[]' '{print $2}' | sort | uniq -c
# 输出:
# 3 123456789-1707292800
# 2 987654321-1707292801
# → 表示这两个分析任务各失败了 3 次和 2 次
应用 3:分布式追踪
如果以后扩展到分布式部署,trace_id 可以传递到其他服务:
# 跨服务调用时传递 trace_id
async def call_external_service(url, data):
headers = {"X-Trace-ID": get_trace_id()}
return await http_client.post(url, json=data, headers=headers)