Files

163 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
运行时监控模块
统计各环节耗时、API调用量/token消耗、成功率,运行结束输出统计报表
"""
import time
from collections import defaultdict
from contextlib import contextmanager
from .logger import get_logger
logger = get_logger()
STAGE_ORDER = [
"rss_fetch", "time_filter", "deduplicate", "translate_titles",
"keyword_filter", "translate_contents", "ai_analyze",
"webzine_generate", "category_overview", "report_generate", "image_generate"
]
STAGE_LABELS = {
"rss_fetch": "RSS抓取",
"time_filter": "时间过滤",
"deduplicate": "去重",
"translate_titles": "标题翻译",
"keyword_filter": "关键词筛选",
"translate_contents": "正文翻译",
"ai_analyze": "AI评分分类",
"webzine_generate": "网摘生成",
"category_overview": "分类介绍",
"report_generate": "报告生成",
"image_generate": "图片生成",
}
class RunMonitor:
def __init__(self):
self.start_time = time.time()
self.stage_times = {}
self.stage_active = {}
self.api_calls = []
self.source_success = defaultdict(int)
self.source_failure = defaultdict(int)
self.articles_total = 0
self.articles_cached = 0
self.articles_processed = 0
@contextmanager
def stage(self, name):
"""上下文管理器,自动记录阶段耗时"""
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
self.stage_times[name] = elapsed
def record_api_call(self, model, purpose, max_tokens, success, retries=0):
"""记录一次API调用"""
self.api_calls.append({
"model": model,
"purpose": purpose,
"max_tokens": max_tokens,
"success": success,
"retries": retries,
})
def record_article_stats(self, total, cached, processed):
self.articles_total = total
self.articles_cached = cached
self.articles_processed = processed
def record_source_result(self, source, success):
if success:
self.source_success[source] += 1
else:
self.source_failure[source] += 1
def report(self):
total_elapsed = time.time() - self.start_time
lines = []
lines.append("")
lines.append("=" * 64)
lines.append(" 📊 运行监控报告")
lines.append("=" * 64)
lines.append("")
lines.append("── ⏱ 阶段耗时 ──")
stage_total = 0.0
for key in STAGE_ORDER:
if key in self.stage_times:
t = self.stage_times[key]
stage_total += t
label = STAGE_LABELS.get(key, key)
lines.append(f" {label: <10s} {t:6.1f}s")
lines.append(f" {'─' * 20}")
lines.append(f" {'合计': <10s} {stage_total:6.1f}s / 总运行 {total_elapsed:.0f}s")
lines.append("")
lines.append("── 📡 文章统计 ──")
lines.append(f" 总计: {self.articles_total} 缓存命中: {self.articles_cached} 本次处理: {self.articles_processed}")
if self.articles_total > 0:
cache_rate = self.articles_cached / self.articles_total * 100
lines.append(f" 缓存命中率: {cache_rate:.0f}%")
lines.append("")
lines.append("── 🤖 API调用统计 ──")
if not self.api_calls:
lines.append(" 无API调用(全部命中缓存)")
else:
by_purpose = defaultdict(lambda: {"count": 0, "tokens": 0, "retries": 0, "failed": 0})
by_model = defaultdict(lambda: {"count": 0, "tokens": 0})
for call in self.api_calls:
p = call["purpose"]
by_purpose[p]["count"] += 1
by_purpose[p]["tokens"] += call["max_tokens"]
by_purpose[p]["retries"] += call["retries"]
if not call["success"]:
by_purpose[p]["failed"] += 1
m = call["model"]
by_model[m]["count"] += 1
by_model[m]["tokens"] += call["max_tokens"]
lines.append(" [按用途]")
for purpose, stats in by_purpose.items():
fail_info = f" 失败{stats['failed']}" if stats['failed'] > 0 else ""
lines.append(f" {purpose}: {stats['count']}次 / tokens≈{stats['tokens']} / 重试{stats['retries']}{fail_info}")
lines.append(" [按模型]")
for model, stats in by_model.items():
lines.append(f" {model}: {stats['count']}次 / tokens≈{stats['tokens']}")
lines.append("")
lines.append("── 📰 源抓取统计 ──")
total_sources = len(self.source_success) + len(self.source_failure)
failed_names = [src for src in self.source_failure if self.source_failure[src] > 0]
success_count = total_sources - len(failed_names)
lines.append(f" 源总数: {total_sources} 成功: {success_count} 失败: {len(failed_names)}")
if failed_names:
lines.append(f" 失败源: {', '.join(failed_names)}")
lines.append("")
lines.append("=" * 64)
report_text = "\n".join(lines)
logger.info(report_text)
return report_text
_monitor = None
def get_monitor():
global _monitor
if _monitor is None:
_monitor = RunMonitor()
return _monitor
def reset_monitor():
global _monitor
_monitor = RunMonitor()