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 3d52bc4b2be..ad9b3f29fd2 100644 --- a/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md +++ b/fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md @@ -37,7 +37,7 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析 ### 2. 分析模式 必须使用 **AskUserQuestion 的离散选项**(不要只发纯文本编号,避免客户端偶发不显示第 4 项): - 选项 1: `全量统计(默认)` — 扫描完整日志 -- 选项 2: `快速查看尾部` — 只看最近的数据(可指定 `2000/2k` 行,或 `30m/2h/1d` 时间窗口) +- 选项 2: `快速查看尾部` — 只看最近的数据(支持 `2000`、`1k`、`1w` 等行数写法) - 选项 3: `指定时间段` — 分析特定时间范围(如 `--start "16:00" --end "17:00"`) 若用户选择“指定时间段”,直接让用户填写: @@ -47,6 +47,7 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析 如果用户未选择,默认使用全量统计。 `--start/--end` 与 `--tail` 互斥。`--start` 和 `--end` 可单独或同时指定。 +`--tail` 仅支持“行数”语义(如 `2000`,也兼容 `1k/1w` 自动换算),不再支持 `30m/2h/1d` 这类时间窗口;按时间请使用 `--start/--end`。 时间格式灵活:支持 `YYYY/MM/DD HH:MM:SS`、`HH:MM:SS`、`HH:MM`、`MM/DD`、`MM/DD HH:MM`。 缺失部分自动从日志首末行推断。 @@ -65,12 +66,8 @@ python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志 # 快速查看尾部数据 python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail # 默认最后 2000 行 python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 5000 # 指定行数 -python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 2k # 行数缩写 -python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 30m # 分钟窗口 -python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 2h # 小时窗口 -python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 1d # 天窗口 - -# 指定时间段(--start 和 --end 可单独或同时使用) +python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 1k # 行数缩写(自动换算) +# 指定时间段(需要按时间筛选时使用;--start 和 --end 可单独或同时使用) python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --start "16:00:00" --end "17:00:00" python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --start "2026/03/31 16:00:00" python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --start "03/31" --end "03/31 18:00" 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 fc729af47c7..1e27f96a476 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 @@ -8,12 +8,11 @@ 3. Per-Worker Stats — 各 worker 缓存利用排名 用法: - python3 stat_cache_hitrate.py [--tail N|2k|30m|2h|1d] [--output DIR] + python3 stat_cache_hitrate.py [--tail N|Nk|Nw] [--output DIR] """ import argparse import json -import math import os import re import subprocess @@ -28,7 +27,6 @@ from chart import render_bar, render_sparkline, render_table from log_parser import ( complete_time_arg, - extract_ts, filter_file_by_time_range, parse_cache_strategy_line, parse_stats_line, @@ -172,16 +170,10 @@ def count_lines(filepath): def read_lines(filepath, tail=None): """读取日志文件,支持 tail 模式。""" if tail is not None: - if isinstance(tail, str) and tail.endswith("m"): - # 按时间 tail:读取全部行,过滤最近 N 分钟 - minutes = int(tail[:-1]) - all_lines = _read_file_lines(filepath) - return _filter_by_time(all_lines, minutes) - else: - # 按行数 tail - n = int(tail) - result = subprocess.run(["tail", "-n", str(n), filepath], capture_output=True, text=True) - return result.stdout.splitlines() if result.returncode == 0 else [] + # 按行数 tail + n = int(tail) + result = subprocess.run(["tail", "-n", str(n), filepath], capture_output=True, text=True) + return result.stdout.splitlines() if result.returncode == 0 else [] return _read_file_lines(filepath) @@ -190,35 +182,6 @@ def _read_file_lines(filepath): return f.readlines() -def _filter_by_time(lines, minutes): - """过滤最近 N 分钟的日志行。""" - # 找最后一行的时间戳作为基准 - last_ts = None - for line in reversed(lines): - ts = extract_ts(line) - if ts: - last_ts = parse_ts(ts) - break - if not last_ts: - return lines - - from datetime import timedelta - - cutoff = last_ts - timedelta(minutes=minutes) - result = [] - for line in lines: - ts = extract_ts(line) - if ts: - try: - if parse_ts(ts) >= cutoff: - result.append(line) - except ValueError: - result.append(line) - else: - result.append(line) - return result - - # ════════════════════════════════════════════════════════════════ # Phase 2: 日志提取与解析 # ════════════════════════════════════════════════════════════════ @@ -237,7 +200,7 @@ def grep_and_parse(filepath, grep_pattern, parse_cmd, tail=None): """大文件模式:grep 过滤 + log_parser.py CLI 管道解析。""" parser_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "log_parser.py") - if tail and not (isinstance(tail, str) and tail.endswith("m")): + if tail: grep_cmd = f"tail -n {tail} {_shell_quote(filepath)} | grep -F {_shell_quote(grep_pattern)} | python3 {_shell_quote(parser_path)} {parse_cmd}" else: grep_cmd = f"grep -F {_shell_quote(grep_pattern)} {_shell_quote(filepath)} | python3 {_shell_quote(parser_path)} {parse_cmd}" @@ -255,7 +218,7 @@ def grep_and_parse(filepath, grep_pattern, parse_cmd, tail=None): def grep_count(filepath, grep_pattern, tail=None): """大文件模式:grep 计数。""" - if tail and not (isinstance(tail, str) and tail.endswith("m")): + if tail: cmd = f"tail -n {tail} {_shell_quote(filepath)} | grep -cE {_shell_quote(grep_pattern)}" else: cmd = f"grep -cE {_shell_quote(grep_pattern)} {_shell_quote(filepath)}" @@ -283,7 +246,7 @@ def extract_data(filepath, tail=None): strategy_recs = grep_and_parse(filepath, STRATEGY_PATTERN, "parse-cache-strategy", tail) stats_recs = grep_and_parse(filepath, STATS_PATTERN, "parse-stats", tail) inference_count = grep_count(filepath, r"\] \[POST\] /v1/chat/completions |\] \[POST\] /v1/completions ", tail) - line_count = int(tail) if tail is not None and not (isinstance(tail, str) and tail.endswith("m")) else total + line_count = int(tail) if tail is not None else total return strategy_recs, stats_recs, inference_count, line_count @@ -989,7 +952,7 @@ def parse_args(): "--tail", nargs="?", const="2000", - help="只分析尾部数据(支持 2000/2k 行,或 30m/2h/1d 时间窗口)", + help="只分析尾部数据(支持 2000、1k、1w 等行数写法)。按时间请使用 --start/--end", ) parser.add_argument( "--output", default=None, help="详细报告输出目录(默认:skill_output/stat-cache-hitrate//)" @@ -1002,7 +965,7 @@ def parse_args(): def parse_tail_arg(tail_str): - """解析 --tail 参数,返回 int(行数) 或 'm'(时间窗口)。""" + """解析 --tail 参数,返回行数 int。支持数字及 k/w 缩写。""" if tail_str is None: return None @@ -1010,34 +973,20 @@ def parse_tail_arg(tail_str): if not s: raise ValueError("--tail 不能为空") - # 行数: 2000 - if re.fullmatch(r"\d+", s): - value = int(s) - if value <= 0: - raise ValueError("--tail 行数必须 > 0") - return value - - # 行数缩写: 2k => 2000 - m = re.fullmatch(r"(\d+)k", s) - if m: - value = int(m.group(1)) * 1000 - if value <= 0: - raise ValueError("--tail 行数必须 > 0") - return value - - # 时间窗口: 30m/2h/1d(最终统一成分钟) - m = re.fullmatch(r"(\d+)(m|h|d)", s) - if m: - num = int(m.group(1)) - unit = m.group(2) - if num <= 0: - raise ValueError("--tail 时间窗口必须 > 0") - factor = {"m": 1, "h": 60, "d": 1440}[unit] - minutes = num * factor - minutes = max(1, math.ceil(minutes)) - return f"{minutes}m" - - raise ValueError("不支持的 --tail 格式:请使用 2000/2k 或 30m/2h/1d") + m = re.fullmatch(r"(\d+)([kw])?", s) + if not m: + raise ValueError("不支持的 --tail 格式:请使用 2000、1k、1w 等行数写法。按时间请改用 --start/--end") + + value = int(m.group(1)) + unit = m.group(2) + if unit == "k": + value *= 1000 + elif unit == "w": + value *= 10000 + + if value <= 0: + raise ValueError("--tail 行数必须 > 0") + return value def main(): diff --git a/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md b/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md index 00c94a2f487..ecb27c1436a 100644 --- a/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md +++ b/fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md @@ -38,7 +38,7 @@ description: > ### 2. 分析范围 必须使用 **AskUserQuestion 的离散选项**(不要只发纯文本编号): - 选项 1: `全量分析(默认)` — 分析整个日志文件 -- 选项 2: `尾部分析` — 只分析最近数据(可指定行数或时间如 `--tail 5000` 或 `--tail 30m`) +- 选项 2: `尾部分析` — 只分析最近数据(仅支持行数,如 `--tail 5000`) - 选项 3: `指定时间段` — 分析特定时间范围内的日志 如果用户未选择,默认使用全量分析。 @@ -50,10 +50,11 @@ description: > 时间格式灵活:支持 `YYYY/MM/DD HH:MM:SS`、`HH:MM:SS`、`HH:MM`、`MM/DD`、`MM/DD HH:MM`。 缺失部分自动从日志首末行推断(缺年份取首行,缺日期取末行)。 `--start/--end` 与 `--tail` 互斥。 +`--tail` 仅支持“行数”语义(如 `5000`,也兼容 `1k/1w` 自动换算),不再支持 `30m` 这类时间写法;凡是按时间筛选都使用 `--start/--end`。 当用户选择“指定时间段”时,必须再发起一次 **AskUserQuestion**(离散选项)引导时间输入: - 选项 1: `当天(00:00:00 到当前)`(推荐) -- 选项 2: `最近半小时`(自动换算为 `--start now-30m --end now` 语义) +- 选项 2: `自定义时间段`(由用户直接输入起止时间) 用户若通过客户端默认 `Other` 输入时间,则将该输入直接作为时间范围参数解析。 可补充一条简短示例引导: @@ -104,9 +105,7 @@ python3 $SCRIPTS/troubleshoot.py --trace all # 尾部分析 python3 $SCRIPTS/troubleshoot.py --tail 5000 -python3 $SCRIPTS/troubleshoot.py --tail 30m - -# 指定时间段(--start 和 --end 可单独或同时使用) +# 指定时间段(需要按时间筛选时使用;--start 和 --end 可单独或同时使用) python3 $SCRIPTS/troubleshoot.py --start "16:00:00" --end "17:00:00" python3 $SCRIPTS/troubleshoot.py --start "2026/03/31 16:00:00" python3 $SCRIPTS/troubleshoot.py --start "03/31" --end "03/31 18:00" diff --git a/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py b/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py index 251a21c7e81..b00521e6b01 100644 --- a/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py +++ b/fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py @@ -12,7 +12,7 @@ --cache 仅分析 Cache 调度 --load 仅分析负载与计数器 --trace ID 追踪指定请求(支持逗号分隔多 ID;传 all 可全量追踪) - --tail N 仅分析尾部 N 行(支持 N 或 Nm 格式如 30m) + --tail N 仅分析尾部 N 行(支持 5000/1k/1w 等行数写法) --start TIME 起始时间(如 "16:00:00"、"03/31 16:00") --end TIME 结束时间(如 "17:00:00"、"2026/03/31 17:00:00") --output DIR 详细报告导出目录(默认: skill_output/troubleshoot//) @@ -21,6 +21,7 @@ """ import argparse +import re import os import sys from datetime import datetime @@ -38,7 +39,6 @@ from analyzers.trace import analyze_trace, format_trace_report from log_parser import ( complete_time_arg, - filter_file_by_recent_minutes, filter_file_by_time_range, ) @@ -106,12 +106,22 @@ def determine_log_file(user_path=None): def parse_tail_arg(tail_str): - """解析 --tail 参数:支持纯数字(行数)或 Nm(分钟)格式。""" + """解析 --tail 参数:支持数字及 k/w 缩写。""" if tail_str is None: return None - if tail_str.endswith("m"): - return {"type": "minutes", "value": int(tail_str[:-1])} - return {"type": "lines", "value": int(tail_str)} + s = str(tail_str).strip().lower() + m = re.fullmatch(r"(\d+)([kw])?", s) + if not m: + raise ValueError("--tail 仅支持行数(如 5000、1k、1w)。按时间请改用 --start/--end") + value = int(m.group(1)) + unit = m.group(2) + if unit == "k": + value *= 1000 + elif unit == "w": + value *= 10000 + if value <= 0: + raise ValueError("--tail 行数必须 > 0") + return {"type": "lines", "value": value} def determine_status(results): @@ -444,7 +454,7 @@ def main(): parser.add_argument("--cache", action="store_true", help="仅分析 Cache 调度") parser.add_argument("--load", action="store_true", help="仅分析负载与计数器") parser.add_argument("--trace", metavar="ID", help="追踪指定请求(逗号分隔多 ID;传 all 可全量追踪)") - parser.add_argument("--tail", help="尾部行数或分钟数 (如 5000 或 30m)") + parser.add_argument("--tail", help="尾部行数(如 5000、1k、1w)。按时间请使用 --start/--end") parser.add_argument( "--start", default=None, help='起始时间(如 "16:00:00"、"03/31 16:00"、"2026/03/31 16:00:00")' ) @@ -478,14 +488,7 @@ def main(): tail_arg = parse_tail_arg(args.tail) tail = None - # --tail Nm 采用真实时间窗口过滤,再全量分析过滤后的临时文件 - if tail_arg and tail_arg["type"] == "minutes": - filtered_path, is_temp = filter_file_by_recent_minutes(log_file, tail_arg["value"]) - if is_temp: - atexit.register(lambda p=filtered_path: os.unlink(p) if os.path.exists(p) else None) - log_file = filtered_path - print(f"--tail {tail_arg['value']}m: 使用日志时间戳过滤最近窗口", file=sys.stderr) - elif tail_arg and tail_arg["type"] == "lines": + if tail_arg and tail_arg["type"] == "lines": tail = tail_arg["value"] # 确定分析模式