Skip to content

feat(metrics): report startup crash cause in not-running alarm#168

Merged
linrunqi08 merged 2 commits into
alibaba:mainfrom
Snssn:feat/report-startup-crash-cause
Jul 22, 2026
Merged

feat(metrics): report startup crash cause in not-running alarm#168
linrunqi08 merged 2 commits into
alibaba:mainfrom
Snssn:feat/report-startup-crash-cause

Conversation

@Snssn

@Snssn Snssn commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

When the collector dies during startup — most notably when the sqlite3 native module fails to load under npm12's stricter install-script policy — the only signal today is a generic SERVICE_NOT_RUNNING_ALARM with the message loongsuite-pilot collector process is not running. The real cause is written to the local service log and then lost, so operators cannot distinguish a native-module failure from an OOM kill, a corrupt config, or an intentional stop.

The root problem is that the process that knows the cause (the dying collector) is not the one that sends the alarm (the updater), and the fatal case happens during ESM module-graph resolution — before main() runs — so no in-process telemetry is available.

This PR bridges that gap with a lightweight crash breadcrumb.

What changes

  • scripts/collector-daemon.js: in the bootstrap import().catch, write ~/.loongsuite-pilot/logs/last-startup-crash.json (phase=module_load). This is the only place that can observe a fatal module-load failure (e.g. sqlite3). Best-effort, atomic write; never masks the original error.
  • src/index.ts: write the breadcrumb (phase=startup) in main().catch, and clear it after a healthy start. A lingering breadcrumb therefore always reflects the most recent failed startup — PID/version-independent, so it stays correct across auto-update version swaps. Clean SIGTERM shutdown writes no breadcrumb.
  • src/updater/updater-metrics.ts: after the existing fix(metrics): reduce not-running alarm false positives #133 debounce (startup grace + consecutive-failure threshold + cooldown) has confirmed the collector is genuinely absent, read the breadcrumb, classify the raw error into a stable reason, and append cause / detail / phase / version to the SERVICE_NOT_RUNNING_ALARM message.

Reason labels: native_module_missing (sqlite3/npm12), module_not_found, config_error, permission_or_disk, unknown (carries the raw message head verbatim).

Design notes:

Example enriched message:

loongsuite-pilot collector process is not running after 2 checks: pid file is missing; no matching process found | cause=native_module_missing detail="Cannot open sqlite3 .node ..." phase=module_load version=1.2.3

Test plan

  • npm run typecheck — no new errors from these files (one pre-existing unrelated error in otlp-trace-flusher.ts).
  • Unit tests: tests/unit/utils/crash-breadcrumb.test.ts, tests/unit/updater/startup-crash-classifier.test.ts, and a new enrichment case in tests/unit/updater/updater-metrics.test.ts (29 tests pass).
  • Local smoke: ran the real collector-daemon.js against an isolated data dir with a simulated sqlite3 load failure → breadcrumb written with phase=module_load; verified the updater read+classify produced cause=native_module_missing; healthy start clears the breadcrumb.

When the collector dies during startup (e.g. sqlite3 native module failing
to load under npm12), the updater only reported a generic "collector process
is not running", losing the real cause.

Record the cause as a breadcrumb the updater can read and surface it in the
alarm message:

- collector-daemon.js: write last-startup-crash.json (phase=module_load) in
  the bootstrap import().catch — the only place that observes a fatal
  module-graph load failure before main() runs.
- index.ts: write the breadcrumb (phase=startup) in main().catch and clear it
  after a healthy start, so a lingering breadcrumb always reflects the most
  recent failed startup (PID/version-independent, survives auto-update).
- updater-metrics: after the existing alibaba#133 debounce confirms absence, classify
  the breadcrumb (native_module_missing / module_not_found / config_error /
  permission_or_disk / unknown) and append cause/detail/phase/version to the
  SERVICE_NOT_RUNNING_ALARM message.

