@glassmkr/crucible 0.7.1 → 0.8.1

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.
Files changed (103) hide show
  1. package/dist/alerts/__tests__/rules.test.d.ts +1 -0
  2. package/dist/alerts/__tests__/rules.test.js +437 -0
  3. package/dist/alerts/__tests__/rules.test.js.map +1 -0
  4. package/dist/alerts/rules.d.ts +8 -0
  5. package/dist/alerts/rules.js +175 -34
  6. package/dist/alerts/rules.js.map +1 -1
  7. package/dist/api.d.ts +2 -0
  8. package/dist/api.js +7 -0
  9. package/dist/api.js.map +1 -0
  10. package/dist/collect/__tests__/dmi.test.d.ts +1 -0
  11. package/dist/collect/__tests__/dmi.test.js +133 -0
  12. package/dist/collect/__tests__/dmi.test.js.map +1 -0
  13. package/dist/collect/__tests__/ipmi.test.js +47 -1
  14. package/dist/collect/__tests__/ipmi.test.js.map +1 -1
  15. package/dist/collect/__tests__/thermal.test.d.ts +1 -0
  16. package/dist/collect/__tests__/thermal.test.js +224 -0
  17. package/dist/collect/__tests__/thermal.test.js.map +1 -0
  18. package/dist/collect/dmi.d.ts +19 -0
  19. package/dist/collect/dmi.js +118 -0
  20. package/dist/collect/dmi.js.map +1 -0
  21. package/dist/collect/ipmi.d.ts +27 -2
  22. package/dist/collect/ipmi.js +90 -2
  23. package/dist/collect/ipmi.js.map +1 -1
  24. package/dist/collect/thermal.d.ts +10 -0
  25. package/dist/collect/thermal.js +232 -0
  26. package/dist/collect/thermal.js.map +1 -0
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.js +2 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/index.js +51 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/__tests__/capability.test.d.ts +1 -0
  33. package/dist/lib/__tests__/capability.test.js +87 -0
  34. package/dist/lib/__tests__/capability.test.js.map +1 -0
  35. package/dist/lib/__tests__/vendor-sensors.test.d.ts +1 -0
  36. package/dist/lib/__tests__/vendor-sensors.test.js +49 -0
  37. package/dist/lib/__tests__/vendor-sensors.test.js.map +1 -0
  38. package/dist/lib/capability.d.ts +21 -0
  39. package/dist/lib/capability.js +110 -0
  40. package/dist/lib/capability.js.map +1 -0
  41. package/dist/lib/cpu-thermal-chips.d.ts +2 -0
  42. package/dist/lib/cpu-thermal-chips.js +28 -0
  43. package/dist/lib/cpu-thermal-chips.js.map +1 -0
  44. package/dist/lib/types.d.ts +58 -0
  45. package/dist/lib/vendor-sensors.d.ts +27 -0
  46. package/dist/lib/vendor-sensors.js +63 -0
  47. package/dist/lib/vendor-sensors.js.map +1 -0
  48. package/dist/notify/telegram.js +1 -1
  49. package/dist/notify/telegram.js.map +1 -1
  50. package/package.json +16 -1
  51. package/rule-ids.json +29 -0
  52. package/.dockerignore +0 -13
  53. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
  54. package/.github/ISSUE_TEMPLATE/no_data.md +0 -26
  55. package/.github/workflows/docker.yml +0 -53
  56. package/.github/workflows/publish.yml +0 -25
  57. package/Dockerfile +0 -59
  58. package/config/collector.example.yaml +0 -43
  59. package/docker-compose.yml +0 -26
  60. package/scripts/sign-release.sh +0 -29
  61. package/src/__tests__/cli.test.ts +0 -74
  62. package/src/__tests__/reboot-marker.test.ts +0 -122
  63. package/src/alerts/evaluator.ts +0 -15
  64. package/src/alerts/rules.ts +0 -283
  65. package/src/alerts/state.ts +0 -92
  66. package/src/cli.ts +0 -112
  67. package/src/collect/__tests__/ipmi.test.ts +0 -96
  68. package/src/collect/__tests__/smart.test.ts +0 -68
  69. package/src/collect/__tests__/system.test.ts +0 -29
  70. package/src/collect/__tests__/zfs.test.ts +0 -72
  71. package/src/collect/conntrack.ts +0 -27
  72. package/src/collect/cpu.ts +0 -92
  73. package/src/collect/disks.ts +0 -91
  74. package/src/collect/fd.ts +0 -31
  75. package/src/collect/io-errors.ts +0 -23
  76. package/src/collect/io-latency.ts +0 -103
  77. package/src/collect/ipmi.ts +0 -207
  78. package/src/collect/memory.ts +0 -30
  79. package/src/collect/network.ts +0 -193
  80. package/src/collect/ntp.ts +0 -114
  81. package/src/collect/os-alerts.ts +0 -43
  82. package/src/collect/raid.ts +0 -40
  83. package/src/collect/security.ts +0 -268
  84. package/src/collect/smart.ts +0 -72
  85. package/src/collect/system.ts +0 -32
  86. package/src/collect/systemd.ts +0 -33
  87. package/src/collect/zfs.ts +0 -66
  88. package/src/config.ts +0 -65
  89. package/src/index.ts +0 -221
  90. package/src/lib/__tests__/parse.test.ts +0 -28
  91. package/src/lib/exec.ts +0 -16
  92. package/src/lib/parse.ts +0 -29
  93. package/src/lib/reboot-marker.ts +0 -88
  94. package/src/lib/types.ts +0 -226
  95. package/src/lib/version-check.ts +0 -39
  96. package/src/lib/version.ts +0 -33
  97. package/src/metrics-server.ts +0 -123
  98. package/src/notify/email.ts +0 -69
  99. package/src/notify/slack.ts +0 -47
  100. package/src/notify/telegram.ts +0 -65
  101. package/src/push/forge.ts +0 -109
  102. package/tsconfig.json +0 -15
  103. package/vitest.config.ts +0 -12
