# -*- 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()