Message-only enrichment: no new alarm type and no AlarmEntry schema change.
Complements alibaba#164 (validate sqlite3 before activation) by making the failure
observable when it still happens. Adds unit tests for the breadcrumb writer,
classifier, and alarm enrichment.
Comment thread src/updater/startup-crash-classifier.ts Outdated
}
if (
phase === 'startup'
&& (text.includes('json') || text.includes('unexpected token') || text.includes('config'))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] config_error 分类过宽:此处对 text.includes(...) 匹配的 texterror_message + error_stack_head 的全量 lowercase(见 classifyStartupCrash 的 haystack)。启动期错误消息/栈里出现 json/config 子串(如解析 manifest/state 的 Unexpected end of JSON input、消息含 config 路径/字段名)会被误标为 config_error;且该判定排在下方 permission_or_disk 之前,导致「startup 期 EACCES 且消息含 config」被误标为配置错误,削弱本 PR 的分诊价值。

影响: 运维按 cause= 分诊被误导,config_error 假阳性偏高。
建议: (1) 仅对 error_message(不含 stack)匹配;(2) 用错误类型/码(SyntaxError 且含 JSON)替代裸子串 config;(3) 将 permission_or_disk(eacces/erofs/enospc)判定移到 config_error 之前。

Generated by LoongSuite-Pilot Code Review Agent


// Resolve the data dir the updater will read the breadcrumb from (honors override).
function resolveDataDir() {
const raw = process.env.LOONGSUITE_PILOT_DATA_DIR;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] dataDir 三处解析不一致。这里的 resolveDataDir()src/updater/index.ts 都只认 env(LOONGSUITE_PILOT_DATA_DIR) ?? ~/.loongsuite-pilot,而 src/index.ts 的写入/清除用 resolveHome(config.dataDir),其中 config.dataDir = env ?? file.dataDir ?? default(config-loader.ts:208)。当用户仅在 config.json 设置 dataDir(不设 env)时三者发散:

  • (A) module_load 面包屑由本文件写到默认目录,但健康启动后 clearStartupCrash(config.dataDir) 清的是自定义目录 → 面包屑永不被清除,违反「lingering breadcrumb = 最近一次失败启动」不变式;后续一次无关的 not-running(手动停止/OOM)告警会读到陈旧面包屑并误标 cause=native_module_missing
  • (B) startup phase 面包屑写到自定义 config.dataDir,但 updater 从默认目录读 → 读不到,增强静默失效。

影响: config.json 覆盖 dataDir 场景下,清除失效导致陈旧原因误报 + startup-phase 增强丢失。
建议: 收敛到单一 dataDir 解析源——让本文件与 updater/index.ts 也读取 config.jsonfile.dataDir(与 config-loader 对齐),或让 index.ts 的写/清除改用相同的 env ?? default 解析。务必让「写/清除/读」落在同一目录。

Generated by LoongSuite-Pilot Code Review Agent

@linrunqi08

Copy link
Copy Markdown
Collaborator

🔍 Code Review Summary

Severity Count
Critical 0
High 0
Medium 2
Low 3

Lifecycle Verdict

Check Result
资源释放 PASS
死锁/卡死风险 PASS
状态恢复正确性 FAIL

