From fe6403165257a1c7443bb19aedfa863ffd1902de Mon Sep 17 00:00:00 2001 From: mouxin <494624263qq@gmail.com> Date: Sun, 12 Apr 2026 21:50:01 +0800 Subject: [PATCH] Refine session detail output: indexed IDs, trace fallback, and switch links --- .../skills/stat-cache-hitrate/SKILL.md | 8 +- .../scripts/session_analysis.py | 13 +- .../scripts/stat_cache_hitrate.py | 147 ++++++++++++++---- .../.claude/skills/troubleshoot/SKILL.md | 8 +- .../troubleshoot/scripts/troubleshoot.py | 26 ++-- 5 files changed, 147 insertions(+), 55 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..e07281576a6 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(无 session_id 时回退 trace_id)的命中明细(Markdown 表格),包含 `id序号 / req_count / first_hit / avg_hit(excl_first) / max_hit / min_hit / all_hits / prefill_urls`,并附「序号与会话ID映射」「切换 reqid 明细(可跳转)」。 ### 交叉诊断矩阵 diff --git a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/session_analysis.py b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/session_analysis.py index 355ba8fc947..f7b4caed542 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/session_analysis.py +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/session_analysis.py @@ -7,7 +7,7 @@ def compute_session_details(strategies, strip_scheme): - """按 session 统计命中详情。""" + """按 session_id(优先)或 trace_id(兜底)统计命中详情。""" def _req_id_from_tags(tags, fallback): return tags.get("request_id") or tags.get("req_id") or tags.get("trace_id") or fallback @@ -18,12 +18,14 @@ def _req_id_from_tags(tags, fallback): continue tags = rec.get("tags", {}) or {} session_id = tags.get("session_id") - if not session_id: + trace_id = tags.get("trace_id") + identity = session_id or trace_id + if not identity: continue - session_records[session_id].append((idx, rec)) + session_records[identity].append((idx, rec)) rows = [] - for session_id, items in session_records.items(): + for identity, items in session_records.items(): items.sort(key=lambda x: (x[1].get("ts_ms", ""), x[1].get("ts", ""), x[0])) recs = [r for _, r in items] hits = [int(r.get("selected_hitRatio", 0)) for r in recs] @@ -62,7 +64,8 @@ def _req_id_from_tags(tags, fallback): rows.append( { - "session": session_id, + "session": identity, + "id_type": "session_id" if recs[0].get("tags", {}).get("session_id") else "trace_id", "req_count": len(hits), "first_hit": f"{hits[0]}%", "avg_hit(excl_first)": f"{avg_excl_first}%" if avg_excl_first != "-" else "-", 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..b5adcb9bd5f 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,33 @@ 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] + "…" + + +def _seq_label(n): + return f"S{n:03d}" + # ════════════════════════════════════════════════════════════════ # Phase 1: 日志读取 # ════════════════════════════════════════════════════════════════ @@ -234,7 +261,6 @@ def compute_session_hitrate(stats_recs, inference_count): total_total = sum(r.get("total", 0) for r in stats_recs) session_hr = round(total_hits / total_total * 100, 1) if total_total else 0 - coverage = round(total_total / inference_count * 100, 1) if inference_count else 0 # 趋势:每个窗口的 hits/total trend = time_bucket(stats_recs, "auto", [("hits", "sum"), ("total", "sum")]) @@ -247,7 +273,6 @@ def compute_session_hitrate(stats_recs, inference_count): "rate": session_hr, "hits": total_hits, "total": total_total, - "coverage": coverage, "inference_count": inference_count, "trend": trend, } @@ -430,8 +455,6 @@ def format_full_report(filepath, line_count, prefix_hr, session_hr, per_worker, # 2. Session Hit Rate parts.append("### 2. Session Hit Rate (请求级路由粘性)") parts.append(f' 累计: {session_hr["rate"]}% (hits={session_hr["hits"]} / total={session_hr["total"]})') - parts.append(f' 覆盖率: {session_hr["coverage"]}% 的推理请求带 session_id') - trend_str = _quartile_trend(session_hr["trend"], "value") if trend_str: parts.append(f" 趋势: {trend_str}") @@ -498,10 +521,7 @@ def format_tail_report(filepath, line_count, prefix_hr, session_hr, scheduling): parts.append(f"**File**: {filepath} | **tail {line_count} lines**") parts.append("") parts.append(f' Prefix Hit Ratio: {prefix_hr["mean"]}% (avg) | Cold start: {prefix_hr["cold_start_rate"]}%') - parts.append( - f' Session Hit Rate: {session_hr["rate"]}% (hits={session_hr["hits"]}/total={session_hr["total"]})' - f' | Coverage: {session_hr["coverage"]}%' - ) + parts.append(f' Session Hit Rate: {session_hr["rate"]}% (hits={session_hr["hits"]}/total={session_hr["total"]})') parts.append( f' Strategy: scoring {scheduling["scoring_count"]} ({scheduling["scoring_pct"]}%)' f' | fallback {scheduling["fallback_count"]}' @@ -595,8 +615,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") @@ -639,7 +662,6 @@ def save_detailed_report( parts.append("### Session Hit Rate") parts.append(f'- 累计: **{session_hr["rate"]}%** (hits={session_hr["hits"]}/total={session_hr["total"]})') - parts.append(f'- 覆盖率: **{session_hr["coverage"]}%**') trend_str = _quartile_trend(session_hr["trend"], "value") if trend_str: parts.append(f"- 趋势: {trend_str}") @@ -693,13 +715,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 +739,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,29 +760,101 @@ def save_detailed_report( f' (N={session_summary["non_first_total"]})' ) session_parts.append("") + focus_columns = [ + "id", + "req_count", + "id_type", + "sticky", + "unique_workers", + "avg_hit(excl_first)", + "max_hit", + "min_hit", + "switch_reqids", + ] + 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 = [] + all_rows_with_seq = [] + for i, r in enumerate(session_rows, start=1): + all_rows_with_seq.append({**r, "id": _seq_label(i)}) + + seq_map = {r["session"]: r["id"] for r in all_rows_with_seq} + + for r in prioritized_rows: + sid = seq_map.get(r["session"], "-") + compact_rows.append( + { + "id": sid, + "req_count": r["req_count"], + "id_type": r.get("id_type", "session_id"), + "sticky": r["sticky"], + "unique_workers": r["unique_workers"], + "avg_hit(excl_first)": r["avg_hit(excl_first)"], + "max_hit": r["max_hit"], + "min_hit": r["min_hit"], + "switch_reqids": f"[查看](#switch-{sid.lower()})" if r["switch_req_pairs"] != "-" else "-", + } + ) + session_parts.append( + _render_markdown_table(compact_rows, focus_columns, align_right={"req_count", "unique_workers"}) + ) + session_parts.append("") + session_columns = [ - "session", + "id", "req_count", + "id_type", "first_hit", "avg_hit(excl_first)", "max_hit", "min_hit", "all_hits", "prefill_urls", - "switch_req_pairs", - "sharp_drop_request_ids", "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( + all_rows_with_seq, + session_columns, + align_right={"req_count", "unique_workers"}, + ) + ) + session_parts.append("") + + session_parts.append("## 序号与会话ID映射") + map_rows = [ + { + "id": r["id"], + "id_type": r.get("id_type", "session_id"), + "session_or_trace_id": r["session"], + } + for r in all_rows_with_seq + ] + session_parts.append(_render_markdown_table(map_rows, ["id", "id_type", "session_or_trace_id"])) session_parts.append("") + session_parts.append("## 切换 reqid 明细(可跳转)") + for r in all_rows_with_seq: + session_parts.append(f'### switch-{r["id"].lower()}') + session_parts.append(f'- ID: **{r["id"]}**') + session_parts.append(f'- 会话标识: `{r["session"]}` ({r.get("id_type", "session_id")})') + session_parts.append(f'- switch_req_pairs: {r["switch_req_pairs"]}') + session_parts.append(f'- sharp_drop_request_ids: {r["sharp_drop_request_ids"]}') + 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 +977,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 +1001,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)