@milaboratories/pl-flight-recorder 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/analyze.d.ts +133 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +525 -0
- package/dist/analyze.js.map +1 -0
- package/dist/data_summary.d.ts +28 -0
- package/dist/data_summary.d.ts.map +1 -0
- package/dist/data_summary.js +73 -0
- package/dist/data_summary.js.map +1 -0
- package/dist/digest.d.ts +28 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +55 -0
- package/dist/digest.js.map +1 -0
- package/dist/events.d.ts +89 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/instrument.d.ts +82 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +313 -0
- package/dist/instrument.js.map +1 -0
- package/dist/recorder.d.ts +82 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +293 -0
- package/dist/recorder.js.map +1 -0
- package/dist/redact.d.ts +53 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +145 -0
- package/dist/redact.js.map +1 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +377 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +73 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +245 -0
- package/dist/rules.js.map +1 -0
- package/dist/sampler.d.ts +22 -0
- package/dist/sampler.d.ts.map +1 -0
- package/dist/sampler.js +33 -0
- package/dist/sampler.js.map +1 -0
- package/dist/sampler_thread.d.ts +1 -0
- package/dist/sampler_thread.js +41 -0
- package/dist/sampler_thread.js.map +1 -0
- package/dist/session.d.ts +40 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/supervisor.d.ts +58 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +108 -0
- package/dist/supervisor.js.map +1 -0
- package/package.json +43 -0
- package/src/analyze.test.ts +539 -0
- package/src/analyze.ts +795 -0
- package/src/data_summary.ts +110 -0
- package/src/digest.ts +49 -0
- package/src/events.ts +102 -0
- package/src/index.ts +104 -0
- package/src/instrument.ts +442 -0
- package/src/recorder.ts +397 -0
- package/src/redact.test.ts +155 -0
- package/src/redact.ts +213 -0
- package/src/report.ts +512 -0
- package/src/rules.test.ts +182 -0
- package/src/rules.ts +383 -0
- package/src/sampler.ts +40 -0
- package/src/sampler_thread.ts +45 -0
- package/src/session.ts +69 -0
- package/src/supervisor.ts +150 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { CRASH_FILE_PREFIX } from "./events.js";
|
|
2
|
+
import { listSessions, sessionIdFromFile } from "./recorder.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region src/supervisor.ts
|
|
6
|
+
/**
|
|
7
|
+
* Records an abnormal end observed from outside the dying thread.
|
|
8
|
+
*
|
|
9
|
+
* A thread that runs out of heap cannot describe its own death: the last reading
|
|
10
|
+
* it wrote predates the blow-up, and when the blow-up is synchronous no sampler
|
|
11
|
+
* tick of its own lands either. The parent is the only place where the cause is
|
|
12
|
+
* known rather than inferred — Node reports `ERR_WORKER_OUT_OF_MEMORY` to it —
|
|
13
|
+
* so the parent writes the verdict down on the dead thread's behalf.
|
|
14
|
+
*/
|
|
15
|
+
function writeCrashMarker(dir, input = {}) {
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
const error = input.error;
|
|
18
|
+
const marker = {
|
|
19
|
+
type: "external-crash",
|
|
20
|
+
wall: Date.now(),
|
|
21
|
+
sessionId: input.sessionId,
|
|
22
|
+
guessedSessionId: input.sessionId === void 0 ? newestOpenSessionId(dir) : void 0,
|
|
23
|
+
reason: input.reason ?? classifyReason(input),
|
|
24
|
+
errorCode: error?.code,
|
|
25
|
+
errorName: error?.name,
|
|
26
|
+
message: truncate(String(error?.message ?? input.error ?? ""), 2e3),
|
|
27
|
+
exitCode: input.code,
|
|
28
|
+
signal: input.signal,
|
|
29
|
+
stderrTail: truncate(input.stderrTail ?? "", 4e3)
|
|
30
|
+
};
|
|
31
|
+
const file = path.join(dir, `${CRASH_FILE_PREFIX}-${marker.wall}.ndjson`);
|
|
32
|
+
fs.writeFileSync(file, `${JSON.stringify(marker)}\n`);
|
|
33
|
+
return file;
|
|
34
|
+
}
|
|
35
|
+
/** Crash markers in a directory, oldest first. */
|
|
36
|
+
function readCrashMarkers(dir) {
|
|
37
|
+
let names;
|
|
38
|
+
try {
|
|
39
|
+
names = fs.readdirSync(dir);
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const markers = [];
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
if (!name.startsWith(`crash-`) || !name.endsWith(".ndjson")) continue;
|
|
46
|
+
try {
|
|
47
|
+
const first = fs.readFileSync(path.join(dir, name), "utf8").split("\n")[0];
|
|
48
|
+
markers.push(JSON.parse(first));
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
return markers.sort((lhs, rhs) => lhs.wall - rhs.wall);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Attaches crash recording to a middle-layer worker thread.
|
|
55
|
+
*
|
|
56
|
+
* A worker whose isolate exhausts its heap dies alone and the parent receives
|
|
57
|
+
* `ERR_WORKER_OUT_OF_MEMORY`, with or without `resourceLimits`. What
|
|
58
|
+
* `resourceLimits.maxOldGenerationSizeMb` adds is a chosen ceiling: V8's default
|
|
59
|
+
* is several gigabytes, so on a small machine the OS can run out of memory and
|
|
60
|
+
* kill the whole process before V8 ever reports the worker's heap as full — and
|
|
61
|
+
* then there is no parent left to write anything.
|
|
62
|
+
*/
|
|
63
|
+
function superviseWorker(worker, dir, options = {}) {
|
|
64
|
+
let recorded = false;
|
|
65
|
+
worker.on("error", (error) => {
|
|
66
|
+
recorded = true;
|
|
67
|
+
const markerFile = writeCrashMarker(dir, {
|
|
68
|
+
error,
|
|
69
|
+
sessionId: options.sessionId
|
|
70
|
+
});
|
|
71
|
+
options.onCrash?.({
|
|
72
|
+
kind: "error",
|
|
73
|
+
error,
|
|
74
|
+
markerFile
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
worker.on("exit", (code) => {
|
|
78
|
+
if (code === 0 || recorded) return;
|
|
79
|
+
const markerFile = writeCrashMarker(dir, {
|
|
80
|
+
reason: "worker-exit",
|
|
81
|
+
code,
|
|
82
|
+
sessionId: options.sessionId
|
|
83
|
+
});
|
|
84
|
+
options.onCrash?.({
|
|
85
|
+
kind: "exit",
|
|
86
|
+
code,
|
|
87
|
+
markerFile
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function newestOpenSessionId(dir) {
|
|
92
|
+
const open = listSessions(dir).find((session) => session.crashed);
|
|
93
|
+
return open ? sessionIdFromFile(open.file) : void 0;
|
|
94
|
+
}
|
|
95
|
+
function classifyReason({ error, code, signal }) {
|
|
96
|
+
if (error?.code === "ERR_WORKER_OUT_OF_MEMORY") return "js-heap-out-of-memory";
|
|
97
|
+
if (signal === "SIGKILL") return "killed-by-os";
|
|
98
|
+
if (signal === "SIGABRT" || code === 134) return "abort-or-fatal-allocation-failure";
|
|
99
|
+
if (typeof code === "number" && code !== 0) return "nonzero-exit";
|
|
100
|
+
return "unknown";
|
|
101
|
+
}
|
|
102
|
+
function truncate(value, limit) {
|
|
103
|
+
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { readCrashMarkers, superviseWorker, writeCrashMarker };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.js","names":[],"sources":["../src/supervisor.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { CRASH_FILE_PREFIX, type CrashMarker, type CrashReason } from \"./events\";\nimport { listSessions, sessionIdFromFile } from \"./recorder\";\n\nexport type CrashMarkerInput = {\n /** Session id the parent assigned to the worker. Omitted, the marker carries no identity. */\n sessionId?: string;\n reason?: CrashReason;\n error?: (Error & { code?: string }) | unknown;\n code?: number;\n signal?: string;\n stderrTail?: string;\n};\n\nexport type SuperviseOptions = {\n /**\n * The session id handed to the worker at spawn (see `FLIGHT_SESSION_ENV`).\n * With it the marker names the dying session with certainty. Without it the\n * analyzer has to attribute the marker by timing, and will decline to\n * attribute it at all when more than one session looks dead.\n */\n sessionId?: string;\n onCrash?: (info: {\n kind: \"error\" | \"exit\";\n markerFile: string;\n error?: unknown;\n code?: number;\n }) => void;\n};\n\n/** Minimal view of a worker, so callers are not forced to import worker_threads. */\nexport type SupervisedWorker = {\n on(event: \"error\", listener: (error: Error) => void): unknown;\n on(event: \"exit\", listener: (code: number) => void): unknown;\n};\n\n/**\n * Records an abnormal end observed from outside the dying thread.\n *\n * A thread that runs out of heap cannot describe its own death: the last reading\n * it wrote predates the blow-up, and when the blow-up is synchronous no sampler\n * tick of its own lands either. The parent is the only place where the cause is\n * known rather than inferred — Node reports `ERR_WORKER_OUT_OF_MEMORY` to it —\n * so the parent writes the verdict down on the dead thread's behalf.\n */\nexport function writeCrashMarker(dir: string, input: CrashMarkerInput = {}): string {\n fs.mkdirSync(dir, { recursive: true });\n const error = input.error as (Error & { code?: string }) | undefined;\n // Only an id the parent handed to the worker is certain, and only a certain\n // id goes in `sessionId`. Reading the newest open flight log names whichever\n // session wrote last, which a concurrent live session makes wrong; recorded\n // as identity that would misattribute the death and, worse, stop the session\n // that actually died from claiming the marker. So it is advisory only.\n const marker: CrashMarker = {\n type: \"external-crash\",\n wall: Date.now(),\n sessionId: input.sessionId,\n guessedSessionId: input.sessionId === undefined ? newestOpenSessionId(dir) : undefined,\n reason: input.reason ?? classifyReason(input),\n errorCode: error?.code,\n errorName: error?.name,\n message: truncate(String(error?.message ?? input.error ?? \"\"), 2000),\n exitCode: input.code,\n signal: input.signal,\n stderrTail: truncate(input.stderrTail ?? \"\", 4000),\n };\n const file = path.join(dir, `${CRASH_FILE_PREFIX}-${marker.wall}.ndjson`);\n fs.writeFileSync(file, `${JSON.stringify(marker)}\\n`);\n return file;\n}\n\n/** Crash markers in a directory, oldest first. */\nexport function readCrashMarkers(dir: string): CrashMarker[] {\n let names: string[];\n try {\n names = fs.readdirSync(dir);\n } catch {\n return [];\n }\n const markers: CrashMarker[] = [];\n for (const name of names) {\n if (!name.startsWith(`${CRASH_FILE_PREFIX}-`) || !name.endsWith(\".ndjson\")) continue;\n try {\n const first = fs.readFileSync(path.join(dir, name), \"utf8\").split(\"\\n\")[0];\n markers.push(JSON.parse(first) as CrashMarker);\n } catch {\n // A marker that cannot be parsed is skipped; it is one line of evidence,\n // not the report.\n }\n }\n return markers.sort((lhs, rhs) => lhs.wall - rhs.wall);\n}\n\n/**\n * Attaches crash recording to a middle-layer worker thread.\n *\n * A worker whose isolate exhausts its heap dies alone and the parent receives\n * `ERR_WORKER_OUT_OF_MEMORY`, with or without `resourceLimits`. What\n * `resourceLimits.maxOldGenerationSizeMb` adds is a chosen ceiling: V8's default\n * is several gigabytes, so on a small machine the OS can run out of memory and\n * kill the whole process before V8 ever reports the worker's heap as full — and\n * then there is no parent left to write anything.\n */\nexport function superviseWorker(\n worker: SupervisedWorker,\n dir: string,\n options: SuperviseOptions = {},\n): void {\n // One death fires `error` and then `exit`. Only `error` carries the cause, so\n // a later `exit` must not overwrite it with a bare exit code.\n let recorded = false;\n worker.on(\"error\", (error: Error) => {\n recorded = true;\n const markerFile = writeCrashMarker(dir, { error, sessionId: options.sessionId });\n options.onCrash?.({ kind: \"error\", error, markerFile });\n });\n worker.on(\"exit\", (code: number) => {\n if (code === 0 || recorded) return;\n const markerFile = writeCrashMarker(dir, {\n reason: \"worker-exit\",\n code,\n sessionId: options.sessionId,\n });\n options.onCrash?.({ kind: \"exit\", code, markerFile });\n });\n}\n\n// Internals\n\n// Advisory only, for a human reading a directory by hand: the dying session has\n// no terminating record, so among the sessions that look dead this names the one\n// that wrote last. Never used as identity — see `CrashMarker.guessedSessionId`.\nfunction newestOpenSessionId(dir: string): string | undefined {\n const open = listSessions(dir).find((session) => session.crashed);\n return open ? sessionIdFromFile(open.file) : undefined;\n}\n\nfunction classifyReason({ error, code, signal }: CrashMarkerInput): CrashReason {\n const errorCode = (error as { code?: string } | undefined)?.code;\n if (errorCode === \"ERR_WORKER_OUT_OF_MEMORY\") return \"js-heap-out-of-memory\";\n if (signal === \"SIGKILL\") return \"killed-by-os\";\n if (signal === \"SIGABRT\" || code === 134) return \"abort-or-fatal-allocation-failure\";\n if (typeof code === \"number\" && code !== 0) return \"nonzero-exit\";\n return \"unknown\";\n}\n\nfunction truncate(value: string, limit: number): string {\n return value.length > limit ? `${value.slice(0, limit)}…` : value;\n}\n"],"mappings":";;;;;;;;;;;;;;AA8CA,SAAgB,iBAAiB,KAAa,QAA0B,CAAC,GAAW;CAClF,GAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;CACrC,MAAM,QAAQ,MAAM;CAMpB,MAAM,SAAsB;EAC1B,MAAM;EACN,MAAM,KAAK,IAAI;EACf,WAAW,MAAM;EACjB,kBAAkB,MAAM,cAAc,KAAA,IAAY,oBAAoB,GAAG,IAAI,KAAA;EAC7E,QAAQ,MAAM,UAAU,eAAe,KAAK;EAC5C,WAAW,OAAO;EAClB,WAAW,OAAO;EAClB,SAAS,SAAS,OAAO,OAAO,WAAW,MAAM,SAAS,EAAE,GAAG,GAAI;EACnE,UAAU,MAAM;EAChB,QAAQ,MAAM;EACd,YAAY,SAAS,MAAM,cAAc,IAAI,GAAI;CACnD;CACA,MAAM,OAAO,KAAK,KAAK,KAAK,GAAG,kBAAkB,GAAG,OAAO,KAAK,QAAQ;CACxE,GAAG,cAAc,MAAM,GAAG,KAAK,UAAU,MAAM,EAAE,GAAG;CACpD,OAAO;AACT;;AAGA,SAAgB,iBAAiB,KAA4B;CAC3D,IAAI;CACJ,IAAI;EACF,QAAQ,GAAG,YAAY,GAAG;CAC5B,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,UAAyB,CAAC;CAChC,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,KAAK,WAAW,QAAuB,KAAK,CAAC,KAAK,SAAS,SAAS,GAAG;EAC5E,IAAI;GACF,MAAM,QAAQ,GAAG,aAAa,KAAK,KAAK,KAAK,IAAI,GAAG,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;GACxE,QAAQ,KAAK,KAAK,MAAM,KAAK,CAAgB;EAC/C,QAAQ,CAGR;CACF;CACA,OAAO,QAAQ,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,IAAI;AACvD;;;;;;;;;;;AAYA,SAAgB,gBACd,QACA,KACA,UAA4B,CAAC,GACvB;CAGN,IAAI,WAAW;CACf,OAAO,GAAG,UAAU,UAAiB;EACnC,WAAW;EACX,MAAM,aAAa,iBAAiB,KAAK;GAAE;GAAO,WAAW,QAAQ;EAAU,CAAC;EAChF,QAAQ,UAAU;GAAE,MAAM;GAAS;GAAO;EAAW,CAAC;CACxD,CAAC;CACD,OAAO,GAAG,SAAS,SAAiB;EAClC,IAAI,SAAS,KAAK,UAAU;EAC5B,MAAM,aAAa,iBAAiB,KAAK;GACvC,QAAQ;GACR;GACA,WAAW,QAAQ;EACrB,CAAC;EACD,QAAQ,UAAU;GAAE,MAAM;GAAQ;GAAM;EAAW,CAAC;CACtD,CAAC;AACH;AAOA,SAAS,oBAAoB,KAAiC;CAC5D,MAAM,OAAO,aAAa,GAAG,CAAC,CAAC,MAAM,YAAY,QAAQ,OAAO;CAChE,OAAO,OAAO,kBAAkB,KAAK,IAAI,IAAI,KAAA;AAC/C;AAEA,SAAS,eAAe,EAAE,OAAO,MAAM,UAAyC;CAE9E,IADmB,OAAyC,SAC1C,4BAA4B,OAAO;CACrD,IAAI,WAAW,WAAW,OAAO;CACjC,IAAI,WAAW,aAAa,SAAS,KAAK,OAAO;CACjD,IAAI,OAAO,SAAS,YAAY,SAAS,GAAG,OAAO;CACnD,OAAO;AACT;AAEA,SAAS,SAAS,OAAe,OAAuB;CACtD,OAAO,MAAM,SAAS,QAAQ,GAAG,MAAM,MAAM,GAAG,KAAK,EAAE,KAAK;AAC9D"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@milaboratories/pl-flight-recorder",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Crash-survivable diagnostics for the block model layer: records join shapes and memory, and explains an out-of-memory death after the fact",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"./dist/**/*",
|
|
9
|
+
"./src/**/*"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@milaboratories/pl-model-common": "1.48.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@vitest/coverage-istanbul": "^4.1.3",
|
|
26
|
+
"typescript": "7.0.2",
|
|
27
|
+
"vitest": "^4.1.3",
|
|
28
|
+
"@milaboratories/build-configs": "2.0.1",
|
|
29
|
+
"@milaboratories/ts-configs": "1.4.0",
|
|
30
|
+
"@milaboratories/ts-builder": "1.7.2"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "ts-builder build --target node --build-config build.config.js",
|
|
34
|
+
"watch": "ts-builder build --target node --build-config build.config.js --watch",
|
|
35
|
+
"check": "ts-builder check --target node",
|
|
36
|
+
"formatter:check": "ts-builder formatter --check",
|
|
37
|
+
"linter:check": "ts-builder linter --check",
|
|
38
|
+
"types:check": "ts-builder type-check --target node",
|
|
39
|
+
"test": "vitest run --coverage",
|
|
40
|
+
"do-pack": "rm -f *.tgz && pnpm pack && mv *.tgz package.tgz",
|
|
41
|
+
"fmt": "ts-builder format"
|
|
42
|
+
}
|
|
43
|
+
}
|