163 lines
5.5 KiB
Python
163 lines
5.5 KiB
Python
# -*- 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()
|