scripts/incident_artifact_tool.py
scripts/incident_artifact_tool.pyBrowse 7 files
6,395 tokens
26,646 bytes
Token encoding: o200k_base
Snapshot a9fb1c3
← Back to SKILL.md
1#!/usr/bin/env python32"""Collect or inspect serving bundles and dumps for SGLang debug."""3 4from __future__ import annotations5 6import argparse7import glob8import json9import math10import os11import pickle12import re13import time14from collections import defaultdict15from datetime import datetime16from pathlib import Path17from typing import Any, Dict, Optional, Sequence18from urllib import error, parse, request19 20METRIC_RE = re.compile(21 r"^(?P<name>[^{\s]+)(?:\{(?P<labels>[^}]*)\})?\s+(?P<value>[-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)$"22)23LABEL_RE = re.compile(r'([a-zA-Z_:][a-zA-Z0-9_:]*)="((?:[^"\\]|\\.)*)"')24ENDPOINT_SPECS = (25 ("text", "health.txt", "/health"),26 ("text", "health_generate.txt", "/health_generate"),27 ("text", "metrics.txt", "/metrics"),28 ("json", "model_info.json", "/model_info"),29 ("json", "server_info.json", "/server_info"),30 ("json", "loads_all.json", "/v1/loads?include=all"),31 (32 "json",33 "loads_core_queues_disagg.json",34 "/v1/loads?include=core,queues,disagg,spec",35 ),36 ("json", "hicache_storage_backend.json", "/hicache/storage-backend"),37)38BUNDLE_NOTES = [39 "This bundle is read-only. It does not start profiling or change trace level.",40 "HiCache status may fail if admin_api_key is not configured or the wrong bearer token was used.",41 "loads_all.json is the best point-in-time load snapshot in this bundle.",42 "metrics.txt is raw Prometheus text intended for follow-up parsing.",43]44 45 46def request_text(47 base_url: str,48 path: str,49 token: Optional[str],50 timeout: float = 10.0,51) -> tuple[bool, int, str]:52 url = parse.urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))53 req = request.Request(url)54 if token:55 req.add_header("Authorization", f"Bearer {token}")56 try:57 with request.urlopen(req, timeout=timeout) as resp:58 body = resp.read().decode("utf-8", errors="replace")59 return True, resp.status, body60 except error.HTTPError as e:61 body = e.read().decode("utf-8", errors="replace")62 return False, e.code, body63 except Exception as e: # noqa: BLE00164 return False, -1, f"{type(e).__name__}: {e}"65 66 67def request_endpoint(68 base_url: str,69 path: str,70 token: Optional[str],71 parse_json: bool,72 timeout: float = 10.0,73) -> Dict[str, Any]:74 ok, status, body = request_text(base_url, path, token, timeout=timeout)75 result: Dict[str, Any] = {"ok": ok, "status": status, "path": path}76 if not ok:77 result["error"] = body78 return result79 if not parse_json:80 result["text"] = body81 return result82 try:83 result["json"] = json.loads(body)84 except json.JSONDecodeError:85 result["text"] = body86 result["decode_error"] = "response was not valid JSON"87 return result88 89 90def write_json(path: Path, obj: Dict[str, Any]) -> None:91 path.write_text(92 json.dumps(obj, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"93 )94 95 96def write_text(path: Path, text: str) -> None:97 path.write_text(text, encoding="utf-8")98 99 100def format_summary_line(filename: str, result: Dict[str, Any]) -> str:101 if result.get("ok"):102 return f"{filename}: ok"103 return (104 f"{filename}: failed status={result.get('status')} error={result.get('error')}"105 )106 107 108def collect_bundle(109 base_url: str,110 token: Optional[str],111 outdir: Optional[str],112 timeout: float,113) -> Path:114 timestamp = time.strftime("%Y%m%d_%H%M%S")115 bundle_dir = Path(outdir or f"./incident_bundle_{timestamp}").resolve()116 bundle_dir.mkdir(parents=True, exist_ok=True)117 118 metadata = {119 "artifact_type": "incident_bundle",120 "base_url": base_url,121 "collected_at": timestamp,122 "token_provided": bool(token),123 "timeout_seconds": timeout,124 }125 write_json(bundle_dir / "metadata.json", metadata)126 127 summary_lines = []128 for kind, filename, path in ENDPOINT_SPECS:129 result = request_endpoint(130 base_url, path, token, parse_json=(kind == "json"), timeout=timeout131 )132 output_path = bundle_dir / filename133 if kind == "text" and result.get("ok"):134 write_text(output_path, str(result.get("text", "")))135 else:136 write_json(137 (138 output_path139 if kind == "json"140 else bundle_dir / f"{filename}.error.json"141 ),142 result,143 )144 summary_lines.append(format_summary_line(filename, result))145 146 write_text(147 bundle_dir / "SUMMARY.txt",148 "\n".join(summary_lines + [""] + BUNDLE_NOTES) + "\n",149 )150 return bundle_dir151 152 153def load_json(path: Path) -> Optional[Dict[str, Any]]:154 if not path.exists():155 return None156 return json.loads(path.read_text(encoding="utf-8"))157 158 159def unwrap_result(path: Path) -> Optional[Dict[str, Any]]:160 obj = load_json(path)161 if obj is None:162 return None163 if isinstance(obj, dict) and "json" in obj:164 return obj.get("json")165 return obj166 167 168def read_text(path: Path) -> Optional[str]:169 if not path.exists():170 return None171 return path.read_text(encoding="utf-8")172 173 174def endpoint_ok(bundle_dir: Path, stem: str) -> bool:175 return (bundle_dir / f"{stem}.txt").exists() and not (176 bundle_dir / f"{stem}.txt.error.json"177 ).exists()178 179 180def parse_labels(raw: Optional[str]) -> Dict[str, str]:181 if not raw:182 return {}183 labels = {}184 for key, value in LABEL_RE.findall(raw):185 labels[key] = bytes(value, "utf-8").decode("unicode_escape")186 return labels187 188 189def parse_metrics(metrics_text: str) -> Dict[str, list[dict[str, Any]]]:190 series: Dict[str, list[dict[str, Any]]] = defaultdict(list)191 for line in metrics_text.splitlines():192 line = line.strip()193 if not line or line.startswith("#"):194 continue195 match = METRIC_RE.match(line)196 if not match:197 continue198 series[match.group("name")].append(199 {200 "labels": parse_labels(match.group("labels")),201 "value": float(match.group("value")),202 }203 )204 return series205 206 207def metric_sum(metrics: Dict[str, list[dict[str, Any]]], name: str) -> float:208 return sum(item["value"] for item in metrics.get(name, []))209 210 211def safe_div(212 numerator: Optional[float], denominator: Optional[float]213) -> Optional[float]:214 if numerator is None or denominator in (None, 0):215 return None216 return numerator / denominator217 218 219def coalesce(*values: Any) -> Any:220 for value in values:221 if value is not None:222 return value223 return None224 225 226def fmt_float(value: Optional[float], digits: int = 3) -> str:227 if value is None or (228 isinstance(value, float) and (math.isnan(value) or math.isinf(value))229 ):230 return "n/a"231 return f"{value:.{digits}f}"232 233 234def is_positive_number(value: Any, threshold: float = 0.0) -> bool:235 return (236 isinstance(value, (int, float))237 and not math.isnan(value)238 and not math.isinf(value)239 and value > threshold240 )241 242 243def compute_stage_averages(244 metrics: Dict[str, list[dict[str, Any]]], sum_name: str, count_name: str245) -> Dict[str, float]:246 grouped_sum: Dict[str, float] = defaultdict(float)247 grouped_count: Dict[str, float] = defaultdict(float)248 for item in metrics.get(sum_name, []):249 stage = item["labels"].get("stage", "")250 rank = item["labels"].get("tp_rank", "")251 grouped_sum[f"{stage}|{rank}"] += item["value"]252 for item in metrics.get(count_name, []):253 stage = item["labels"].get("stage", "")254 rank = item["labels"].get("tp_rank", "")255 grouped_count[f"{stage}|{rank}"] += item["value"]256 257 result: Dict[str, float] = {}258 for key, total_sum in grouped_sum.items():259 stage, _rank = key.split("|", 1)260 avg = safe_div(total_sum, grouped_count.get(key))261 if avg is None:262 continue263 result[stage] = max(result.get(stage, 0.0), avg)264 return result265 266 267def add_signal(signals: list[str], text: str) -> None:268 if text not in signals:269 signals.append(text)270 271 272def build_bundle_summary(bundle_dir: Path) -> Dict[str, Any]:273 metadata = load_json(bundle_dir / "metadata.json") or {}274 model_info = unwrap_result(bundle_dir / "model_info.json") or {}275 server_info = unwrap_result(bundle_dir / "server_info.json") or {}276 loads_info = unwrap_result(bundle_dir / "loads_all.json") or {}277 metrics_text = read_text(bundle_dir / "metrics.txt") or ""278 metrics = parse_metrics(metrics_text)279 280 aggregate = loads_info.get("aggregate") or {}281 loads = loads_info.get("loads") or []282 load0 = loads[0] if loads else {}283 internal_states = server_info.get("internal_states") or []284 runtime_state = internal_states[0] if internal_states else {}285 memory_usage = runtime_state.get("memory_usage") or load0.get("memory") or {}286 287 ttft_avg = safe_div(288 metric_sum(metrics, "sglang:time_to_first_token_seconds_sum"),289 metric_sum(metrics, "sglang:time_to_first_token_seconds_count"),290 )291 e2e_avg = safe_div(292 metric_sum(metrics, "sglang:e2e_request_latency_seconds_sum"),293 metric_sum(metrics, "sglang:e2e_request_latency_seconds_count"),294 )295 queue_avg = safe_div(296 metric_sum(metrics, "sglang:queue_time_seconds_sum"),297 metric_sum(metrics, "sglang:queue_time_seconds_count"),298 )299 per_stage_avg = compute_stage_averages(300 metrics,301 "sglang:per_stage_req_latency_seconds_sum",302 "sglang:per_stage_req_latency_seconds_count",303 )304 305 summary: Dict[str, Any] = {306 "artifact_type": "incident_bundle",307 "bundle_dir": str(bundle_dir),308 "base_url": metadata.get("base_url"),309 "collected_at": metadata.get("collected_at"),310 "health": {311 "health_ok": endpoint_ok(bundle_dir, "health"),312 "health_generate_ok": endpoint_ok(bundle_dir, "health_generate"),313 },314 "model": {315 "model_path": model_info.get("model_path") or server_info.get("model_path"),316 "served_model_name": server_info.get("served_model_name"),317 "weight_version": model_info.get("weight_version")318 or server_info.get("weight_version"),319 "model_type": model_info.get("model_type"),320 "is_generation": model_info.get("is_generation"),321 },322 "topology": {323 "tp_size": server_info.get("tp_size"),324 "dp_size": server_info.get("dp_size"),325 "pp_size": server_info.get("pp_size"),326 "ep_size": server_info.get("ep_size"),327 "disaggregation_mode": server_info.get("disaggregation_mode"),328 "attention_backend": server_info.get("attention_backend"),329 "sampling_backend": server_info.get("sampling_backend"),330 "schedule_policy": server_info.get("schedule_policy"),331 "enable_trace": server_info.get("enable_trace"),332 "enable_metrics": server_info.get("enable_metrics"),333 },334 "capacity": {335 "max_total_num_tokens": server_info.get("max_total_num_tokens"),336 "max_req_input_len": server_info.get("max_req_input_len"),337 "effective_max_running_requests_per_dp": coalesce(338 runtime_state.get("effective_max_running_requests_per_dp"),339 load0.get("max_running_requests"),340 ),341 "weight_gb": coalesce(342 memory_usage.get("weight"), memory_usage.get("weight_gb")343 ),344 "kv_cache_gb": coalesce(345 memory_usage.get("kvcache"), memory_usage.get("kv_cache_gb")346 ),347 "graph_gb": coalesce(348 memory_usage.get("graph"), memory_usage.get("graph_gb")349 ),350 "token_capacity": memory_usage.get("token_capacity"),351 },352 "point_in_time_load": {353 "running_reqs": coalesce(354 aggregate.get("total_running_reqs"), load0.get("num_running_reqs")355 ),356 "waiting_reqs": coalesce(357 aggregate.get("total_waiting_reqs"), load0.get("num_waiting_reqs")358 ),359 "total_reqs": coalesce(360 aggregate.get("total_reqs"), load0.get("num_total_reqs")361 ),362 "token_usage": coalesce(363 aggregate.get("avg_token_usage"), load0.get("token_usage")364 ),365 "avg_throughput": coalesce(366 aggregate.get("avg_throughput"), load0.get("gen_throughput")367 ),368 "avg_utilization": coalesce(369 aggregate.get("avg_utilization"), load0.get("utilization")370 ),371 "cache_hit_rate": load0.get("cache_hit_rate"),372 "queues": load0.get("queues"),373 "disaggregation": load0.get("disaggregation"),374 },375 "metrics": {376 "request_count": metric_sum(metrics, "sglang:num_requests_total"),377 "prompt_tokens_total": metric_sum(metrics, "sglang:prompt_tokens_total"),378 "generation_tokens_total": metric_sum(379 metrics, "sglang:generation_tokens_total"380 ),381 "avg_ttft_seconds": ttft_avg,382 "avg_e2e_seconds": e2e_avg,383 "avg_queue_time_seconds": queue_avg,384 "stage_avg_seconds_max_tp_rank": per_stage_avg,385 },386 "signals": [],387 }388 389 signals = summary["signals"]390 health = summary["health"]391 point_in_time_load = summary["point_in_time_load"]392 running_reqs = point_in_time_load.get("running_reqs")393 waiting_reqs = point_in_time_load.get("waiting_reqs")394 395 if health["health_ok"] and not health["health_generate_ok"]:396 add_signal(397 signals,398 "/health is green but /health_generate failed. Suspect runtime or scheduler path, not just HTTP liveness.",399 )400 if not health["health_ok"]:401 add_signal(402 signals,403 "/health failed. Start with startup, crash, or global unhealthy paths.",404 )405 if is_positive_number(waiting_reqs):406 add_signal(407 signals,408 f"Point-in-time load shows queue buildup: waiting_reqs={waiting_reqs}.",409 )410 if (411 point_in_time_load.get("token_usage") is not None412 and point_in_time_load["token_usage"] >= 0.9413 ):414 add_signal(415 signals,416 "Token usage is near saturation. KV or token-capacity pressure may explain latency.",417 )418 if (419 ttft_avg is not None420 and queue_avg is not None421 and ttft_avg > 2.0422 and queue_avg < 0.2423 ):424 add_signal(425 signals,426 f"Average TTFT is high ({fmt_float(ttft_avg)}s) while average queue time is low ({fmt_float(queue_avg)}s). This looks more like prefill or request-path work than queue pressure.",427 )428 prefill_forward = per_stage_avg.get("prefill_forward")429 request_process = per_stage_avg.get("request_process")430 if (431 prefill_forward is not None432 and request_process is not None433 and prefill_forward > max(0.5, request_process * 10)434 ):435 add_signal(436 signals,437 f"Prefill forward dominates quick stage timing: prefill_forward~{fmt_float(prefill_forward)}s vs request_process~{fmt_float(request_process)}s.",438 )439 if running_reqs == 0 and waiting_reqs == 0:440 add_signal(441 signals,442 "Bundle snapshot was captured while the server was effectively idle. Reproduce under live traffic or replayed workload if the problem is intermittent.",443 )444 445 return summary446 447 448def render_bundle_text(summary: Dict[str, Any]) -> str:449 health = summary["health"]450 model = summary["model"]451 topology = summary["topology"]452 capacity = summary["capacity"]453 load = summary["point_in_time_load"]454 metrics = summary["metrics"]455 stage_avgs = metrics["stage_avg_seconds_max_tp_rank"]456 457 lines = [458 f"Bundle: {summary['bundle_dir']}",459 f"Base URL: {summary.get('base_url') or 'n/a'}",460 f"Collected At: {summary.get('collected_at') or 'n/a'}",461 "",462 f"Health: /health={'ok' if health['health_ok'] else 'failed'} /health_generate={'ok' if health['health_generate_ok'] else 'failed'}",463 f"Model: {model.get('model_path') or 'n/a'} weight_version={model.get('weight_version') or 'n/a'} type={model.get('model_type') or 'n/a'}",464 "Topology: "465 f"tp={topology.get('tp_size')} dp={topology.get('dp_size')} pp={topology.get('pp_size')} ep={topology.get('ep_size')} "466 f"disagg={topology.get('disaggregation_mode')} trace={topology.get('enable_trace')} metrics={topology.get('enable_metrics')}",467 "Capacity: "468 f"max_total_tokens={capacity.get('max_total_num_tokens')} "469 f"max_running_reqs={capacity.get('effective_max_running_requests_per_dp')} "470 f"weight_gb={fmt_float(capacity.get('weight_gb'))} "471 f"kv_cache_gb={fmt_float(capacity.get('kv_cache_gb'))} "472 f"graph_gb={fmt_float(capacity.get('graph_gb'))}",473 "Point-in-time load: "474 f"running={load.get('running_reqs')} waiting={load.get('waiting_reqs')} total={load.get('total_reqs')} "475 f"token_usage={fmt_float(load.get('token_usage'))} throughput={fmt_float(load.get('avg_throughput'))} "476 f"cache_hit_rate={fmt_float(load.get('cache_hit_rate'))}",477 "Metrics: "478 f"requests={fmt_float(metrics.get('request_count'), 0)} "479 f"prompt_tokens={fmt_float(metrics.get('prompt_tokens_total'), 0)} "480 f"generation_tokens={fmt_float(metrics.get('generation_tokens_total'), 0)} "481 f"avg_ttft_s={fmt_float(metrics.get('avg_ttft_seconds'))} "482 f"avg_e2e_s={fmt_float(metrics.get('avg_e2e_seconds'))} "483 f"avg_queue_s={fmt_float(metrics.get('avg_queue_time_seconds'))}",484 ]485 486 if stage_avgs:487 stage_parts = [488 f"{name}={fmt_float(value)}s" for name, value in sorted(stage_avgs.items())489 ]490 lines.append("Stage Averages (max across TP ranks): " + ", ".join(stage_parts))491 492 queues = load.get("queues") or {}493 if queues:494 lines.append(495 "Queues: "496 + ", ".join(f"{key}={value}" for key, value in sorted(queues.items()))497 )498 499 disagg = load.get("disaggregation") or {}500 if disagg:501 lines.append(502 "Disaggregation: "503 + ", ".join(f"{key}={value}" for key, value in sorted(disagg.items()))504 )505 506 lines.append("")507 lines.append("What stands out:")508 if summary["signals"]:509 lines.extend(f"- {signal}" for signal in summary["signals"])510 else:511 lines.append("- No strong signal from this bundle.")512 513 return "\n".join(lines) + "\n"514 515 516def get_field(obj: Any, name: str, default: Any = None) -> Any:517 if obj is None:518 return default519 if isinstance(obj, dict):520 return obj.get(name, default)521 return getattr(obj, name, default)522 523 524def iter_dump_files(525 input_file: Optional[str], input_folder: Optional[str]526) -> Sequence[Path]:527 if input_file:528 return [Path(input_file)]529 if input_folder:530 return [Path(p) for p in sorted(glob.glob(f"{input_folder}/*.pkl"))]531 raise SystemExit("Either --input-file or --input-folder must be provided.")532 533 534def load_dump_payload(path: Path) -> dict[str, Any]:535 with path.open("rb") as fh:536 payload = pickle.load(fh)537 if isinstance(payload, dict):538 return payload539 return {"requests": payload}540 541 542def pick_text_preview(req: Any) -> str:543 candidates = [544 get_field(req, "origin_input_text"),545 get_field(req, "text"),546 get_field(req, "prompt"),547 ]548 for value in candidates:549 if isinstance(value, str) and value:550 return value551 if isinstance(value, list) and value:552 first = value[0]553 if isinstance(first, str) and first:554 return first555 return ""556 557 558def format_timestamp(ts: Any) -> str:559 if not isinstance(ts, (int, float)):560 return "n/a"561 return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")562 563 564def summarize_request(565 record: tuple[Any, dict[str, Any], Any, Any], idx: int, preview_chars: int566) -> list[str]:567 req, output, start_time, end_time = record568 preview = pick_text_preview(req).replace("\n", " ").strip()569 if len(preview) > preview_chars:570 preview = preview[: preview_chars - 3] + "..."571 572 output_dict = output if isinstance(output, dict) else {}573 meta_info = get_field(output_dict, "meta_info", {}) or {}574 rid = get_field(req, "rid") or get_field(meta_info, "id")575 stream = bool(get_field(req, "stream", False))576 prompt_tokens = get_field(meta_info, "prompt_tokens")577 completion_tokens = get_field(meta_info, "completion_tokens")578 duration = (579 end_time - start_time580 if isinstance(start_time, (int, float)) and isinstance(end_time, (int, float))581 else None582 )583 584 elapsed_str = f"{duration:.3f}" if duration is not None else "n/a"585 lines = [586 f"[{idx}] rid={rid or 'n/a'} stream={stream} "587 f"prompt_tokens={prompt_tokens if prompt_tokens is not None else 'n/a'} "588 f"completion_tokens={completion_tokens if completion_tokens is not None else 'n/a'} "589 f"start={format_timestamp(start_time)} elapsed_s={elapsed_str}"590 ]591 if preview:592 lines.append(f" text={preview}")593 return lines594 595 596def summarize_dump_file(path: Path, max_requests: int, preview_chars: int) -> str:597 payload = load_dump_payload(path)598 requests = payload.get("requests") or []599 server_args = payload.get("server_args")600 launch_command = payload.get("launch_command")601 602 model_path = get_field(server_args, "model_path")603 tp_size = get_field(server_args, "tp_size")604 dp_size = get_field(server_args, "dp_size")605 pp_size = get_field(server_args, "pp_size")606 host = get_field(server_args, "host")607 port = get_field(server_args, "port")608 609 timestamps = [610 record[2]611 for record in requests612 if isinstance(record, tuple)613 and len(record) >= 4614 and isinstance(record[2], (int, float))615 ]616 time_span = (617 max(timestamps) - min(timestamps)618 if len(timestamps) >= 2619 else 0.0620 if len(timestamps) == 1621 else None622 )623 624 lines = [625 f"File: {path}",626 "Dump Type: request_or_crash_dump",627 f"Requests: {len(requests)}",628 f"Model: {model_path or 'n/a'}",629 f"Topology: tp={tp_size if tp_size is not None else 'n/a'} "630 f"dp={dp_size if dp_size is not None else 'n/a'} "631 f"pp={pp_size if pp_size is not None else 'n/a'}",632 f"Endpoint: {host or 'n/a'}:{port if port is not None else 'n/a'}",633 (634 f"Time span seconds: {time_span:.3f}"635 if time_span is not None636 else "Time span seconds: n/a"637 ),638 ]639 if launch_command:640 lines.append(f"Launch command: {launch_command}")641 642 for idx, record in enumerate(requests[:max_requests]):643 if not isinstance(record, tuple) or len(record) < 4:644 lines.append(f"[{idx}] Unsupported record shape: {type(record)!r}")645 continue646 lines.extend(summarize_request(record, idx, preview_chars))647 648 if len(requests) > max_requests:649 lines.append(f"... truncated {len(requests) - max_requests} more requests")650 return "\n".join(lines)651 652 653def main() -> int:654 parser = argparse.ArgumentParser(655 description="Collect or inspect serving bundles and dumps for SGLang debug."656 )657 subparsers = parser.add_subparsers(dest="command", required=True)658 659 collect_parser = subparsers.add_parser(660 "collect-bundle", help="Collect a read-only live bundle from a running server"661 )662 collect_parser.add_argument("--base-url", required=True)663 collect_parser.add_argument(664 "--token",665 default=os.environ.get("SGLANG_BEARER_TOKEN"),666 help="Bearer token for protected endpoints. Defaults to $SGLANG_BEARER_TOKEN.",667 )668 collect_parser.add_argument("--outdir", default=None)669 collect_parser.add_argument("--timeout", type=float, default=10.0)670 671 bundle_parser = subparsers.add_parser(672 "summarize-bundle", help="Summarize a bundle directory"673 )674 bundle_parser.add_argument("bundle_dir")675 bundle_parser.add_argument("--out", default=None)676 bundle_parser.add_argument("--json-out", default=None)677 bundle_parser.add_argument("--stdout-json", action="store_true")678 679 dump_parser = subparsers.add_parser(680 "summarize-dump", help="Summarize a trusted request dump or crash dump"681 )682 dump_parser.add_argument("--input-file", default=None)683 dump_parser.add_argument("--input-folder", default=None)684 dump_parser.add_argument("--max-requests", type=int, default=20)685 dump_parser.add_argument("--preview-chars", type=int, default=160)686 687 args = parser.parse_args()688 689 if args.command == "collect-bundle":690 bundle_dir = collect_bundle(691 args.base_url, args.token, args.outdir, args.timeout692 )693 print(bundle_dir)694 return 0695 696 if args.command == "summarize-bundle":697 bundle_dir = Path(args.bundle_dir).resolve()698 if not bundle_dir.is_dir():699 raise SystemExit(700 f"bundle_dir does not exist or is not a directory: {bundle_dir}"701 )702 summary = build_bundle_summary(bundle_dir)703 out_text = render_bundle_text(summary)704 text_path = Path(args.out) if args.out else bundle_dir / "SUMMARY_REPORT.txt"705 json_path = (706 Path(args.json_out) if args.json_out else bundle_dir / "SUMMARY_REPORT.json"707 )708 text_path.write_text(out_text, encoding="utf-8")709 json_path.write_text(710 json.dumps(summary, indent=2, ensure_ascii=False) + "\n",711 encoding="utf-8",712 )713 if args.stdout_json:714 print(json.dumps(summary, indent=2, ensure_ascii=False))715 else:716 print(out_text, end="")717 return 0718 719 files = iter_dump_files(args.input_file, args.input_folder)720 if not files:721 raise SystemExit("No .pkl files matched the provided input.")722 for idx, path in enumerate(files):723 if idx:724 print()725 print(726 summarize_dump_file(727 path=path,728 max_requests=args.max_requests,729 preview_chars=args.preview_chars,730 )731 )732 return 0733 734 735if __name__ == "__main__":736 raise SystemExit(main())737 Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 63.SKILL.mdView in source ↗63```bash64python3 scripts/incident_artifact_tool.py collect-bundle \65 --base-url http://127.0.0.1:30000 \
Source excerpt starting at line 68.SKILL.mdView in source ↗68python3 scripts/incident_artifact_tool.py summarize-bundle \69 /tmp/incident_bundle
Source excerpt starting at line 74.SKILL.mdView in source ↗74```bash75python3 scripts/incident_artifact_tool.py collect-bundle \76 --base-url http://127.0.0.1:30000 \
Source excerpt starting at line 157.SKILL.mdView in source ↗157```bash158python3 scripts/incident_artifact_tool.py summarize-dump \159 --input-file /path/to/crash_dump.pkl
Source excerpt starting at line 282.282- [scripts/incident_artifact_tool.py](scripts/incident_artifact_tool.py)283 - collect a read-only live bundle