From 5791a801135df0ff05a497b4f5824b34c78bbb20 Mon Sep 17 00:00:00 2001 From: mouxin <494624263qq@gmail.com> Date: Sun, 12 Apr 2026 21:34:37 +0800 Subject: [PATCH] Improve skill reports: markdown session tables and timestamped output layout --- .../skills/stat-cache-hitrate/SKILL.md | 8 +- .../scripts/stat_cache_hitrate.py | 96 +++++++++++++++---- .../.claude/skills/troubleshoot/SKILL.md | 8 +- .../troubleshoot/scripts/troubleshoot.py | 26 +++-- 4 files changed, 100 insertions(+), 38 deletions(-) diff --git a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md index e7925127dec..150a15a4dd7 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md @@ -50,7 +50,7 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析 ### 3. 输出目录 分析结果默认保存到 `skill_output/stat-cache-hitrate//`(自动按运行时间创建子目录)。 -用户可通过 `--output` 指定自定义目录。 +用户可通过 `--output` 指定**基目录**,脚本会继续在其下创建 `/summary` 与 `/detail`,避免覆盖历史明细。 ## 使用方式 @@ -92,9 +92,9 @@ python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志 详细报告和图表输出到 `skill_output/stat-cache-hitrate//` 目录,每次运行自动创建带时间戳的子目录。 -- 主报告 `cache_hitrate_report_*.md` — Per-Worker 统计 + Fallback 明细 -- `details/per_window_data.md` — 每5s窗口明细(连续空窗口自动合并为 3 行:起始/合并说明/结束) -- `details/session_hit_details.md` — 每个 session 的命中明细(TSV 单行格式,便于横向滚动查看),包含 `session / req_count / first_hit / avg_hit(excl_first) / max_hit / min_hit / all_hits / prefill_urls / switch_req_pairs / sharp_drop_request_ids` +- `summary/cache_hitrate_report.md` — Per-Worker 统计 + Fallback 明细 + 详情链接 +- `detail/per_window_data.md` — 每5s窗口明细(连续空窗口自动合并为 3 行:起始/合并说明/结束) +- `detail/session_hit_details.md` — 每个 session 的命中明细(Markdown 表格),包含 `session / req_count / first_hit / avg_hit(excl_first) / max_hit / min_hit / all_hits / prefill_urls / switch_req_pairs / sharp_drop_request_ids` ### 交叉诊断矩阵 diff --git a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py index 1476e61d724..c338e46030b 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py @@ -70,6 +70,29 @@ def _escape(v): lines.append("\t".join(_escape(row.get(col, "")) for col in columns)) return "```tsv\n" + "\n".join(lines) + "\n```" + +def _render_markdown_table(data, columns, align_right=None): + """渲染 Markdown 表格,便于在终端/文档中直接阅读。""" + if not data: + return "_(no data)_" + + align_right = align_right or set() + + def _escape_md(v): + return str(v).replace("\n", "
").replace("|", "\\|") + + header = "| " + " | ".join(columns) + " |" + align = "| " + " | ".join("---:" if c in align_right else "---" for c in columns) + " |" + rows = [] + for row in data: + rows.append("| " + " | ".join(_escape_md(row.get(c, "")) for c in columns) + " |") + return "\n".join([header, align] + rows) + + +def _truncate_text(v, limit=72): + s = str(v) + return s if len(s) <= limit else s[: limit - 1] + "…" + # ════════════════════════════════════════════════════════════════ # Phase 1: 日志读取 # ════════════════════════════════════════════════════════════════ @@ -595,8 +618,11 @@ def save_detailed_report( 主报告包含 Per-Worker 统计和 Fallback 明细。 每窗口明细数据拆分到 details/per_window_data.md。 """ - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - output_path = os.path.join(output_dir, f"cache_hitrate_report_{timestamp}.md") + summary_dir = os.path.join(output_dir, "summary") + details_dir = os.path.join(output_dir, "detail") + os.makedirs(summary_dir, exist_ok=True) + os.makedirs(details_dir, exist_ok=True) + output_path = os.path.join(summary_dir, "cache_hitrate_report.md") parts = [] parts.append("# Cache Hit Rate Detailed Report") @@ -693,13 +719,11 @@ def save_detailed_report( # 主报告中添加引用 parts.append( f"> 每5s窗口明细数据(原始 {len(window_rows)} 条,合并后 {len(window_rows_merged)} 条):" - " [details/per_window_data.md](details/per_window_data.md)" + " [../detail/per_window_data.md](../detail/per_window_data.md)" ) parts.append("") # 写入 details 子目录 - details_dir = os.path.join(output_dir, "details") - os.makedirs(details_dir, exist_ok=True) detail_parts = ["# 每5s窗口明细数据", ""] detail_parts.append( "> 注:连续空窗口(Prefix/Session 都为空、且 Scoring/Fallback=0)已按 3 行格式合并展示(起始/合并说明/结束)。" @@ -719,9 +743,7 @@ def save_detailed_report( f.write("\n".join(detail_parts)) if session_rows: - parts.append( - f"> Session 命中详情 ({len(session_rows)} sessions): [details/session_hit_details.md](details/session_hit_details.md)" - ) + parts.append(f"> Session 命中详情 ({len(session_rows)} sessions): [../detail/session_hit_details.md](../detail/session_hit_details.md)") parts.append("") session_parts = ["# Session 命中详情", ""] @@ -742,6 +764,42 @@ def save_detailed_report( f' (N={session_summary["non_first_total"]})' ) session_parts.append("") + focus_columns = [ + "session", + "req_count", + "sticky", + "unique_workers", + "avg_hit(excl_first)", + "min_hit", + "switch_req_pairs", + "sharp_drop_request_ids", + ] + session_parts.append("## 优先排查 Session(Top 20)") + prioritized_rows = sorted( + session_rows, + key=lambda r: ( + 0 if r.get("sticky") == "no" else 1, + int(str(r.get("min_hit", "0")).rstrip("%") or 0), + -int(r.get("req_count", 0)), + ), + )[:20] + compact_rows = [] + for r in prioritized_rows: + compact_rows.append( + { + "session": r["session"], + "req_count": r["req_count"], + "sticky": r["sticky"], + "unique_workers": r["unique_workers"], + "avg_hit(excl_first)": r["avg_hit(excl_first)"], + "min_hit": r["min_hit"], + "switch_req_pairs": _truncate_text(r["switch_req_pairs"]), + "sharp_drop_request_ids": _truncate_text(r["sharp_drop_request_ids"]), + } + ) + session_parts.append(_render_markdown_table(compact_rows, focus_columns, align_right={"req_count", "unique_workers"})) + session_parts.append("") + session_columns = [ "session", "req_count", @@ -756,15 +814,20 @@ def save_detailed_report( "sticky", "unique_workers", ] - session_parts.append("## 明细(单行 TSV,可横向滚动)") - session_parts.append(_render_scrollable_tsv(session_rows, session_columns)) + session_parts.append("## 全量明细(Markdown 表格)") + session_parts.append( + _render_markdown_table( + session_rows, + session_columns, + align_right={"req_count", "unique_workers"}, + ) + ) session_parts.append("") session_path = os.path.join(details_dir, "session_hit_details.md") with open(session_path, "w") as f: f.write("\n".join(session_parts)) - os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) with open(output_path, "w") as f: f.write("\n".join(parts)) @@ -887,13 +950,14 @@ def main(): ) # 导出详细报告 + run_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") if args.output: - output_dir = args.output + output_base = args.output else: script_dir = os.path.dirname(os.path.abspath(__file__)) golang_router_root = os.path.normpath(os.path.join(script_dir, "..", "..", "..", "..")) - run_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - output_dir = os.path.join(golang_router_root, "skill_output", "stat-cache-hitrate", run_timestamp) + output_base = os.path.join(golang_router_root, "skill_output", "stat-cache-hitrate") + output_dir = os.path.join(output_base, run_timestamp) report_path = save_detailed_report( args.log_file, strategy_recs, @@ -910,12 +974,12 @@ def main(): report_abs, report_uri = _build_path_links(report_path) print(f" - 报告文件: {report_abs}") print(f" URI: {report_uri}") - details_path = os.path.join(os.path.dirname(report_path), "details", "per_window_data.md") + details_path = os.path.join(output_dir, "detail", "per_window_data.md") if os.path.exists(details_path): details_abs, details_uri = _build_path_links(details_path) print(f" - 窗口明细: {details_abs}") print(f" URI: {details_uri}") - session_detail_path = os.path.join(os.path.dirname(report_path), "details", "session_hit_details.md") + session_detail_path = os.path.join(output_dir, "detail", "session_hit_details.md") if os.path.exists(session_detail_path): session_abs, session_uri = _build_path_links(session_detail_path) print(f" - Session 明细: {session_abs}") diff --git a/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md b/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md index ab0c3ce7219..43ee91a46b1 100644 --- a/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md +++ b/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md @@ -64,7 +64,7 @@ IMPORTANT: 执行前务必先读取 references/log_patterns.md 了解日志格 ### 4. 输出目录 诊断报告默认保存到 `skill_output/troubleshoot//`(自动按运行时间创建子目录)。 -用户可通过 `--output` 指定自定义目录。 +用户可通过 `--output` 指定**基目录**,脚本会继续在其下创建 `/summary` 与 `/detail`,避免覆盖历史明细。 ## 用法 @@ -107,9 +107,9 @@ python3 $SCRIPTS/troubleshoot.py --start "16:00" --end "17:00" --erro ## 输出 - **终端**:简洁三层汇总(Router / FD 后端 / 客户端),含状态码分布、错误 Top N、趋势图 -- **文件**:详细报告导出到 `skill_output/troubleshoot//troubleshoot_report_.md` - - 逐分钟事件详情拆分到 `details/health_events.md` - - 请求追踪事件链拆分到 `details/trace_.md` +- **文件**:详细报告导出到 `skill_output/troubleshoot//summary/troubleshoot_report.md` + - 逐分钟事件详情拆分到 `detail/health_events.md` + - 请求追踪事件链拆分到 `detail/trace_.md` - **状态行**:`STATUS: HEALTHY / DEGRADED / CRITICAL` ## 三层诊断框架 diff --git a/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py b/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py index 5096c5b294a..30b9df0f443 100644 --- a/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py +++ b/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py @@ -190,30 +190,27 @@ def save_detailed_report(report_text, output_dir, details=None): output_dir: 输出目录 details: 详情数据 dict(来自 format_full_report) """ - os.makedirs(output_dir, exist_ok=True) - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"troubleshoot_report_{timestamp}.md" - filepath = os.path.join(output_dir, filename) + summary_dir = os.path.join(output_dir, "summary") + detail_dir = os.path.join(output_dir, "detail") + os.makedirs(summary_dir, exist_ok=True) + os.makedirs(detail_dir, exist_ok=True) + filepath = os.path.join(summary_dir, "troubleshoot_report.md") with open(filepath, "w", encoding="utf-8") as f: f.write("# Router Troubleshooting Report\n") f.write(f'> Generated at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n\n') f.write(report_text) - # 保存详情到 details/ 子目录 + # 保存详情到 detail/ 子目录 if details: - details_dir = os.path.join(output_dir, "details") - if details.get("health_events"): - os.makedirs(details_dir, exist_ok=True) - health_path = os.path.join(details_dir, "health_events.md") + health_path = os.path.join(detail_dir, "health_events.md") with open(health_path, "w", encoding="utf-8") as f: f.write(details["health_events"]) for trace_id, trace_text in details.get("trace_files", {}).items(): - os.makedirs(details_dir, exist_ok=True) safe_id = trace_id.replace("/", "_") - trace_path = os.path.join(details_dir, f"trace_{safe_id}.md") + trace_path = os.path.join(detail_dir, f"trace_{safe_id}.md") with open(trace_path, "w", encoding="utf-8") as f: f.write(trace_text) @@ -327,13 +324,14 @@ def main(): print(report) # 保存详细报告 + run_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") if args.output: - output_dir = args.output + output_base = args.output else: script_dir = os.path.dirname(os.path.abspath(__file__)) golang_router_root = os.path.normpath(os.path.join(script_dir, "..", "..", "..", "..")) - run_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - output_dir = os.path.join(golang_router_root, "skill_output", "troubleshoot", run_timestamp) + output_base = os.path.join(golang_router_root, "skill_output", "troubleshoot") + output_dir = os.path.join(output_base, run_timestamp) filepath = save_detailed_report(report, output_dir, details=details) print(f"\n详细报告已保存到: {filepath}", file=sys.stderr)