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 @@ -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"`)

若用户选择“指定时间段”,直接让用户填写:
Expand All @@ -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`。
缺失部分自动从日志首末行推断。

Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@
3. Per-Worker Stats — 各 worker 缓存利用排名

用法:
python3 stat_cache_hitrate.py <log_file> [--tail N|2k|30m|2h|1d] [--output DIR]
python3 stat_cache_hitrate.py <log_file> [--tail N|Nk|Nw] [--output DIR]
"""

import argparse
import json
import math
import os
import re
import subprocess
Expand All @@ -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,
Expand Down Expand Up @@ -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)


Expand All @@ -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: 日志提取与解析
# ════════════════════════════════════════════════════════════════
Expand All @@ -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}"
Expand All @@ -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)}"
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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/<timestamp>/)"
Expand All @@ -1002,42 +965,28 @@ def parse_args():


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

s = str(tail_str).strip().lower()
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():
Expand Down
9 changes: 4 additions & 5 deletions fastdeploy/golang_router/.claude/skills/troubleshoot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ description: >
### 2. 分析范围
必须使用 **AskUserQuestion 的离散选项**(不要只发纯文本编号):
- 选项 1: `全量分析(默认)` — 分析整个日志文件
- 选项 2: `尾部分析` — 只分析最近数据(可指定行数或时间如 `--tail 5000` 或 `--tail 30m`)
- 选项 2: `尾部分析` — 只分析最近数据(仅支持行数,如 `--tail 5000`)
- 选项 3: `指定时间段` — 分析特定时间范围内的日志

如果用户未选择,默认使用全量分析。
Expand All @@ -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` 输入时间,则将该输入直接作为时间范围参数解析。
可补充一条简短示例引导:
Expand Down Expand Up @@ -104,9 +105,7 @@ python3 $SCRIPTS/troubleshoot.py <log_file> --trace all

# 尾部分析
python3 $SCRIPTS/troubleshoot.py <log_file> --tail 5000
python3 $SCRIPTS/troubleshoot.py <log_file> --tail 30m

# 指定时间段(--start 和 --end 可单独或同时使用)
# 指定时间段(需要按时间筛选时使用;--start 和 --end 可单独或同时使用)
python3 $SCRIPTS/troubleshoot.py <log_file> --start "16:00:00" --end "17:00:00"
python3 $SCRIPTS/troubleshoot.py <log_file> --start "2026/03/31 16:00:00"
python3 $SCRIPTS/troubleshoot.py <log_file> --start "03/31" --end "03/31 18:00"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/<timestamp>/)
Expand All @@ -21,6 +21,7 @@
"""

import argparse
import re
import os
import sys
from datetime import datetime
Expand All @@ -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,
)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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")'
)
Expand Down Expand Up @@ -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"]

# 确定分析模式
Expand Down
Loading