Skip to content

Commit a928338

Browse files
authored
Merge pull request #22 from mouxinqq/codex/update-stat-cache-hitratio-for-line-input
skills: generalize --tail to numeric line counts with k/w shorthand and remove time-window tails
2 parents 26fb4b4 + 984a925 commit a928338

4 files changed

Lines changed: 50 additions & 102 deletions

File tree

fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/SKILL.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析
3737
### 2. 分析模式
3838
必须使用 **AskUserQuestion 的离散选项**(不要只发纯文本编号,避免客户端偶发不显示第 4 项):
3939
- 选项 1: `全量统计(默认)` — 扫描完整日志
40-
- 选项 2: `快速查看尾部` — 只看最近的数据(可指定 `2000/2k` 行,或 `30m/2h/1d` 时间窗口
40+
- 选项 2: `快速查看尾部` — 只看最近的数据(支持 `2000``1k``1w` 等行数写法
4141
- 选项 3: `指定时间段` — 分析特定时间范围(如 `--start "16:00" --end "17:00"`
4242

4343
若用户选择“指定时间段”,直接让用户填写:
@@ -47,6 +47,7 @@ IMPORTANT: 执行前阅读 references/log_formats.md 了解日志格式和解析
4747
如果用户未选择,默认使用全量统计。
4848

4949
`--start/--end``--tail` 互斥。`--start``--end` 可单独或同时指定。
50+
`--tail` 仅支持“行数”语义(如 `2000`,也兼容 `1k/1w` 自动换算),不再支持 `30m/2h/1d` 这类时间窗口;按时间请使用 `--start/--end`
5051
时间格式灵活:支持 `YYYY/MM/DD HH:MM:SS``HH:MM:SS``HH:MM``MM/DD``MM/DD HH:MM`
5152
缺失部分自动从日志首末行推断。
5253

@@ -65,12 +66,8 @@ python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志
6566
# 快速查看尾部数据
6667
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail # 默认最后 2000 行
6768
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 5000 # 指定行数
68-
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 2k # 行数缩写
69-
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 30m # 分钟窗口
70-
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 2h # 小时窗口
71-
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 1d # 天窗口
72-
73-
# 指定时间段(--start 和 --end 可单独或同时使用)
69+
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --tail 1k # 行数缩写(自动换算)
70+
# 指定时间段(需要按时间筛选时使用;--start 和 --end 可单独或同时使用)
7471
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --start "16:00:00" --end "17:00:00"
7572
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --start "2026/03/31 16:00:00"
7673
python3 .claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py <日志文件> --start "03/31" --end "03/31 18:00"

fastdeploy/golang_router/.claude/skills/stat-cache-hitrate/scripts/stat_cache_hitrate.py

Lines changed: 24 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
3. Per-Worker Stats — 各 worker 缓存利用排名
99
1010
用法:
11-
python3 stat_cache_hitrate.py <log_file> [--tail N|2k|30m|2h|1d] [--output DIR]
11+
python3 stat_cache_hitrate.py <log_file> [--tail N|Nk|Nw] [--output DIR]
1212
"""
1313

1414
import argparse
1515
import json
16-
import math
1716
import os
1817
import re
1918
import subprocess
@@ -28,7 +27,6 @@
2827
from chart import render_bar, render_sparkline, render_table
2928
from log_parser import (
3029
complete_time_arg,
31-
extract_ts,
3230
filter_file_by_time_range,
3331
parse_cache_strategy_line,
3432
parse_stats_line,
@@ -172,16 +170,10 @@ def count_lines(filepath):
172170
def read_lines(filepath, tail=None):
173171
"""读取日志文件,支持 tail 模式。"""
174172
if tail is not None:
175-
if isinstance(tail, str) and tail.endswith("m"):
176-
# 按时间 tail:读取全部行,过滤最近 N 分钟
177-
minutes = int(tail[:-1])
178-
all_lines = _read_file_lines(filepath)
179-
return _filter_by_time(all_lines, minutes)
180-
else:
181-
# 按行数 tail
182-
n = int(tail)
183-
result = subprocess.run(["tail", "-n", str(n), filepath], capture_output=True, text=True)
184-
return result.stdout.splitlines() if result.returncode == 0 else []
173+
# 按行数 tail
174+
n = int(tail)
175+
result = subprocess.run(["tail", "-n", str(n), filepath], capture_output=True, text=True)
176+
return result.stdout.splitlines() if result.returncode == 0 else []
185177
return _read_file_lines(filepath)
186178

187179

@@ -190,35 +182,6 @@ def _read_file_lines(filepath):
190182
return f.readlines()
191183

192184

193-
def _filter_by_time(lines, minutes):
194-
"""过滤最近 N 分钟的日志行。"""
195-
# 找最后一行的时间戳作为基准
196-
last_ts = None
197-
for line in reversed(lines):
198-
ts = extract_ts(line)
199-
if ts:
200-
last_ts = parse_ts(ts)
201-
break
202-
if not last_ts:
203-
return lines
204-
205-
from datetime import timedelta
206-
207-
cutoff = last_ts - timedelta(minutes=minutes)
208-
result = []
209-
for line in lines:
210-
ts = extract_ts(line)
211-
if ts:
212-
try:
213-
if parse_ts(ts) >= cutoff:
214-
result.append(line)
215-
except ValueError:
216-
result.append(line)
217-
else:
218-
result.append(line)
219-
return result
220-
221-
222185
# ════════════════════════════════════════════════════════════════
223186
# Phase 2: 日志提取与解析
224187
# ════════════════════════════════════════════════════════════════
@@ -237,7 +200,7 @@ def grep_and_parse(filepath, grep_pattern, parse_cmd, tail=None):
237200
"""大文件模式:grep 过滤 + log_parser.py CLI 管道解析。"""
238201
parser_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "log_parser.py")
239202

240-
if tail and not (isinstance(tail, str) and tail.endswith("m")):
203+
if tail:
241204
grep_cmd = f"tail -n {tail} {_shell_quote(filepath)} | grep -F {_shell_quote(grep_pattern)} | python3 {_shell_quote(parser_path)} {parse_cmd}"
242205
else:
243206
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):
255218

256219
def grep_count(filepath, grep_pattern, tail=None):
257220
"""大文件模式:grep 计数。"""
258-
if tail and not (isinstance(tail, str) and tail.endswith("m")):
221+
if tail:
259222
cmd = f"tail -n {tail} {_shell_quote(filepath)} | grep -cE {_shell_quote(grep_pattern)}"
260223
else:
261224
cmd = f"grep -cE {_shell_quote(grep_pattern)} {_shell_quote(filepath)}"
@@ -283,7 +246,7 @@ def extract_data(filepath, tail=None):
283246
strategy_recs = grep_and_parse(filepath, STRATEGY_PATTERN, "parse-cache-strategy", tail)
284247
stats_recs = grep_and_parse(filepath, STATS_PATTERN, "parse-stats", tail)
285248
inference_count = grep_count(filepath, r"\] \[POST\] /v1/chat/completions |\] \[POST\] /v1/completions ", tail)
286-
line_count = int(tail) if tail is not None and not (isinstance(tail, str) and tail.endswith("m")) else total
249+
line_count = int(tail) if tail is not None else total
287250
return strategy_recs, stats_recs, inference_count, line_count
288251

289252

@@ -989,7 +952,7 @@ def parse_args():
989952
"--tail",
990953
nargs="?",
991954
const="2000",
992-
help="只分析尾部数据(支持 2000/2k 行,或 30m/2h/1d 时间窗口)",
955+
help="只分析尾部数据(支持 2000、1k、1w 等行数写法)。按时间请使用 --start/--end",
993956
)
994957
parser.add_argument(
995958
"--output", default=None, help="详细报告输出目录(默认:skill_output/stat-cache-hitrate/<timestamp>/)"
@@ -1002,42 +965,28 @@ def parse_args():
1002965

1003966

1004967
def parse_tail_arg(tail_str):
1005-
"""解析 --tail 参数,返回 int(行数) 或 '<minutes>m'(时间窗口)。"""
968+
"""解析 --tail 参数,返回行数 int。支持数字及 k/w 缩写。"""
1006969
if tail_str is None:
1007970
return None
1008971

1009972
s = str(tail_str).strip().lower()
1010973
if not s:
1011974
raise ValueError("--tail 不能为空")
1012975

1013-
# 行数: 2000
1014-
if re.fullmatch(r"\d+", s):
1015-
value = int(s)
1016-
if value <= 0:
1017-
raise ValueError("--tail 行数必须 > 0")
1018-
return value
1019-
1020-
# 行数缩写: 2k => 2000
1021-
m = re.fullmatch(r"(\d+)k", s)
1022-
if m:
1023-
value = int(m.group(1)) * 1000
1024-
if value <= 0:
1025-
raise ValueError("--tail 行数必须 > 0")
1026-
return value
1027-
1028-
# 时间窗口: 30m/2h/1d(最终统一成分钟)
1029-
m = re.fullmatch(r"(\d+)(m|h|d)", s)
1030-
if m:
1031-
num = int(m.group(1))
1032-
unit = m.group(2)
1033-
if num <= 0:
1034-
raise ValueError("--tail 时间窗口必须 > 0")
1035-
factor = {"m": 1, "h": 60, "d": 1440}[unit]
1036-
minutes = num * factor
1037-
minutes = max(1, math.ceil(minutes))
1038-
return f"{minutes}m"
1039-
1040-
raise ValueError("不支持的 --tail 格式:请使用 2000/2k 或 30m/2h/1d")
976+
m = re.fullmatch(r"(\d+)([kw])?", s)
977+
if not m:
978+
raise ValueError("不支持的 --tail 格式:请使用 2000、1k、1w 等行数写法。按时间请改用 --start/--end")
979+
980+
value = int(m.group(1))
981+
unit = m.group(2)
982+
if unit == "k":
983+
value *= 1000
984+
elif unit == "w":
985+
value *= 10000
986+
987+
if value <= 0:
988+
raise ValueError("--tail 行数必须 > 0")
989+
return value
1041990

1042991

1043992
def main():

fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ description: >
3838
### 2. 分析范围
3939
必须使用 **AskUserQuestion 的离散选项**(不要只发纯文本编号):
4040
- 选项 1: `全量分析(默认)` — 分析整个日志文件
41-
- 选项 2: `尾部分析` — 只分析最近数据(可指定行数或时间如 `--tail 5000``--tail 30m`
41+
- 选项 2: `尾部分析` — 只分析最近数据(仅支持行数,如 `--tail 5000`
4242
- 选项 3: `指定时间段` — 分析特定时间范围内的日志
4343

4444
如果用户未选择,默认使用全量分析。
@@ -50,10 +50,11 @@ description: >
5050
时间格式灵活:支持 `YYYY/MM/DD HH:MM:SS``HH:MM:SS``HH:MM``MM/DD``MM/DD HH:MM`
5151
缺失部分自动从日志首末行推断(缺年份取首行,缺日期取末行)。
5252
`--start/--end``--tail` 互斥。
53+
`--tail` 仅支持“行数”语义(如 `5000`,也兼容 `1k/1w` 自动换算),不再支持 `30m` 这类时间写法;凡是按时间筛选都使用 `--start/--end`
5354

5455
当用户选择“指定时间段”时,必须再发起一次 **AskUserQuestion**(离散选项)引导时间输入:
5556
- 选项 1: `当天(00:00:00 到当前)`(推荐)
56-
- 选项 2: `最近半小时`(自动换算为 `--start now-30m --end now` 语义
57+
- 选项 2: `自定义时间段`(由用户直接输入起止时间
5758

5859
用户若通过客户端默认 `Other` 输入时间,则将该输入直接作为时间范围参数解析。
5960
可补充一条简短示例引导:
@@ -104,9 +105,7 @@ python3 $SCRIPTS/troubleshoot.py <log_file> --trace all
104105

105106
# 尾部分析
106107
python3 $SCRIPTS/troubleshoot.py <log_file> --tail 5000
107-
python3 $SCRIPTS/troubleshoot.py <log_file> --tail 30m
108-
109-
# 指定时间段(--start 和 --end 可单独或同时使用)
108+
# 指定时间段(需要按时间筛选时使用;--start 和 --end 可单独或同时使用)
110109
python3 $SCRIPTS/troubleshoot.py <log_file> --start "16:00:00" --end "17:00:00"
111110
python3 $SCRIPTS/troubleshoot.py <log_file> --start "2026/03/31 16:00:00"
112111
python3 $SCRIPTS/troubleshoot.py <log_file> --start "03/31" --end "03/31 18:00"

fastdeploy/golang_router/.claude/skills/troubleshoot/scripts/troubleshoot.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
--cache 仅分析 Cache 调度
1313
--load 仅分析负载与计数器
1414
--trace ID 追踪指定请求(支持逗号分隔多 ID;传 all 可全量追踪)
15-
--tail N 仅分析尾部 N 行(支持 N 或 Nm 格式如 30m
15+
--tail N 仅分析尾部 N 行(支持 5000/1k/1w 等行数写法
1616
--start TIME 起始时间(如 "16:00:00"、"03/31 16:00")
1717
--end TIME 结束时间(如 "17:00:00"、"2026/03/31 17:00:00")
1818
--output DIR 详细报告导出目录(默认: skill_output/troubleshoot/<timestamp>/)
@@ -21,6 +21,7 @@
2121
"""
2222

2323
import argparse
24+
import re
2425
import os
2526
import sys
2627
from datetime import datetime
@@ -38,7 +39,6 @@
3839
from analyzers.trace import analyze_trace, format_trace_report
3940
from log_parser import (
4041
complete_time_arg,
41-
filter_file_by_recent_minutes,
4242
filter_file_by_time_range,
4343
)
4444

@@ -106,12 +106,22 @@ def determine_log_file(user_path=None):
106106

107107

108108
def parse_tail_arg(tail_str):
109-
"""解析 --tail 参数:支持纯数字(行数)或 Nm(分钟)格式。"""
109+
"""解析 --tail 参数:支持数字及 k/w 缩写。"""
110110
if tail_str is None:
111111
return None
112-
if tail_str.endswith("m"):
113-
return {"type": "minutes", "value": int(tail_str[:-1])}
114-
return {"type": "lines", "value": int(tail_str)}
112+
s = str(tail_str).strip().lower()
113+
m = re.fullmatch(r"(\d+)([kw])?", s)
114+
if not m:
115+
raise ValueError("--tail 仅支持行数(如 5000、1k、1w)。按时间请改用 --start/--end")
116+
value = int(m.group(1))
117+
unit = m.group(2)
118+
if unit == "k":
119+
value *= 1000
120+
elif unit == "w":
121+
value *= 10000
122+
if value <= 0:
123+
raise ValueError("--tail 行数必须 > 0")
124+
return {"type": "lines", "value": value}
115125

116126

117127
def determine_status(results):
@@ -444,7 +454,7 @@ def main():
444454
parser.add_argument("--cache", action="store_true", help="仅分析 Cache 调度")
445455
parser.add_argument("--load", action="store_true", help="仅分析负载与计数器")
446456
parser.add_argument("--trace", metavar="ID", help="追踪指定请求(逗号分隔多 ID;传 all 可全量追踪)")
447-
parser.add_argument("--tail", help="尾部行数或分钟数 (如 5000 或 30m)")
457+
parser.add_argument("--tail", help="尾部行数(如 5000、1k、1w)。按时间请使用 --start/--end")
448458
parser.add_argument(
449459
"--start", default=None, help='起始时间(如 "16:00:00"、"03/31 16:00"、"2026/03/31 16:00:00")'
450460
)
@@ -478,14 +488,7 @@ def main():
478488

479489
tail_arg = parse_tail_arg(args.tail)
480490
tail = None
481-
# --tail Nm 采用真实时间窗口过滤,再全量分析过滤后的临时文件
482-
if tail_arg and tail_arg["type"] == "minutes":
483-
filtered_path, is_temp = filter_file_by_recent_minutes(log_file, tail_arg["value"])
484-
if is_temp:
485-
atexit.register(lambda p=filtered_path: os.unlink(p) if os.path.exists(p) else None)
486-
log_file = filtered_path
487-
print(f"--tail {tail_arg['value']}m: 使用日志时间戳过滤最近窗口", file=sys.stderr)
488-
elif tail_arg and tail_arg["type"] == "lines":
491+
if tail_arg and tail_arg["type"] == "lines":
489492
tail = tail_arg["value"]
490493

491494
# 确定分析模式

0 commit comments

Comments
 (0)