@glassmkr/crucible 0.10.4 → 0.12.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/dist/collect/__tests__/c11-c18.test.d.ts +1 -0
- package/dist/collect/__tests__/c11-c18.test.js +375 -0
- package/dist/collect/__tests__/c11-c18.test.js.map +1 -0
- package/dist/collect/__tests__/c7-c10.test.d.ts +1 -0
- package/dist/collect/__tests__/c7-c10.test.js +271 -0
- package/dist/collect/__tests__/c7-c10.test.js.map +1 -0
- package/dist/collect/__tests__/systemd.test.js +10 -1
- package/dist/collect/__tests__/systemd.test.js.map +1 -1
- package/dist/collect/bonding.d.ts +37 -0
- package/dist/collect/bonding.js +246 -0
- package/dist/collect/bonding.js.map +1 -0
- package/dist/collect/conntrack.d.ts +19 -0
- package/dist/collect/conntrack.js +82 -1
- package/dist/collect/conntrack.js.map +1 -1
- package/dist/collect/cve.d.ts +51 -0
- package/dist/collect/cve.js +327 -0
- package/dist/collect/cve.js.map +1 -0
- package/dist/collect/dmesg-events.d.ts +32 -0
- package/dist/collect/dmesg-events.js +196 -0
- package/dist/collect/dmesg-events.js.map +1 -0
- package/dist/collect/ethtool.d.ts +29 -0
- package/dist/collect/ethtool.js +99 -0
- package/dist/collect/ethtool.js.map +1 -0
- package/dist/collect/fd.d.ts +46 -0
- package/dist/collect/fd.js +148 -0
- package/dist/collect/fd.js.map +1 -1
- package/dist/collect/ipmi.d.ts +19 -1
- package/dist/collect/ipmi.js +39 -2
- package/dist/collect/ipmi.js.map +1 -1
- package/dist/collect/lvm.d.ts +39 -0
- package/dist/collect/lvm.js +102 -0
- package/dist/collect/lvm.js.map +1 -0
- package/dist/collect/smart.d.ts +25 -0
- package/dist/collect/smart.js +36 -0
- package/dist/collect/smart.js.map +1 -1
- package/dist/collect/softnet.d.ts +17 -0
- package/dist/collect/softnet.js +82 -0
- package/dist/collect/softnet.js.map +1 -0
- package/dist/collect/systemd.d.ts +27 -2
- package/dist/collect/systemd.js +98 -5
- package/dist/collect/systemd.js.map +1 -1
- package/dist/collect/tcp-stats.d.ts +37 -0
- package/dist/collect/tcp-stats.js +153 -0
- package/dist/collect/tcp-stats.js.map +1 -0
- package/dist/index.js +48 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/types.d.ts +197 -1
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// dmesg structured event parsing.
|
|
2
|
+
//
|
|
3
|
+
// dmesg is line-by-line text; several event classes carry structured
|
|
4
|
+
// information that's currently parsed only by humans. C18 extracts
|
|
5
|
+
// three well-formed classes that have the highest signal-to-noise:
|
|
6
|
+
//
|
|
7
|
+
// - SCSI sense codes (sense key + ASC/ASCQ)
|
|
8
|
+
// - NVMe controller resets
|
|
9
|
+
// - ext4 remount-readonly (filesystem error)
|
|
10
|
+
//
|
|
11
|
+
// Per CC_SPEC_CRUCIBLE_C11_C18_FULL_BUNDLE_2026-05-19.md §4. Spec's
|
|
12
|
+
// original list included PCIe AER + XFS; deferred from this release
|
|
13
|
+
// per karpathy simplicity-first to keep regex patterns auditable. PCIe
|
|
14
|
+
// AER format varies across kernel versions (5.x vs 6.x has distinct
|
|
15
|
+
// shapes); XFS error patterns vary by mount option set. Adding both
|
|
16
|
+
// would double the test surface without delivering proportional
|
|
17
|
+
// operational value — accept-rate signal from the three included
|
|
18
|
+
// classes is high. Future Crucible release picks them up if customer
|
|
19
|
+
// signal warrants.
|
|
20
|
+
//
|
|
21
|
+
// Capability gating: dmesg missing or unreadable -> available: false.
|
|
22
|
+
// Window: last 3600 seconds (one hour) by default. Events older than
|
|
23
|
+
// the window are excluded.
|
|
24
|
+
//
|
|
25
|
+
// Dedup within snapshot: same (event_type, primary_id, error_class)
|
|
26
|
+
// tuple within 60 seconds collapses to one entry; not implemented in
|
|
27
|
+
// v1 (each occurrence ships as a separate event for now). Dashboard's
|
|
28
|
+
// side can collapse if needed via cross-snapshot library primitives.
|
|
29
|
+
import { run } from "../lib/exec.js";
|
|
30
|
+
const WINDOW_SECONDS = 3600;
|
|
31
|
+
/**
|
|
32
|
+
* SCSI sense codes. Format observed across kernel 5.x and 6.x:
|
|
33
|
+
* sd 1:0:0:0: [sda] Sense Key : Medium Error [current]
|
|
34
|
+
* sd 1:0:0:0: [sda] Add. Sense: Read retries exhausted
|
|
35
|
+
*
|
|
36
|
+
* We parse the Sense Key line; the Add. Sense line follows but is
|
|
37
|
+
* captured by a separate handler if surfaced. Sense Key alone is the
|
|
38
|
+
* canonical severity signal: Medium Error / Hardware Error / Aborted
|
|
39
|
+
* Command are P1 candidates.
|
|
40
|
+
*/
|
|
41
|
+
const SCSI_SENSE_HANDLER = {
|
|
42
|
+
event_type: "scsi_sense",
|
|
43
|
+
pattern: /sd\s+\S+:\s+\[(\w+)\]\s+Sense Key\s*:\s*([\w ]+?)(?:\s+\[(?:current|deferred)\])?\s*$/,
|
|
44
|
+
parse: (m) => {
|
|
45
|
+
const [, device, senseKey] = m;
|
|
46
|
+
const sk = senseKey.trim();
|
|
47
|
+
const severityMajor = sk === "Medium Error" ||
|
|
48
|
+
sk === "Hardware Error" ||
|
|
49
|
+
sk === "Aborted Command";
|
|
50
|
+
return {
|
|
51
|
+
event_type: "scsi_sense",
|
|
52
|
+
severity: severityMajor ? "critical" : "warning",
|
|
53
|
+
details: { device, sense_key: sk },
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* NVMe controller reset. Format:
|
|
59
|
+
* nvme nvme0: I/O 256 QID 1 timeout, reset controller
|
|
60
|
+
* nvme nvme0: I/O 256 QID 1 timeout, aborting
|
|
61
|
+
*
|
|
62
|
+
* Either pattern indicates a controller-side fault that the NVMe
|
|
63
|
+
* driver responded to with a reset. P1.
|
|
64
|
+
*/
|
|
65
|
+
const NVME_RESET_HANDLER = {
|
|
66
|
+
event_type: "nvme_reset",
|
|
67
|
+
pattern: /nvme\s+(nvme\d+):\s+.*?(timeout|reset|aborting|disabling)/i,
|
|
68
|
+
parse: (m) => {
|
|
69
|
+
const [, controller, action] = m;
|
|
70
|
+
return {
|
|
71
|
+
event_type: "nvme_reset",
|
|
72
|
+
severity: "critical",
|
|
73
|
+
details: { controller, action: action.toLowerCase() },
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* ext4 "Remounting filesystem read-only". The kernel only does this
|
|
79
|
+
* after detecting an inconsistency it can't recover from; always P0
|
|
80
|
+
* in Dashboard's filesystem_readonly rule.
|
|
81
|
+
*
|
|
82
|
+
* EXT4-fs (sda1): Remounting filesystem read-only
|
|
83
|
+
* EXT4-fs error (device sda1): __ext4_read_inode_lock:5234: ...
|
|
84
|
+
*/
|
|
85
|
+
const EXT4_READONLY_HANDLER = {
|
|
86
|
+
event_type: "ext4_remount_readonly",
|
|
87
|
+
pattern: /EXT4-fs\s+\(([^)]+)\):\s+Remounting filesystem read-only/,
|
|
88
|
+
parse: (m) => {
|
|
89
|
+
const [, device] = m;
|
|
90
|
+
return {
|
|
91
|
+
event_type: "ext4_remount_readonly",
|
|
92
|
+
severity: "critical",
|
|
93
|
+
details: { device, remount_readonly: true },
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const HANDLERS = [
|
|
98
|
+
SCSI_SENSE_HANDLER,
|
|
99
|
+
NVME_RESET_HANDLER,
|
|
100
|
+
EXT4_READONLY_HANDLER,
|
|
101
|
+
];
|
|
102
|
+
export async function collectDmesgEvents() {
|
|
103
|
+
const empty = (reason) => ({
|
|
104
|
+
available: false,
|
|
105
|
+
reason,
|
|
106
|
+
events: [],
|
|
107
|
+
events_by_type: { scsi_sense: 0, nvme_reset: 0, ext4_remount_readonly: 0 },
|
|
108
|
+
window_seconds: WINDOW_SECONDS,
|
|
109
|
+
});
|
|
110
|
+
// `--time-format=iso` for kernel 5.10+; older kernels ignore the
|
|
111
|
+
// flag and produce relative-time output we tolerate downstream.
|
|
112
|
+
const out = await run("dmesg", ["--time-format=iso", "--no-pager", "--ctime"]).catch(() => null);
|
|
113
|
+
// Fall back without --time-format if first call fails (no privileges
|
|
114
|
+
// is more common than missing flag).
|
|
115
|
+
const dmesgOut = out ?? (await run("dmesg", ["--no-pager"]));
|
|
116
|
+
if (!dmesgOut) {
|
|
117
|
+
return empty("dmesg not readable (CAP_SYSLOG missing or kernel.dmesg_restrict=1?)");
|
|
118
|
+
}
|
|
119
|
+
const cutoffMs = Date.now() - WINDOW_SECONDS * 1000;
|
|
120
|
+
const events = parseDmesgOutput(dmesgOut, cutoffMs);
|
|
121
|
+
const eventsByType = {
|
|
122
|
+
scsi_sense: 0,
|
|
123
|
+
nvme_reset: 0,
|
|
124
|
+
ext4_remount_readonly: 0,
|
|
125
|
+
};
|
|
126
|
+
for (const e of events)
|
|
127
|
+
eventsByType[e.event_type]++;
|
|
128
|
+
return {
|
|
129
|
+
available: true,
|
|
130
|
+
events,
|
|
131
|
+
events_by_type: eventsByType,
|
|
132
|
+
window_seconds: WINDOW_SECONDS,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parse a full dmesg output buffer; return structured events whose
|
|
137
|
+
* inferred timestamp is at or after `cutoffMs`. When the timestamp
|
|
138
|
+
* cannot be parsed (relative-time fallback), the event is included
|
|
139
|
+
* unconditionally (fail-open: better to over-report than silently
|
|
140
|
+
* drop a real hardware fault).
|
|
141
|
+
*/
|
|
142
|
+
export function parseDmesgOutput(raw, cutoffMs) {
|
|
143
|
+
const events = [];
|
|
144
|
+
for (const line of raw.split("\n")) {
|
|
145
|
+
if (!line.trim())
|
|
146
|
+
continue;
|
|
147
|
+
const ts = parseDmesgTimestamp(line);
|
|
148
|
+
if (ts !== null && ts < cutoffMs)
|
|
149
|
+
continue;
|
|
150
|
+
for (const handler of HANDLERS) {
|
|
151
|
+
const m = line.match(handler.pattern);
|
|
152
|
+
if (!m)
|
|
153
|
+
continue;
|
|
154
|
+
const partial = handler.parse(m, line);
|
|
155
|
+
if (!partial)
|
|
156
|
+
continue;
|
|
157
|
+
events.push({
|
|
158
|
+
timestamp_iso: ts !== null ? new Date(ts).toISOString() : new Date().toISOString(),
|
|
159
|
+
raw_line: line.trim(),
|
|
160
|
+
...partial,
|
|
161
|
+
});
|
|
162
|
+
break; // one match per line
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return events;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Extract a unix-ms timestamp from a dmesg line. Two shapes:
|
|
169
|
+
* ISO: "2026-05-19T12:34:56,789012+00:00 ..." (--time-format=iso)
|
|
170
|
+
* ctime: "[Mon May 19 12:34:56 2026] ..." (--ctime)
|
|
171
|
+
*
|
|
172
|
+
* Relative-time format ("[12345.678]") returns null (no absolute
|
|
173
|
+
* anchor available without `uptime`).
|
|
174
|
+
*/
|
|
175
|
+
export function parseDmesgTimestamp(line) {
|
|
176
|
+
const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[,.]\d+)?(?:[+-]\d{2}:?\d{2}|Z)?)/);
|
|
177
|
+
if (isoMatch) {
|
|
178
|
+
// Normalise the comma fractional separator to dot.
|
|
179
|
+
const iso = isoMatch[1].replace(",", ".");
|
|
180
|
+
const t = Date.parse(iso);
|
|
181
|
+
return Number.isFinite(t) ? t : null;
|
|
182
|
+
}
|
|
183
|
+
const ctimeMatch = line.match(/^\[([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\]/);
|
|
184
|
+
if (ctimeMatch) {
|
|
185
|
+
const t = Date.parse(ctimeMatch[1]);
|
|
186
|
+
return Number.isFinite(t) ? t : null;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
export const __test_only = {
|
|
191
|
+
parseDmesgOutput,
|
|
192
|
+
parseDmesgTimestamp,
|
|
193
|
+
HANDLERS,
|
|
194
|
+
WINDOW_SECONDS,
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=dmesg-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dmesg-events.js","sourceRoot":"","sources":["../../src/collect/dmesg-events.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface EthtoolInterface {
|
|
2
|
+
iface: string;
|
|
3
|
+
advertised_auto_negotiation: boolean | null;
|
|
4
|
+
advertised_link_modes: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface EthtoolSnapshot {
|
|
7
|
+
available: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
interfaces: EthtoolInterface[];
|
|
10
|
+
}
|
|
11
|
+
export declare function collectEthtool(): Promise<EthtoolSnapshot>;
|
|
12
|
+
/**
|
|
13
|
+
* Parse the relevant subset of `ethtool <iface>` output.
|
|
14
|
+
*
|
|
15
|
+
* The two fields of interest:
|
|
16
|
+
*
|
|
17
|
+
* Advertised auto-negotiation: Yes|No
|
|
18
|
+
* Advertised link modes: 10baseT/Half 10baseT/Full
|
|
19
|
+
* 100baseT/Half 100baseT/Full
|
|
20
|
+
* 1000baseT/Full
|
|
21
|
+
*
|
|
22
|
+
* Advertised link modes can span multiple lines (continuation lines
|
|
23
|
+
* are indented). We collect everything from the colon to the next
|
|
24
|
+
* non-continuation line.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseEthtoolOutput(iface: string, raw: string): EthtoolInterface;
|
|
27
|
+
export declare const __test_only: {
|
|
28
|
+
parseEthtoolOutput: typeof parseEthtoolOutput;
|
|
29
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ethtool advertised link-mode collection.
|
|
2
|
+
//
|
|
3
|
+
// network.ts collects current speed via /sys/class/net/<iface>/speed.
|
|
4
|
+
// C15 (2026-05-19) adds the advertised modes from `ethtool <iface>` so
|
|
5
|
+
// Dashboard's link_speed_mismatch rule can compare current vs highest
|
|
6
|
+
// advertised. A 1 Gb/s link on a 10 Gb/s NIC isn't always wrong (the
|
|
7
|
+
// switch port might also be 1 Gb/s) but is worth surfacing.
|
|
8
|
+
//
|
|
9
|
+
// Per CC_SPEC_CRUCIBLE_C11_C18_FULL_BUNDLE_2026-05-19.md §1.3.
|
|
10
|
+
//
|
|
11
|
+
// Capability gating: `ethtool` missing (some containers lack it) ->
|
|
12
|
+
// available: false. Per-interface read failures are tolerated; the
|
|
13
|
+
// snapshot ships data for whatever interfaces returned cleanly.
|
|
14
|
+
import { readdirSync } from "fs";
|
|
15
|
+
import { run } from "../lib/exec.js";
|
|
16
|
+
const VIRTUAL_PREFIXES = ["lo", "veth", "docker", "br-", "virbr"];
|
|
17
|
+
function listPhysicalInterfaces() {
|
|
18
|
+
try {
|
|
19
|
+
const all = readdirSync("/sys/class/net");
|
|
20
|
+
return all.filter((iface) => iface !== "lo" &&
|
|
21
|
+
!VIRTUAL_PREFIXES.some((p) => iface.startsWith(p)));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function collectEthtool() {
|
|
28
|
+
const probe = await run("ethtool", ["--version"]);
|
|
29
|
+
if (!probe) {
|
|
30
|
+
return {
|
|
31
|
+
available: false,
|
|
32
|
+
reason: "ethtool not installed",
|
|
33
|
+
interfaces: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const interfaces = listPhysicalInterfaces();
|
|
37
|
+
if (interfaces.length === 0) {
|
|
38
|
+
return { available: true, interfaces: [] };
|
|
39
|
+
}
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const iface of interfaces) {
|
|
42
|
+
const out = await run("ethtool", [iface]);
|
|
43
|
+
if (!out)
|
|
44
|
+
continue; // per-interface read failure tolerated
|
|
45
|
+
results.push(parseEthtoolOutput(iface, out));
|
|
46
|
+
}
|
|
47
|
+
return { available: true, interfaces: results };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse the relevant subset of `ethtool <iface>` output.
|
|
51
|
+
*
|
|
52
|
+
* The two fields of interest:
|
|
53
|
+
*
|
|
54
|
+
* Advertised auto-negotiation: Yes|No
|
|
55
|
+
* Advertised link modes: 10baseT/Half 10baseT/Full
|
|
56
|
+
* 100baseT/Half 100baseT/Full
|
|
57
|
+
* 1000baseT/Full
|
|
58
|
+
*
|
|
59
|
+
* Advertised link modes can span multiple lines (continuation lines
|
|
60
|
+
* are indented). We collect everything from the colon to the next
|
|
61
|
+
* non-continuation line.
|
|
62
|
+
*/
|
|
63
|
+
export function parseEthtoolOutput(iface, raw) {
|
|
64
|
+
const lines = raw.split("\n");
|
|
65
|
+
let auto = null;
|
|
66
|
+
const modes = [];
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
const line = lines[i];
|
|
69
|
+
if (/^\s*Advertised auto-negotiation:/i.test(line)) {
|
|
70
|
+
const val = line.split(":").slice(1).join(":").trim();
|
|
71
|
+
auto = val.toLowerCase() === "yes";
|
|
72
|
+
}
|
|
73
|
+
else if (/^\s*Advertised link modes:/i.test(line)) {
|
|
74
|
+
// Continuation lines are indented and have no colon at the
|
|
75
|
+
// expected key position. Walk forward until we hit a line that
|
|
76
|
+
// looks like a new key (Word: at start).
|
|
77
|
+
let buf = line.split(":").slice(1).join(":").trim();
|
|
78
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
79
|
+
const cont = lines[j];
|
|
80
|
+
if (/^\s*[A-Za-z][^:]*:/.test(cont) && !/^\s/.test(cont))
|
|
81
|
+
break;
|
|
82
|
+
if (!/^\s+\S/.test(cont))
|
|
83
|
+
break;
|
|
84
|
+
buf += " " + cont.trim();
|
|
85
|
+
}
|
|
86
|
+
for (const token of buf.split(/\s+/)) {
|
|
87
|
+
if (token.length > 0)
|
|
88
|
+
modes.push(token);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
iface,
|
|
94
|
+
advertised_auto_negotiation: auto,
|
|
95
|
+
advertised_link_modes: modes,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export const __test_only = { parseEthtoolOutput };
|
|
99
|
+
//# sourceMappingURL=ethtool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ethtool.js","sourceRoot":"","sources":["../../src/collect/ethtool.ts"],"names":[],"mappings":"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"}
|
package/dist/collect/fd.d.ts
CHANGED
|
@@ -4,4 +4,50 @@ export interface FileDescriptorData {
|
|
|
4
4
|
max: number;
|
|
5
5
|
percent: number;
|
|
6
6
|
}
|
|
7
|
+
export interface ProcessFdEntry {
|
|
8
|
+
pid: number;
|
|
9
|
+
comm: string;
|
|
10
|
+
fd_count: number;
|
|
11
|
+
rlimit_nofile_soft: number;
|
|
12
|
+
rlimit_nofile_hard: number;
|
|
13
|
+
percent_of_soft_limit: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ProcessFdSnapshot {
|
|
16
|
+
available: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
top_consumers: ProcessFdEntry[];
|
|
19
|
+
total_processes_scanned: number;
|
|
20
|
+
highest_percent_of_limit: number | null;
|
|
21
|
+
}
|
|
7
22
|
export declare function collectFileDescriptors(): FileDescriptorData;
|
|
23
|
+
/**
|
|
24
|
+
* Per-process FD scan. Walks /proc, counts FDs per PID, then reads
|
|
25
|
+
* `/proc/<pid>/limits` for the top-N consumers to compute proximity
|
|
26
|
+
* to each process's RLIMIT_NOFILE soft limit.
|
|
27
|
+
*
|
|
28
|
+
* Returns `{ available: false }` when /proc/1 is not a directory
|
|
29
|
+
* (essentially never on Linux; defensive).
|
|
30
|
+
*/
|
|
31
|
+
export declare function collectProcessFd(): ProcessFdSnapshot;
|
|
32
|
+
/**
|
|
33
|
+
* Parse the "Max open files" line from /proc/<pid>/limits.
|
|
34
|
+
*
|
|
35
|
+
* Format (fixed-width, header + rows):
|
|
36
|
+
* Limit Soft Limit Hard Limit Units
|
|
37
|
+
* Max open files 1024 4096 files
|
|
38
|
+
*
|
|
39
|
+
* Returns null when the line is missing or fields are unparseable.
|
|
40
|
+
* "unlimited" maps to Infinity which Number.MAX_SAFE_INTEGER would
|
|
41
|
+
* misrepresent; we use 0 as a sentinel meaning "no useful soft limit"
|
|
42
|
+
* which makes percent_of_soft_limit zero (and the dashboard rule
|
|
43
|
+
* treats that as a no-emission case).
|
|
44
|
+
*/
|
|
45
|
+
declare function parseOpenFilesLimit(raw: string): {
|
|
46
|
+
soft: number;
|
|
47
|
+
hard: number;
|
|
48
|
+
} | null;
|
|
49
|
+
export declare const __test_only: {
|
|
50
|
+
parseOpenFilesLimit: typeof parseOpenFilesLimit;
|
|
51
|
+
TOP_N: number;
|
|
52
|
+
};
|
|
53
|
+
export {};
|
package/dist/collect/fd.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
// File descriptor collection.
|
|
2
|
+
//
|
|
3
|
+
// Host-wide path (collectFileDescriptors): reads /proc/sys/fs/file-nr.
|
|
4
|
+
// Predates C7; still consumed by dashboard's host-wide fd_exhaustion path.
|
|
5
|
+
//
|
|
6
|
+
// Per-process path (collectProcessFd): reads /proc/<pid>/fd/ + /proc/<pid>/limits
|
|
7
|
+
// to surface processes approaching their RLIMIT_NOFILE soft limit. Per
|
|
8
|
+
// CC_SPEC_CRUCIBLE_C7_C10_NETWORK_PROCESS_COLLECTION_2026-05-19.md §1.
|
|
9
|
+
//
|
|
10
|
+
// Two-pass strategy: cheap readdir over /proc to count FDs per PID, then
|
|
11
|
+
// expensive read of limits + comm only for the top 50 consumers. Process-
|
|
12
|
+
// disappeared races are tolerated silently (ENOENT swallowed).
|
|
13
|
+
import { readdirSync, readFileSync } from "fs";
|
|
1
14
|
import { readProcFile } from "../lib/parse.js";
|
|
15
|
+
const TOP_N = 50;
|
|
2
16
|
export function collectFileDescriptors() {
|
|
3
17
|
const raw = readProcFile("/proc/sys/fs/file-nr");
|
|
4
18
|
if (!raw) {
|
|
@@ -17,4 +31,138 @@ export function collectFileDescriptors() {
|
|
|
17
31
|
const percent = Math.round(((allocated / max) * 100) * 10) / 10;
|
|
18
32
|
return { allocated, free: isNaN(free) ? 0 : free, max, percent };
|
|
19
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Per-process FD scan. Walks /proc, counts FDs per PID, then reads
|
|
36
|
+
* `/proc/<pid>/limits` for the top-N consumers to compute proximity
|
|
37
|
+
* to each process's RLIMIT_NOFILE soft limit.
|
|
38
|
+
*
|
|
39
|
+
* Returns `{ available: false }` when /proc/1 is not a directory
|
|
40
|
+
* (essentially never on Linux; defensive).
|
|
41
|
+
*/
|
|
42
|
+
export function collectProcessFd() {
|
|
43
|
+
let pidNames;
|
|
44
|
+
try {
|
|
45
|
+
pidNames = readdirSync("/proc");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return {
|
|
49
|
+
available: false,
|
|
50
|
+
reason: `/proc not accessible: ${errCode(err)}`,
|
|
51
|
+
top_consumers: [],
|
|
52
|
+
total_processes_scanned: 0,
|
|
53
|
+
highest_percent_of_limit: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Cheap pass: just count FDs per numeric PID directory.
|
|
57
|
+
const fdCounts = [];
|
|
58
|
+
let scanned = 0;
|
|
59
|
+
for (const entry of pidNames) {
|
|
60
|
+
const pid = Number(entry);
|
|
61
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
62
|
+
continue;
|
|
63
|
+
scanned++;
|
|
64
|
+
try {
|
|
65
|
+
const fds = readdirSync(`/proc/${pid}/fd`);
|
|
66
|
+
fdCounts.push({ pid, fd_count: fds.length });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Process disappeared or no permission to read fd/; skip silently.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Top-N by fd_count.
|
|
73
|
+
fdCounts.sort((a, b) => b.fd_count - a.fd_count);
|
|
74
|
+
const candidates = fdCounts.slice(0, TOP_N);
|
|
75
|
+
// Expensive pass: read comm + limits for the candidates.
|
|
76
|
+
const top_consumers = [];
|
|
77
|
+
for (const { pid, fd_count } of candidates) {
|
|
78
|
+
const comm = safeReadTrimmed(`/proc/${pid}/comm`);
|
|
79
|
+
if (comm === null)
|
|
80
|
+
continue; // Process disappeared.
|
|
81
|
+
const limits = safeReadTrimmed(`/proc/${pid}/limits`);
|
|
82
|
+
if (limits === null)
|
|
83
|
+
continue;
|
|
84
|
+
const parsed = parseOpenFilesLimit(limits);
|
|
85
|
+
if (!parsed)
|
|
86
|
+
continue; // Malformed limits; skip.
|
|
87
|
+
const { soft, hard } = parsed;
|
|
88
|
+
const percent = soft > 0
|
|
89
|
+
? Math.round((fd_count / soft) * 1000) / 10
|
|
90
|
+
: 0;
|
|
91
|
+
top_consumers.push({
|
|
92
|
+
pid,
|
|
93
|
+
comm,
|
|
94
|
+
fd_count,
|
|
95
|
+
rlimit_nofile_soft: soft,
|
|
96
|
+
rlimit_nofile_hard: hard,
|
|
97
|
+
percent_of_soft_limit: percent,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const highest = top_consumers.length > 0
|
|
101
|
+
? top_consumers.reduce((m, e) => (e.percent_of_soft_limit > m ? e.percent_of_soft_limit : m), 0)
|
|
102
|
+
: null;
|
|
103
|
+
return {
|
|
104
|
+
available: true,
|
|
105
|
+
top_consumers,
|
|
106
|
+
total_processes_scanned: scanned,
|
|
107
|
+
highest_percent_of_limit: highest,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function safeReadTrimmed(path) {
|
|
111
|
+
try {
|
|
112
|
+
return readFileSync(path, "utf-8").trim();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function errCode(err) {
|
|
119
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
120
|
+
const code = err.code;
|
|
121
|
+
if (typeof code === "string")
|
|
122
|
+
return code;
|
|
123
|
+
}
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Parse the "Max open files" line from /proc/<pid>/limits.
|
|
128
|
+
*
|
|
129
|
+
* Format (fixed-width, header + rows):
|
|
130
|
+
* Limit Soft Limit Hard Limit Units
|
|
131
|
+
* Max open files 1024 4096 files
|
|
132
|
+
*
|
|
133
|
+
* Returns null when the line is missing or fields are unparseable.
|
|
134
|
+
* "unlimited" maps to Infinity which Number.MAX_SAFE_INTEGER would
|
|
135
|
+
* misrepresent; we use 0 as a sentinel meaning "no useful soft limit"
|
|
136
|
+
* which makes percent_of_soft_limit zero (and the dashboard rule
|
|
137
|
+
* treats that as a no-emission case).
|
|
138
|
+
*/
|
|
139
|
+
function parseOpenFilesLimit(raw) {
|
|
140
|
+
for (const line of raw.split("\n")) {
|
|
141
|
+
if (!line.startsWith("Max open files"))
|
|
142
|
+
continue;
|
|
143
|
+
// After the label there are at least two whitespace-separated values.
|
|
144
|
+
const rest = line.slice("Max open files".length).trim();
|
|
145
|
+
const parts = rest.split(/\s+/);
|
|
146
|
+
if (parts.length < 2)
|
|
147
|
+
return null;
|
|
148
|
+
const soft = parseLimitValue(parts[0]);
|
|
149
|
+
const hard = parseLimitValue(parts[1]);
|
|
150
|
+
if (soft === null || hard === null)
|
|
151
|
+
return null;
|
|
152
|
+
return { soft, hard };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function parseLimitValue(v) {
|
|
157
|
+
if (v === "unlimited")
|
|
158
|
+
return 0; // sentinel; see parseOpenFilesLimit comment
|
|
159
|
+
const n = Number(v);
|
|
160
|
+
if (!Number.isFinite(n) || n < 0)
|
|
161
|
+
return null;
|
|
162
|
+
return n;
|
|
163
|
+
}
|
|
164
|
+
export const __test_only = {
|
|
165
|
+
parseOpenFilesLimit,
|
|
166
|
+
TOP_N,
|
|
167
|
+
};
|
|
20
168
|
//# sourceMappingURL=fd.js.map
|
package/dist/collect/fd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fd.js","sourceRoot":"","sources":["../../src/collect/fd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"fd.js","sourceRoot":"","sources":["../../src/collect/fd.ts"],"names":[],"mappings":"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"}
|
package/dist/collect/ipmi.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import type { IpmiInfo, FanStatus, Vendor, IpmiCapability } from "../lib/types.js";
|
|
1
|
+
import type { BmcVendor, IpmiInfo, FanStatus, Vendor, IpmiCapability } from "../lib/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* C11 (2026-05-19): map DMI vendor to BMC vendor + parser quality tier.
|
|
4
|
+
*
|
|
5
|
+
* Fleet-tested: dell, hpe, supermicro (years of fleet validation; SEL
|
|
6
|
+
* classifier confirmed against real-host event streams).
|
|
7
|
+
* Stub: lenovo, cisco, openbmc (parser ships but unvalidated against
|
|
8
|
+
* real fleet data; first real customer per vendor surfaces gaps).
|
|
9
|
+
* Unknown: everything else (asrockrack, inspur, generic, virtual, ...);
|
|
10
|
+
* classifier still runs (it's keyword-driven, vendor-agnostic) but
|
|
11
|
+
* we tag the events honestly.
|
|
12
|
+
*/
|
|
13
|
+
declare function mapVendorToBmcVendor(vendor: Vendor): BmcVendor;
|
|
14
|
+
declare function parserQualityFor(bmcVendor: BmcVendor): "fleet-tested" | "stub" | "unknown";
|
|
2
15
|
/**
|
|
3
16
|
* Collect IPMI snapshot.
|
|
4
17
|
*
|
|
@@ -10,6 +23,10 @@ import type { IpmiInfo, FanStatus, Vendor, IpmiCapability } from "../lib/types.j
|
|
|
10
23
|
* and back-compat).
|
|
11
24
|
*/
|
|
12
25
|
export declare function collectIpmi(vendor?: Vendor, capability?: IpmiCapability): Promise<IpmiInfo>;
|
|
26
|
+
export declare const __test_only_c11: {
|
|
27
|
+
mapVendorToBmcVendor: typeof mapVendorToBmcVendor;
|
|
28
|
+
parserQualityFor: typeof parserQualityFor;
|
|
29
|
+
};
|
|
13
30
|
/**
|
|
14
31
|
* Re-parse the full SEL elist counting ECC events on the Memory entity.
|
|
15
32
|
* Used for vendors (notably Dell) that don't expose ECC counters as
|
|
@@ -29,3 +46,4 @@ export declare function parseSelTimestamp(date: string, time: string): string;
|
|
|
29
46
|
export declare function classifySensor(sensor: string): string;
|
|
30
47
|
export declare function deriveSelSeverity(event: string, sensorType: string): string;
|
|
31
48
|
export declare function parseFanStatus(output: string): FanStatus[];
|
|
49
|
+
export {};
|
package/dist/collect/ipmi.js
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import { run } from "../lib/exec.js";
|
|
2
|
+
/**
|
|
3
|
+
* C11 (2026-05-19): map DMI vendor to BMC vendor + parser quality tier.
|
|
4
|
+
*
|
|
5
|
+
* Fleet-tested: dell, hpe, supermicro (years of fleet validation; SEL
|
|
6
|
+
* classifier confirmed against real-host event streams).
|
|
7
|
+
* Stub: lenovo, cisco, openbmc (parser ships but unvalidated against
|
|
8
|
+
* real fleet data; first real customer per vendor surfaces gaps).
|
|
9
|
+
* Unknown: everything else (asrockrack, inspur, generic, virtual, ...);
|
|
10
|
+
* classifier still runs (it's keyword-driven, vendor-agnostic) but
|
|
11
|
+
* we tag the events honestly.
|
|
12
|
+
*/
|
|
13
|
+
function mapVendorToBmcVendor(vendor) {
|
|
14
|
+
if (vendor === "dell" || vendor === "hpe" || vendor === "supermicro")
|
|
15
|
+
return vendor;
|
|
16
|
+
if (vendor === "lenovo" || vendor === "cisco")
|
|
17
|
+
return vendor;
|
|
18
|
+
// OpenBMC isn't a DMI vendor; it would surface as "generic" with a
|
|
19
|
+
// BMC manufacturer of "OpenBMC Project" in ipmitool mc info. We'd
|
|
20
|
+
// need to probe that to identify; deferred to a follow-up.
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
function parserQualityFor(bmcVendor) {
|
|
24
|
+
if (bmcVendor === "dell" || bmcVendor === "hpe" || bmcVendor === "supermicro") {
|
|
25
|
+
return "fleet-tested";
|
|
26
|
+
}
|
|
27
|
+
if (bmcVendor === "lenovo" || bmcVendor === "cisco" || bmcVendor === "openbmc") {
|
|
28
|
+
return "stub";
|
|
29
|
+
}
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
2
32
|
import { isPsuRedundancySensor, classifyPsuRedundancyState } from "../lib/vendor-sensors.js";
|
|
3
33
|
import { filterRedundantCpuDtsSensors } from "../lib/ipmi-sensor-filter.js";
|
|
4
34
|
/**
|
|
@@ -83,8 +113,13 @@ export async function collectIpmi(vendor = "generic", capability) {
|
|
|
83
113
|
if (match)
|
|
84
114
|
selCount = parseInt(match[1], 10);
|
|
85
115
|
}
|
|
86
|
-
// SEL recent events
|
|
87
|
-
|
|
116
|
+
// SEL recent events. C11 (2026-05-19): tag each event with the BMC
|
|
117
|
+
// vendor's parser_quality so Dashboard can render an honesty surface
|
|
118
|
+
// for stub-parser BMCs (Lenovo, Cisco, OpenBMC).
|
|
119
|
+
const bmcVendor = mapVendorToBmcVendor(vendor);
|
|
120
|
+
const selParserQuality = parserQualityFor(bmcVendor);
|
|
121
|
+
const selEventsRaw = await collectSelEvents();
|
|
122
|
+
const selEvents = selEventsRaw.map((e) => ({ ...e, parser_quality: selParserQuality }));
|
|
88
123
|
// ECC errors from SEL events (Dell iDRAC reports memory ECC only via SEL).
|
|
89
124
|
// Counts ALL events since last SEL clear, not just the recent window —
|
|
90
125
|
// re-parse the full SEL elist for accurate cumulative counts.
|
|
@@ -104,6 +139,7 @@ export async function collectIpmi(vendor = "generic", capability) {
|
|
|
104
139
|
const fans = await collectFanStatus();
|
|
105
140
|
return {
|
|
106
141
|
available: true,
|
|
142
|
+
bmc_vendor: bmcVendor,
|
|
107
143
|
sensors,
|
|
108
144
|
ecc_errors: { correctable, uncorrectable },
|
|
109
145
|
ecc_errors_from_sel: selEccCounts,
|
|
@@ -114,6 +150,7 @@ export async function collectIpmi(vendor = "generic", capability) {
|
|
|
114
150
|
detection: capability,
|
|
115
151
|
};
|
|
116
152
|
}
|
|
153
|
+
export const __test_only_c11 = { mapVendorToBmcVendor, parserQualityFor };
|
|
117
154
|
/**
|
|
118
155
|
* Re-parse the full SEL elist counting ECC events on the Memory entity.
|
|
119
156
|
* Used for vendors (notably Dell) that don't expose ECC counters as
|