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 @@ -50,7 +50,7 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析

### 3. 输出目录
分析结果默认保存到 `skill_output/stat-cache-hitrate/<YYYYMMDD_HHMMSS>/`(自动按运行时间创建子目录)。
用户可通过 `--output` 指定自定义目录
用户可通过 `--output` 指定**基目录**,脚本会继续在其下创建 `<YYYYMMDD_HHMMSS>/summary` 与 `<YYYYMMDD_HHMMSS>/detail`,避免覆盖历史明细

## 使用方式

Expand Down Expand Up @@ -92,9 +92,9 @@ python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志

详细报告和图表输出到 `skill_output/stat-cache-hitrate/<YYYYMMDD_HHMMSS>/` 目录,每次运行自动创建带时间戳的子目录。

- 主报告 `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`

### 交叉诊断矩阵

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", "<br>").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: 日志读取
# ════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 行格式合并展示(起始/合并说明/结束)。"
Expand All @@ -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 命中详情", ""]
Expand All @@ -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",
Expand All @@ -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))

Expand Down Expand Up @@ -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,
Expand All @@ -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}")
Expand Down
8 changes: 4 additions & 4 deletions fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ IMPORTANT: 执行前务必先读取 references/log_patterns.md 了解日志格

### 4. 输出目录
诊断报告默认保存到 `skill_output/troubleshoot/<YYYYMMDD_HHMMSS>/`(自动按运行时间创建子目录)。
用户可通过 `--output` 指定自定义目录
用户可通过 `--output` 指定**基目录**,脚本会继续在其下创建 `<YYYYMMDD_HHMMSS>/summary` 与 `<YYYYMMDD_HHMMSS>/detail`,避免覆盖历史明细

## 用法

Expand Down Expand Up @@ -107,9 +107,9 @@ python3 $SCRIPTS/troubleshoot.py <log_file> --start "16:00" --end "17:00" --erro
## 输出

- **终端**:简洁三层汇总(Router / FD 后端 / 客户端),含状态码分布、错误 Top N、趋势图
- **文件**:详细报告导出到 `skill_output/troubleshoot/<YYYYMMDD_HHMMSS>/troubleshoot_report_<timestamp>.md`
- 逐分钟事件详情拆分到 `details/health_events.md`
- 请求追踪事件链拆分到 `details/trace_<ID>.md`
- **文件**:详细报告导出到 `skill_output/troubleshoot/<YYYYMMDD_HHMMSS>/summary/troubleshoot_report.md`
- 逐分钟事件详情拆分到 `detail/health_events.md`
- 请求追踪事件链拆分到 `detail/trace_<ID>.md`
- **状态行**:`STATUS: HEALTHY / DEGRADED / CRITICAL`

## 三层诊断框架
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
Loading