Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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。
Expand Down Expand Up @@ -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` 参数执行。

如果用户未选择,默认使用全量统计。

Expand Down Expand Up @@ -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 时间段,可跳转)」。

### 交叉诊断矩阵

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 "-",
Expand All @@ -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 "-",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -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"]}**'
Expand All @@ -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",
Expand All @@ -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("")
Expand All @@ -902,19 +921,19 @@ 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 明细(可跳转)")
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'- 时间段: `{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("")
Expand Down
Loading