@@ -0,0 +1,110 @@
1
+ // One-shot IPMI capability detection at process startup.
2
+ //
3
+ // The agent runs collectIpmi() every cycle. Without this layer, hosts
4
+ // without a BMC (Pi, laptop, VM, container without /dev mapped) hit four
5
+ // ipmitool ENOENT or "Could not open device" execs every interval forever.
6
+ // They're silent (lib/exec.ts swallows ENOENT) but still wasted process
7
+ // spawns, and there's no log telling the user IPMI is unavailable here.
8
+ //
9
+ // This module probes once at startup and caches the result. collectIpmi()
10
+ // reads the cached capability and short-circuits when unavailable.
11
+ import { promises as fs } from "node:fs";
12
+ import { execFile } from "node:child_process";
13
+ import { promisify } from "node:util";
14
+ const execFileAsync = promisify(execFile);
15
+ const DEVICE_CANDIDATES = ["/dev/ipmi0", "/dev/ipmi/0", "/dev/ipmidev/0"];
16
+ async function defaultStatDevice(path) {
17
+ try {
18
+ await fs.access(path, fs.constants.R_OK);
19
+ return "ok";
20
+ }
21
+ catch (err) {
22
+ if (err.code === "EACCES" || err.code === "EPERM")
23
+ return "eacces";
24
+ return "enoent";
25
+ }
26
+ }
27
+ async function defaultRunIpmitool(args) {
28
+ const { stdout, stderr } = await execFileAsync("ipmitool", args, { timeout: 2000 });
29
+ return { stdout, stderr };
30
+ }
31
+ export async function detectIpmiCapability(deps = {}) {
32
+ const statDevice = deps.statDevice ?? defaultStatDevice;
33
+ const runIpmitool = deps.runIpmitool ?? defaultRunIpmitool;
34
+ // Step 1: probe /dev/ipmi* device nodes.
35
+ let deviceFound = false;
36
+ let permissionDenied = false;
37
+ for (const path of DEVICE_CANDIDATES) {
38
+ const result = await statDevice(path);
39
+ if (result === "ok") {
40
+ deviceFound = true;
41
+ break;
42
+ }
43
+ if (result === "eacces") {
44
+ permissionDenied = true;
45
+ }
46
+ }
47
+ if (permissionDenied && !deviceFound) {
48
+ return {
49
+ available: false,
50
+ reason: "permission_denied",
51
+ detail: "/dev/ipmi0 exists but is not readable; run as root or add user to ipmi group",
52
+ };
53
+ }
54
+ // Step 2: probe ipmitool binary.
55
+ let ipmitoolVersion = null;
56
+ try {
57
+ const { stdout } = await runIpmitool(["-V"]);
58
+ const m = stdout.match(/ipmitool version (\S+)/);
59
+ ipmitoolVersion = m ? m[1] : null;
60
+ }
61
+ catch (err) {
62
+ if (err?.code === "ENOENT") {
63
+ return { available: false, reason: "no_ipmitool_binary" };
64
+ }
65
+ return {
66
+ available: false,
67
+ reason: "execution_failed",
68
+ detail: String(err?.stderr ?? err?.message ?? err).split("\n")[0]?.slice(0, 200),
69
+ };
70
+ }
71
+ // Step 3: device + binary present → assume capable.
72
+ if (deviceFound) {
73
+ return { available: true, method: "ipmitool_in_band", ipmitool_version: ipmitoolVersion };
74
+ }
75
+ // Step 4: binary present but no device node — try one sensor probe to
76
+ // disambiguate (some kernels expose IPMI through unconventional paths).
77
+ try {
78
+ const { stdout, stderr } = await runIpmitool(["sensor"]);
79
+ const out = stdout || "";
80
+ const errOut = (stderr || "").toLowerCase();
81
+ if (out.trim().length > 0 && !errOut.includes("could not open")) {
82
+ return { available: true, method: "ipmitool_in_band", ipmitool_version: ipmitoolVersion };
83
+ }
84
+ return { available: false, reason: "no_bmc_device" };
85
+ }
86
+ catch (err) {
87
+ const stderr = String(err?.stderr ?? "").toLowerCase();
88
+ if (stderr.includes("could not open")) {
89
+ return { available: false, reason: "no_bmc_device" };
90
+ }
91
+ return {
92
+ available: false,
93
+ reason: "execution_failed",
94
+ detail: String(err?.stderr ?? err?.message ?? err).split("\n")[0]?.slice(0, 200),
95
+ };
96
+ }
97
+ }
98
+ export function formatCapabilityLine(cap) {
99
+ if (cap.available) {
100
+ const v = cap.ipmitool_version ? `ipmitool ${cap.ipmitool_version}, ` : "";
101
+ return `IPMI: available (${v}${cap.method.replace(/_/g, " ")})`;
102
+ }
103
+ switch (cap.reason) {
104
+ case "no_ipmitool_binary": return "IPMI: not available (ipmitool not installed)";
105
+ case "no_bmc_device": return "IPMI: not available (no /dev/ipmi*, BMC not detected)";
106
+ case "permission_denied": return `IPMI: not available (${cap.detail ?? "permission denied"})`;
107
+ case "execution_failed": return `IPMI: not available (execution failed${cap.detail ? `: ${cap.detail}` : ""})`;
108
+ }
109
+ }
110
+ //# sourceMappingURL=capability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability.js","sourceRoot":"","sources":["../../src/lib/capability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const CPU_THERMAL_CHIPS: ReadonlySet<string>;
2
+ export declare function isCpuChip(name: string): boolean;
@@ -0,0 +1,28 @@
1
+ // Allowlist of hwmon chip `name` values that report CPU die / package
2
+ // temperatures. Adding a new platform here is the supported way to teach
3
+ // the thermal collector that a chip's readings represent CPU temperature.
4
+ //
5
+ // Drivers explicitly excluded:
6
+ // - "nvme" already covered by SMART; double counts otherwise
7
+ // - "acpitz" often reads ambient or chassis air, not CPU die. Goes to
8
+ // other_readings if found, never to cpu_readings.
9
+ export const CPU_THERMAL_CHIPS = new Set([
10
+ // Intel x86
11
+ "coretemp",
12
+ // AMD x86 (modern)
13
+ "k10temp",
14
+ "zenpower",
15
+ // ARM / SoC
16
+ "cpu_thermal", // Raspberry Pi 4/5, many ARM SBCs
17
+ "armada_thermal", // Marvell Armada SoCs
18
+ "tegra_thermal", // NVIDIA Tegra
19
+ "qcom_tsens", // Qualcomm
20
+ "imx_thermal", // NXP i.MX
21
+ "sun4i_ts", // Allwinner sunxi
22
+ "rockchip_thermal", // Rockchip RK3399 etc.
23
+ "exynos_thermal", // Samsung Exynos
24
+ ]);
25
+ export function isCpuChip(name) {
26
+ return CPU_THERMAL_CHIPS.has(name);
27
+ }
28
+ //# sourceMappingURL=cpu-thermal-chips.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpu-thermal-chips.js","sourceRoot":"","sources":["../../src/lib/cpu-thermal-chips.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,yEAAyE;AACzE,0EAA0E;AAC1E,EAAE;AACF,+BAA+B;AAC/B,iEAAiE;AACjE,wEAAwE;AACxE,+DAA+D;AAC/D,MAAM,CAAC,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IAC5D,YAAY;IACZ,UAAU;IACV,mBAAmB;IACnB,SAAS;IACT,UAAU;IACV,YAAY;IACZ,aAAa,EAAO,kCAAkC;IACtD,gBAAgB,EAAI,sBAAsB;IAC1C,eAAe,EAAK,eAAe;IACnC,YAAY,EAAQ,WAAW;IAC/B,aAAa,EAAO,WAAW;IAC/B,UAAU,EAAU,kBAAkB;IACtC,kBAAkB,EAAE,uBAAuB;IAC3C,gBAAgB,EAAI,iBAAiB;CACtC,CAAC,CAAC;AAEH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrC,CAAC"}
@@ -9,6 +9,8 @@ export interface Snapshot {
9
9
  network: NetworkInfo[];
10
10
  raid: RaidInfo[];
11
11
  ipmi: IpmiInfo;
12
+ dmi?: DmiInfo;
13
+ thermal?: ThermalInfo;
12
14
  os_alerts: OsAlerts;
13
15
  security?: SecurityData;
14
16
  zfs?: ZfsData;
@@ -206,8 +208,33 @@ export interface FanStatus {
206
208
  rpm: number;
207
209
  status: string;
208
210
  }
211
+ export type Vendor = "dell" | "hpe" | "supermicro" | "asrockrack" | "lenovo" | "inspur" | "cisco" | "generic" | "virtual";
212
+ export interface DmiInfo {
213
+ available: boolean;
214
+ vendor: Vendor;
215
+ /** Exact /sys/class/dmi/id/sys_vendor contents, trimmed. */
216
+ raw_vendor: string | null;
217
+ product_name: string | null;
218
+ bios_version: string | null;
219
+ bios_date: string | null;
220
+ is_virtual: boolean;
221
+ }
222
+ export type PsuRedundancyState = "fully_redundant" | "redundancy_lost" | "redundancy_degraded" | "unknown";
223
+ export type IpmiCapability = {
224
+ available: true;
225
+ method: "ipmitool_in_band";
226
+ ipmitool_version: string | null;
227
+ } | {
228
+ available: false;
229
+ reason: "no_ipmitool_binary" | "no_bmc_device" | "execution_failed" | "permission_denied";
230
+ detail?: string;
231
+ };
209
232
  export interface IpmiInfo {
210
233
  available: boolean;
234
+ /** One-shot startup detection result; helps Forge surface "IPMI not
235
+ * available on this host" with a precise reason. Not present on
236
+ * pre-detection snapshots (older agent versions). */
237
+ detection?: IpmiCapability;
211
238
  sensors: Array<{
212
239
  name: string;
213
240
  value: number | string;
@@ -219,10 +246,41 @@ export interface IpmiInfo {
219
246
  correctable: number;
220
247
  uncorrectable: number;
221
248
  };
249
+ /**
250
+ * ECC error counts derived from SEL events instead of named sensors.
251
+ * Dell iDRAC reports memory ECC only via SEL on the Memory entity, so
252
+ * the named-sensor counter (`ecc_errors`) stays at zero on Dell. The
253
+ * `ecc_errors` rule reads max(named, sel) to cover both vendors.
254
+ * Cumulative since last SEL clear, not rate over interval.
255
+ */
256
+ ecc_errors_from_sel?: {
257
+ correctable: number;
258
+ uncorrectable: number;
259
+ newest_event_timestamp: string | null;
260
+ };
261
+ /**
262
+ * Aggregate PSU redundancy state from a vendor sensor (currently Dell
263
+ * `PS Redundancy` only). Undefined on hosts where no aggregate sensor
264
+ * exists; the rule then falls back to per-PSU status checks.
265
+ */
266
+ psu_redundancy_state?: PsuRedundancyState;
222
267
  sel_entries_count: number;
223
268
  sel_events_recent: SelEvent[];
224
269
  fans: FanStatus[];
225
270
  }
271
+ export interface ThermalReading {
272
+ label: string;
273
+ value_celsius: number;
274
+ source_chip: string;
275
+ source: "hwmon" | "thermal_zone";
276
+ }
277
+ export interface ThermalInfo {
278
+ available: boolean;
279
+ source: "hwmon" | "thermal_zone" | "none";
280
+ cpu_readings: ThermalReading[];
281
+ other_readings: ThermalReading[];
282
+ max_cpu_celsius: number | null;
283
+ }
226
284
  export interface OsAlerts {
227
285
  oom_kills_recent: number;
228
286
  zombie_processes: number;
@@ -0,0 +1,27 @@
1
+ import type { Vendor } from "./types.js";
2
+ /**
3
+ * True if `name` looks like an individual PSU sensor (status / wattage / etc.)
4
+ * for the given vendor.
5
+ *
6
+ * Generic patterns cover Supermicro, HPE iLO, ASRockRack:
7
+ * "PSU1 Status", "Power Supply 1", "PSU2 Power Out"
8
+ *
9
+ * Dell iDRAC adds:
10
+ * "PS1 Status", "PS2 Status", "PS3 Status"
11
+ *
12
+ * Note that "PS Redundancy" is NOT an individual PSU sensor; see
13
+ * `isPsuRedundancySensor`.
14
+ */
15
+ export declare function isPsuSensor(name: string, vendor: Vendor): boolean;
16
+ /**
17
+ * True if `name` is the aggregate PSU redundancy state sensor.
18
+ * Currently only Dell exposes this as a discrete sensor reading;
19
+ * HPE/Supermicro report it via SEL events instead.
20
+ */
21
+ export declare function isPsuRedundancySensor(name: string, vendor: Vendor): boolean;
22
+ /**
23
+ * Map the value/status text of a Dell `PS Redundancy` sensor to a canonical
24
+ * state. Dell reports strings like "Fully Redundant", "Redundancy Lost",
25
+ * "Redundancy Degraded", or "0x01"-style raw codes depending on iDRAC firmware.
26
+ */
27
+ export declare function classifyPsuRedundancyState(valueOrStatus: string): "fully_redundant" | "redundancy_lost" | "redundancy_degraded" | "unknown";
@@ -0,0 +1,63 @@
1
+ // Vendor-aware classifiers for IPMI sensor names.
2
+ //
3
+ // The substring filters in alert rules historically assumed Supermicro /
4
+ // ASRockRack naming conventions (`PSU1 Status`, `CPU1 Temp`, etc.). Dell
5
+ // iDRAC names sensors very differently: `PS1 Status`, `PS Redundancy`,
6
+ // bare `Temp` per processor entity. HPE iLO is closer to Supermicro's
7
+ // shape but has its own quirks. Adding a new vendor means adding a case
8
+ // here, not editing every rule.
9
+ /**
10
+ * True if `name` looks like an individual PSU sensor (status / wattage / etc.)
11
+ * for the given vendor.
12
+ *
13
+ * Generic patterns cover Supermicro, HPE iLO, ASRockRack:
14
+ * "PSU1 Status", "Power Supply 1", "PSU2 Power Out"
15
+ *
16
+ * Dell iDRAC adds:
17
+ * "PS1 Status", "PS2 Status", "PS3 Status"
18
+ *
19
+ * Note that "PS Redundancy" is NOT an individual PSU sensor; see
20
+ * `isPsuRedundancySensor`.
21
+ */
22
+ export function isPsuSensor(name, vendor) {
23
+ const lower = name.toLowerCase();
24
+ if (lower.includes("psu") || lower.includes("power supply"))
25
+ return true;
26
+ if (vendor === "dell") {
27
+ // PS1, PS2 ... optionally followed by " Status" or other suffix.
28
+ // Excludes "PS Redundancy" and similar non-numeric.
29
+ if (/^ps\d+\b/i.test(name))
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+ /**
35
+ * True if `name` is the aggregate PSU redundancy state sensor.
36
+ * Currently only Dell exposes this as a discrete sensor reading;
37
+ * HPE/Supermicro report it via SEL events instead.
38
+ */
39
+ export function isPsuRedundancySensor(name, vendor) {
40
+ if (vendor === "dell") {
41
+ return /^ps\s+redundancy$/i.test(name);
42
+ }
43
+ return false;
44
+ }
45
+ /**
46
+ * Map the value/status text of a Dell `PS Redundancy` sensor to a canonical
47
+ * state. Dell reports strings like "Fully Redundant", "Redundancy Lost",
48
+ * "Redundancy Degraded", or "0x01"-style raw codes depending on iDRAC firmware.
49
+ */
50
+ export function classifyPsuRedundancyState(valueOrStatus) {
51
+ const lower = valueOrStatus.toLowerCase();
52
+ if (lower.includes("fully redundant") || lower.includes("fully-redundant"))
53
+ return "fully_redundant";
54
+ if (lower.includes("lost"))
55
+ return "redundancy_lost";
56
+ if (lower.includes("degraded"))
57
+ return "redundancy_degraded";
58
+ // Some iDRAC firmwares report "ok" + numeric value 1 = fully redundant
59
+ if (lower === "ok")
60
+ return "fully_redundant";
61
+ return "unknown";
62
+ }
63
+ //# sourceMappingURL=vendor-sensors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vendor-sensors.js","sourceRoot":"","sources":["../../src/lib/vendor-sensors.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,uEAAuE;AACvE,sEAAsE;AACtE,wEAAwE;AACxE,gCAAgC;AAIhC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,MAAc;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,IAAI,CAAC;IACzE,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,iEAAiE;QACjE,oDAAoD;QACpD,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,MAAc;IAChE,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CAAC,aAAqB;IAC9D,MAAM,KAAK,GAAG,aAAa,CAAC,WAAW,EAAE,CAAC;IAC1C,IAAI,KAAK,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,iBAAiB,CAAC;QAAE,OAAO,iBAAiB,CAAC;IACrG,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,iBAAiB,CAAC;IACrD,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,qBAAqB,CAAC;IAC7D,uEAAuE;IACvE,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,iBAAiB,CAAC;IAC7C,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -3,7 +3,7 @@ const PRIORITY_MAP = {
3
3
  oom_kills: "P2", ram_high: "P2", disk_space_high: "P2", ipmi_sel_critical: "P2", disk_io_errors: "P2", zfs_pool_unhealthy: "P2",
4
4
  cpu_iowait_high: "P3", nvme_wear_high: "P3", disk_latency_high: "P3", cpu_temperature_high: "P3",
5
5
  ssh_root_password: "P3", pending_security_updates: "P3", kernel_vulnerabilities: "P3", zfs_scrub_errors: "P3",
6
- swap_active: "P4", no_firewall: "P4", kernel_needs_reboot: "P4", unattended_upgrades_disabled: "P4",
6
+ swap_high: "P4", no_firewall: "P4", kernel_needs_reboot: "P4", unattended_upgrades_disabled: "P4",
7
7
  interface_errors: "P4", link_speed_mismatch: "P4", interface_saturation: "P4",
8
8
  };
9
9
  const PRIORITY_LABELS = {
@@ -1 +1 @@
1
- {"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../src/notify/telegram.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../src/notify/telegram.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,24 @@
1
1
  {
2
2
  "name": "@glassmkr/crucible",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Lightweight bare metal server monitoring. IPMI, SMART, OS, network. Opinionated alerts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./api": {
10
+ "types": "./dist/api.d.ts",
11
+ "default": "./dist/api.js"
12
+ },
13
+ "./rule-ids.json": "./rule-ids.json",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "rule-ids.json",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
7
22
  "bin": {
8
23
  "glassmkr-crucible": "./dist/index.js"
9
24
  },
package/rule-ids.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "$comment": "Static, hand-maintained list of every rule ID this collector ships. Kept in sync with src/alerts/rules.ts (ALL_RULE_IDS). Published in the npm package so tooling can read it without dynamic-importing the CLI module. CI checks the two stay in sync.",
3
+ "version": 1,
4
+ "rule_ids": [
5
+ "ram_high",
6
+ "swap_high",
7
+ "disk_space_high",
8
+ "cpu_iowait_high",
9
+ "oom_kills",
10
+ "smart_failing",
11
+ "nvme_wear_high",
12
+ "raid_degraded",
13
+ "disk_latency_high",
14
+ "interface_errors",
15
+ "link_speed_mismatch",
16
+ "interface_saturation",
17
+ "cpu_temperature_high",
18
+ "ecc_errors",
19
+ "psu_redundancy_loss",
20
+ "ipmi_sel_critical",
21
+ "ipmi_fan_failure",
22
+ "ssh_root_password",
23
+ "no_firewall",
24
+ "pending_security_updates",
25
+ "kernel_vulnerabilities",
26
+ "kernel_needs_reboot",
27
+ "unattended_upgrades_disabled"
28
+ ]
29
+ }
package/.dockerignore DELETED
@@ -1,13 +0,0 @@
1
- node_modules
2
- .git
3
- .github
4
- *.md
5
- !README.md
6
- .svelte-kit
7
- dist
8
- coverage
9
- .env*
10
- .vscode
11
- .idea
12
- *.log
13
- .DS_Store
@@ -1,24 +0,0 @@
1
- ---
2
- name: Bug Report
3
- about: Something is not working as expected
4
- ---
5
-
6
- **Crucible version:** (check package.json or service logs)
7
- **OS and version:** (e.g., Ubuntu 24.04)
8
- **Kernel:** (output of `uname -r`)
9
- **Node.js version:** (output of `node --version`)
10
- **Running as root:** yes/no
11
-
12
- **What happened:**
13
-
14
- **What you expected:**
15
-
16
- **Service status:**
17
- ```
18
- systemctl status glassmkr-crucible
19
- ```
20
-
21
- **Last 50 log lines:**
22
- ```
23
- journalctl -u glassmkr-crucible -n 50 --no-pager
24
- ```
@@ -1,26 +0,0 @@
1
- ---
2
- name: No Data in Forge
3
- about: Crucible is running but Forge shows no data
4
- ---
5
-
6
- **Crucible version:**
7
- **OS and version:**
8
- **How long since install:**
9
-
10
- **Can you reach Forge?**
11
- ```
12
- curl -s -o /dev/null -w "%{http_code}" https://forge.glassmkr.com/api/health
13
- ```
14
-
15
- **Service status:**
16
- ```
17
- systemctl status glassmkr-crucible
18
- ```
19
-
20
- **Last 50 log lines:**
21
- ```
22
- journalctl -u glassmkr-crucible -n 50 --no-pager
23
- ```
24
-
25
- **Is smartctl installed?** `which smartctl`
26
- **Is ipmitool installed?** `which ipmitool`
@@ -1,53 +0,0 @@
1
- name: Build and publish Docker image
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- tags: ['v*']
7
-
8
- permissions:
9
- contents: read
10
- packages: write
11
-
12
- jobs:
13
- docker:
14
- name: Build and push to ghcr.io and docker.io
15
- runs-on: ubuntu-latest
16
- steps:
17
- - uses: actions/checkout@v4
18
-
19
- - uses: docker/setup-buildx-action@v3
20
-
21
- - uses: docker/login-action@v3
22
- with:
23
- registry: ghcr.io
24
- username: ${{ github.actor }}
25
- password: ${{ secrets.GITHUB_TOKEN }}
26
-
27
- - uses: docker/login-action@v3
28
- with:
29
- registry: docker.io
30
- username: ${{ secrets.DOCKERHUB_USERNAME }}
31
- password: ${{ secrets.DOCKERHUB_TOKEN }}
32
-
33
- - uses: docker/metadata-action@v5
34
- id: meta
35
- with:
36
- images: |
37
- ghcr.io/glassmkr/crucible
38
- docker.io/glassmkr/crucible
39
- tags: |
40
- type=semver,pattern={{version}}
41
- type=semver,pattern={{major}}.{{minor}}
42
- type=raw,value=latest,enable={{is_default_branch}}
43
- type=sha,prefix=sha-,format=short
44
-
45
- - uses: docker/build-push-action@v6
46
- with:
47
- context: .
48
- platforms: linux/amd64
49
- push: true
50
- tags: ${{ steps.meta.outputs.tags }}
51
- labels: ${{ steps.meta.outputs.labels }}
52
- cache-from: type=gha
53
- cache-to: type=gha,mode=max
@@ -1,25 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- push:
5
- tags: ['v*']
6
-
7
- permissions:
8
- contents: read
9
- id-token: write
10
-
11
- jobs:
12
- publish:
13
- name: npm publish
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-node@v4
18
- with:
19
- # Node 24 ships with npm 11.x which has working npm Trusted Publishing.
20
- node-version: 24
21
- registry-url: 'https://registry.npmjs.org'
22
- - run: npm --version
23
- - run: npm install
24
- - run: npm run build
25
- - run: npm publish --access public --provenance
package/Dockerfile DELETED
@@ -1,59 +0,0 @@
1
- # syntax=docker/dockerfile:1.7
2
- # Multi-stage build for Glassmkr Crucible monitoring agent.
3
-
4
- # ---------- Stage 1: build TypeScript to dist/ ----------
5
- FROM node:24-slim AS builder
6
- WORKDIR /build
7
- COPY package.json package-lock.json* ./
8
- RUN npm install --include=dev --no-audit --no-fund
9
- COPY tsconfig.json ./
10
- COPY src ./src
11
- RUN npm run build
12
-
13
- # ---------- Stage 2: production runtime ----------
14
- FROM node:24-slim AS runtime
15
-
16
- # Hardware monitoring tools. Crucible shells out to these; they must be on PATH.
17
- RUN apt-get update && apt-get install -y --no-install-recommends \
18
- smartmontools \
19
- ipmitool \
20
- dmidecode \
21
- lm-sensors \
22
- ethtool \
23
- util-linux \
24
- procps \
25
- net-tools \
26
- iproute2 \
27
- ca-certificates \
28
- && rm -rf /var/lib/apt/lists/*
29
-
30
- WORKDIR /app
31
-
32
- # Production node_modules only.
33
- COPY package.json package-lock.json* ./
34
- RUN npm ci --omit=dev --no-audit --no-fund && npm cache clean --force
35
-
36
- # Built code.
37
- COPY --from=builder /build/dist ./dist
38
-
39
- # Create a non-root user for future use. IPMI and SMART typically require root,
40
- # so the container is expected to run with --privileged or cap_add DAC_READ_SEARCH etc.
41
- # Keeping the user available lets operators drop privileges when hardware access is not needed.
42
- RUN useradd --system --no-create-home --shell /usr/sbin/nologin glassmkr
43
-
44
- # Crucible reads /etc/glassmkr/collector.yaml by default.
45
- # Mount the host config directory at this path.
46
- RUN mkdir -p /etc/glassmkr
47
-
48
- # Container health: verify the Node process is actually running and hasn't crashed.
49
- HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \
50
- CMD pgrep -f "node /app/dist/index.js" > /dev/null || exit 1
51
-
52
- LABEL org.opencontainers.image.source="https://github.com/glassmkr/crucible" \
53
- org.opencontainers.image.description="Glassmkr Crucible - bare metal server monitoring agent" \
54
- org.opencontainers.image.licenses="MIT" \
55
- org.opencontainers.image.title="Crucible" \
56
- org.opencontainers.image.vendor="Glassmkr"
57
-
58
- # Crucible does not listen on any port; data flows outbound to Forge.
59
- ENTRYPOINT ["node", "/app/dist/index.js"]