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 e07281576a6..251cbb04c2a 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md @@ -4,7 +4,7 @@ description: > 统计 FastDeploy Go Router 日志中的三层 cache 命中率指标,生成可视化报告。 三层指标:Prefix Hit Ratio(KV Cache 内容复用度)、Session Hit Rate(请求级路由粘性)、 Per-Worker Cache Stats(各 prefill worker 的缓存利用排名)。支持全量统计、tail 快速查看、 - 持续监控模式。 + 持续监控模式、指定时间段统计(--start/--end)。 当用户提到以下内容时触发此 skill:统计/查看 cache 命中率、查看 cache-aware 调度效果、 查看缓存预热情况、统计 hitRatio、查看 prefix 命中率、session hit rate。 @@ -35,12 +35,15 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析 如果用户直接确认或未指定路径,使用默认值 `logs/router.log`。 ### 2. 分析模式 -向用户询问分析模式: -> "请选择分析模式: -> 1. **全量统计**(默认)— 扫描完整日志 -> 2. **快速查看尾部** — 只看最近的数据(可指定行数如 2000 或时间如 30m) -> 3. **持续监控** — 全量分析后提示监控命令 -> 4. **指定时间段** — 分析特定时间范围(如 `--start "16:00" --end "17:00"`)" +必须使用 **AskUserQuestion 的离散选项**(不要只发纯文本编号,避免客户端偶发不显示第 4 项): +- 选项 1: `全量统计(默认)` — 扫描完整日志 +- 选项 2: `快速查看尾部` — 只看最近的数据(可指定行数如 2000 或时间如 30m) +- 选项 3: `持续监控` — 全量分析后提示监控命令 +- 选项 4: `指定时间段` — 分析特定时间范围(如 `--start "16:00" --end "17:00"`) + +若用户选择“指定时间段”,直接让用户填写: +- 从 `xxx` 开始,到 `xxx` 结束(`start/end` 可只填一个); +- 然后映射为 `--start/--end` 参数执行。 如果用户未选择,默认使用全量统计。 @@ -94,7 +97,7 @@ python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志 - `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 明细(可跳转)」。 +- `detail/session_hit_details.md` — 每个 session(无 session_id 时回退 trace_id)的命中明细(Markdown 表格),包含 `id序号 / req_count / first_hit / avg-hit(=去首请求平均命中率) / max_hit / min_hit / all_hits / purl_cnt / prefill_urls`,并附「序号与会话ID映射」「切换 reqid 明细(含 session 时间段,可跳转)」。 ### 交叉诊断矩阵 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 f7b4caed542..7de5b7f6042 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 @@ -66,6 +66,8 @@ def _req_id_from_tags(tags, fallback): { "session": identity, "id_type": "session_id" if recs[0].get("tags", {}).get("session_id") else "trace_id", + "first_ts": recs[0].get("ts", "-"), + "last_ts": recs[-1].get("ts", "-"), "req_count": len(hits), "first_hit": f"{hits[0]}%", "avg_hit(excl_first)": f"{avg_excl_first}%" if avg_excl_first != "-" else "-", @@ -74,6 +76,7 @@ def _req_id_from_tags(tags, fallback): "all_hits": ", ".join(f"{h}%" for h in hits), "sticky": "yes" if len(workers) <= 1 else "no", "unique_workers": len(workers), + "prefill_url_count": len(prefill_urls), "prefill_urls": " | ".join(strip_scheme(u) for u in prefill_urls), "switch_req_pairs": " ; ".join(switch_events) if switch_events else "-", "sharp_drop_request_ids": " ; ".join(sharp_drop_req_ids) if sharp_drop_req_ids 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 bd055393637..fb6b45b56fa 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 @@ -134,20 +134,25 @@ def _summarize_id_type_ranges(rows_with_seq): current_type = rows_with_seq[0].get("id_type", "session_id") start_id = rows_with_seq[0]["id"] end_id = start_id + start_ts = rows_with_seq[0].get("first_ts", "-") + end_ts = rows_with_seq[0].get("last_ts", "-") for row in rows_with_seq[1:]: row_type = row.get("id_type", "session_id") row_id = row["id"] if row_type == current_type and _extract_seq_num(row_id) == _extract_seq_num(end_id) + 1: end_id = row_id + end_ts = row.get("last_ts", end_ts) continue - ranges.append((start_id, end_id, current_type)) + ranges.append((start_id, end_id, current_type, start_ts, end_ts)) current_type = row_type start_id = row_id end_id = row_id + start_ts = row.get("first_ts", "-") + end_ts = row.get("last_ts", "-") - ranges.append((start_id, end_id, current_type)) + ranges.append((start_id, end_id, current_type, start_ts, end_ts)) return ranges # ════════════════════════════════════════════════════════════════ @@ -800,23 +805,31 @@ def save_detailed_report( all_rows_with_seq.append({**r, "id": _seq_label(i)}) id_type_ranges = _summarize_id_type_ranges(all_rows_with_seq) seq_map = {r["session"]: r["id"] for r in all_rows_with_seq} + ts_starts = [r.get("first_ts", "-") for r in all_rows_with_seq if r.get("first_ts", "-") != "-"] + ts_ends = [r.get("last_ts", "-") for r in all_rows_with_seq if r.get("last_ts", "-") != "-"] session_parts = ["# Session 命中详情", ""] + overall_start_ts = min(ts_starts) if ts_starts else "-" + overall_end_ts = max(ts_ends) if ts_ends else "-" + session_parts.append("## 时间范围") + session_parts.append(f"- 分析覆盖时间段: `{overall_start_ts} ~ {overall_end_ts}`") + session_parts.append("") session_parts.append("## id_type 摘要") if len(id_type_ranges) == 1: - start_id, end_id, id_type = id_type_ranges[0] + start_id, end_id, id_type, range_start_ts, range_end_ts = id_type_ranges[0] if start_id == end_id: - session_parts.append(f"- `{start_id}`: `{id_type}`") + session_parts.append(f"- `{start_id}`: `{id_type}` (`{range_start_ts} ~ {range_end_ts}`)") else: - session_parts.append(f"- `{start_id}~{end_id}`: `{id_type}`") + session_parts.append(f"- `{start_id}~{end_id}`: `{id_type}` (`{range_start_ts} ~ {range_end_ts}`)") else: - for start_id, end_id, id_type in id_type_ranges: + for start_id, end_id, id_type, range_start_ts, range_end_ts in id_type_ranges: if start_id == end_id: - session_parts.append(f"- `{start_id}`: `{id_type}`") + session_parts.append(f"- `{start_id}`: `{id_type}` (`{range_start_ts} ~ {range_end_ts}`)") else: - session_parts.append(f"- `{start_id}~{end_id}`: `{id_type}`") + session_parts.append(f"- `{start_id}~{end_id}`: `{id_type}` (`{range_start_ts} ~ {range_end_ts}`)") session_parts.append("") session_parts.append("## 概览") + session_parts.append("- 字段说明:`avg-hit` = `avg_hit(excl_first)`(去除首请求后的平均命中率)") session_parts.append(f'- Total sessions: **{session_summary["total_sessions"]}**') session_parts.append( f'- Sessions with >1 request: **{session_summary["multi_req"]}**' @@ -836,10 +849,9 @@ def save_detailed_report( focus_columns = [ "id", "req_count", - "id_type", "sticky", - "unique_workers", - "avg_hit(excl_first)", + "purl_cnt", + "avg-hit", "max_hit", "min_hit", "switch_reqids", @@ -861,39 +873,46 @@ def save_detailed_report( { "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)"], + "purl_cnt": r.get("prefill_url_count", 0), + "avg-hit": 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"}) + _render_markdown_table(compact_rows, focus_columns, align_right={"req_count", "purl_cnt"}) ) session_parts.append("") session_columns = [ "id", "req_count", - "id_type", "first_hit", - "avg_hit(excl_first)", + "avg-hit", "max_hit", "min_hit", "all_hits", + "purl_cnt", "prefill_urls", "sticky", - "unique_workers", ] + all_rows_for_table = [] + for r in all_rows_with_seq: + all_rows_for_table.append( + { + **r, + "avg-hit": r["avg_hit(excl_first)"], + "purl_cnt": r.get("prefill_url_count", 0), + } + ) session_parts.append("## 全量明细(Markdown 表格)") session_parts.append( _render_markdown_table( - all_rows_with_seq, + all_rows_for_table, session_columns, - align_right={"req_count", "unique_workers"}, + align_right={"req_count", "purl_cnt"}, ) ) session_parts.append("") @@ -902,12 +921,11 @@ def save_detailed_report( 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(_render_markdown_table(map_rows, ["id", "session_or_trace_id"])) session_parts.append("") session_parts.append("## 切换 reqid 明细(可跳转)") @@ -915,6 +933,7 @@ def save_detailed_report( 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'- 时间段: `{r.get("first_ts", "-")} ~ {r.get("last_ts", "-")}`') 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("")