状态恢复正确性 FAIL 证据: PR 中心不变式「lingering breadcrumb 始终反映最近一次失败启动」在 config.json 覆盖 dataDir(无 env)场景下不成立——clearStartupCrash 清除的目录(config.dataDir)与 collector-daemon.js 写入 module_load 面包屑的目录(env/default)不一致(Medium #1);且 !config.enabled 早退路径不清除面包屑(Low)。默认 dataDir 路径正确。

总体结论

不阻断合入,无 Critical/High。核心默认路径(默认 dataDir + sqlite3 module_load 场景)逻辑正确且有单测覆盖。原子写 + 全链路 best-effort、复用既有告警类型不改 schema、与 #133 去抖正交组合——设计干净。

主要风险集中在边界与分诊精度,建议合入前处理两条 Medium(已行内标注):

  1. dataDir 三处解析不一致(collector-daemon.js / updater/index.ts 只认 env-or-default,index.tsconfig.dataDir)→ config.json 覆盖场景下清除失效(陈旧原因误报)+ startup-phase 增强丢失。
  2. config_error 分类过宽(对 message+stack 全量匹配裸子串 json/config,且排在 permission_or_disk 之前)→ 分诊假阳性。

可后续改进(Low):!config.enabled 早退清除面包屑;基于 ts 的新鲜度约束;告警消息 detail="..." 转义。

Highlights(正向实践)

  • 原子写(tmp + renameSync)+ 全链路 best-effort try/catch:读取无撕裂,写入绝不掩盖原始错误或改退出码。
  • 复用 SERVICE_NOT_RUNNING_ALARM,零 schema 变更,与 fix(metrics): reduce not-running alarm false positives #133 正交、向后兼容。
  • 分类器 unknown 兜底携带原始消息首行,信息不丢失。
  • 单测覆盖到位(写/读/清除/不可写目录/各分类分支/告警增强),29 例通过。

评审报告详见: code-review/pr-168/final-report.md
Generated by LoongSuite-Pilot Code Review Agent

…ause classification

Medium alibaba#1 (dataDir divergence): the breadcrumb reader (updater) and the daemon
writer both use env-or-default, but index.ts used config.dataDir (which includes
file.dataDir). When dataDir is set only in config.json, clear-on-success and the
startup-phase write missed the reader's directory, breaking the "lingering
breadcrumb = latest failed startup" invariant. Introduce resolveBreadcrumbDataDir()
(env ?? ~/.loongsuite-pilot) and use it for all write/clear in index.ts.

Medium alibaba#2 (config_error too broad): reorder permission_or_disk before config_error,
and match config_error only on a JSON-parse signature in the error message (not the
full stack, which routinely contains config-loader.ts paths). Drop bare
"config"/"json" substrings.

Low alibaba#1: clear the breadcrumb on the `!config.enabled` deliberate-exit path.
Low alibaba#3: sanitize detailHead (strip quotes/control chars, collapse whitespace) so it
is safe to embed in the alarm `detail="..."`.

Update classifier tests for the narrowed rules, ordering, stack-path safety and
detail sanitization.
@linrunqi08

Copy link
Copy Markdown
Collaborator

✅ Fixes Confirmed — Round 2 (f1e0b0d)

复核修复提交 f1e0b0df1 "address review — consistent breadcrumb dir & narrower classifier",上轮 review 意见已处理:

Finding 严重度 状态 说明
#1 dataDir 三处解析不一致 Medium ✅ fixed 新增 resolveBreadcrumbDataDir()(env-or-default),index.ts 写/清除改用它,与 daemon writer + updater reader 对齐;不再用 config.dataDir。不变式在 config.json 覆盖 dataDir 场景下现已成立
#2 config_error 分类过宽 Medium ✅ fixed 收紧为仅匹配 message(不含 stack)中的 JSON-parse 签名;permission_or_disk 前置到 config_error 之前
#3 !config.enabled 早退不清除 Low ✅ fixed disabled 分支补上 clearStartupCrash(...)
#5 detail="..." 未转义 Low ✅ fixed 新增 sanitizeDetail() 去引号/控制字符
#4 ts 新鲜度约束 Low ⬜ 未做(可接受) #1/#3 修复后陈旧面包屑窗口已显著收窄,残余风险低,属可选优化

测试:分类器新增 4 个针对性用例(bare "config" 不触发、config-loader.ts 栈路径不误判、EACCES+config.json 优先 permission、detail 转义)。本地 vitest run(node 22):33 passed / 3 files

Lifecycle Verdict:状态恢复正确性 FAIL → PASS。无阻断项,LGTM ✅

Generated by LoongSuite-Pilot Code Review Agent

@linrunqi08
linrunqi08 merged commit c984555 into alibaba:main Jul 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants