@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
@@ -1,92 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from "fs";
2
- import type { AlertResult } from "../lib/types.js";
3
-
4
- const STATE_FILE = "/var/lib/glassmkr/alert-state.json";
5
-
6
- interface AlertState {
7
- type: string;
8
- first_seen: string;
9
- last_seen: string;
10
- notified: boolean;
11
- }
12
-
13
- let state: Map<string, AlertState> = new Map();
14
-
15
- function load() {
16
- try {
17
- const raw = readFileSync(STATE_FILE, "utf-8");
18
- const data: Record<string, AlertState> = JSON.parse(raw);
19
- state = new Map(Object.entries(data));
20
- } catch {
21
- state = new Map();
22
- }
23
- }
24
-
25
- function save() {
26
- try {
27
- mkdirSync("/var/lib/glassmkr", { recursive: true });
28
- const obj: Record<string, AlertState> = {};
29
- for (const [k, v] of state) obj[k] = v;
30
- writeFileSync(STATE_FILE, JSON.stringify(obj, null, 2));
31
- } catch (err) {
32
- console.error("[state] Failed to save alert state:", err);
33
- }
34
- }
35
-
36
- // Initialize on import
37
- load();
38
-
39
- export function updateAlertState(currentAlerts: AlertResult[]): {
40
- newAlerts: AlertResult[];
41
- resolvedAlerts: AlertResult[];
42
- } {
43
- const now = new Date().toISOString();
44
- const currentTypes = new Set(currentAlerts.map((a) => a.type));
45
- const newAlerts: AlertResult[] = [];
46
- const resolvedAlerts: AlertResult[] = [];
47
-
48
- // Check for new alerts
49
- for (const alert of currentAlerts) {
50
- const existing = state.get(alert.type);
51
- if (!existing) {
52
- // New alert
53
- state.set(alert.type, { type: alert.type, first_seen: now, last_seen: now, notified: false });
54
- newAlerts.push(alert);
55
- } else {
56
- // Existing alert, update last_seen
57
- existing.last_seen = now;
58
- }
59
- }
60
-
61
- // Check for resolved alerts
62
- for (const [type, alertState] of state) {
63
- if (!currentTypes.has(type)) {
64
- resolvedAlerts.push({
65
- type,
66
- severity: "warning",
67
- title: `Resolved: ${type}`,
68
- message: `Condition cleared. Active for ${timeSince(alertState.first_seen)}.`,
69
- evidence: {},
70
- recommendation: "",
71
- });
72
- state.delete(type);
73
- }
74
- }
75
-
76
- save();
77
- return { newAlerts, resolvedAlerts };
78
- }
79
-
80
- function timeSince(isoDate: string): string {
81
- const ms = Date.now() - new Date(isoDate).getTime();
82
- const minutes = Math.floor(ms / 60000);
83
- if (minutes < 60) return `${minutes} minute(s)`;
84
- const hours = Math.floor(minutes / 60);
85
- if (hours < 24) return `${hours} hour(s) ${minutes % 60} minute(s)`;
86
- const days = Math.floor(hours / 24);
87
- return `${days} day(s)`;
88
- }
89
-
90
- export function getActiveAlerts(): string[] {
91
- return Array.from(state.keys());
92
- }
package/src/cli.ts DELETED
@@ -1,112 +0,0 @@
1
- // CLI argument handling for the Crucible binary. Runs before any config load
2
- // or collector initialization so --version and --help exit cleanly even when
3
- // the config file is missing or the host lacks the tools the collectors need.
4
-
5
- export type CliMode = "version" | "help" | "run" | "mark-reboot" | "reboot";
6
-
7
- export interface CliArgs {
8
- mode: CliMode;
9
- configPath: string;
10
- reason?: string;
11
- ttl?: string; // raw duration string, parsed by caller
12
- }
13
-
14
- export const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
15
-
16
- export function parseCliArgs(argv: string[], version: string): { result: CliArgs; output: string | null } {
17
- // argv is typically process.argv.slice(2)
18
- let configPath = DEFAULT_CONFIG_PATH;
19
-
20
- // Subcommand dispatch: `mark-reboot` and `reboot` take their own flags
21
- // (--reason, --ttl) but re-use --help.
22
- if (argv[0] === "mark-reboot" || argv[0] === "reboot") {
23
- const mode: "mark-reboot" | "reboot" = argv[0];
24
- let reason: string | undefined;
25
- let ttl: string | undefined;
26
- for (let i = 1; i < argv.length; i++) {
27
- const a = argv[i];
28
- if (a === "--help" || a === "-h") {
29
- return { result: { mode: "help", configPath: "" }, output: subcommandHelp(mode, version) };
30
- }
31
- if (a === "--reason") { reason = argv[++i]; continue; }
32
- if (a.startsWith("--reason=")) { reason = a.slice("--reason=".length); continue; }
33
- if (a === "--ttl") { ttl = argv[++i]; continue; }
34
- if (a.startsWith("--ttl=")) { ttl = a.slice("--ttl=".length); continue; }
35
- }
36
- return { result: { mode, configPath: "", reason, ttl }, output: null };
37
- }
38
-
39
- for (let i = 0; i < argv.length; i++) {
40
- const arg = argv[i];
41
- if (arg === "--version" || arg === "-v") {
42
- return { result: { mode: "version", configPath: "" }, output: `glassmkr-crucible v${version}` };
43
- }
44
- if (arg === "--help" || arg === "-h") {
45
- return { result: { mode: "help", configPath: "" }, output: helpText(version) };
46
- }
47
- // -c <path> or --config <path>
48
- if (arg === "-c" || arg === "--config") {
49
- const next = argv[i + 1];
50
- if (next) {
51
- configPath = next;
52
- i++;
53
- }
54
- continue;
55
- }
56
- // --config=<path>
57
- if (arg.startsWith("--config=")) {
58
- configPath = arg.slice("--config=".length);
59
- continue;
60
- }
61
- // Legacy positional argument: first non-flag token
62
- if (!arg.startsWith("-")) {
63
- configPath = arg;
64
- }
65
- }
66
-
67
- return { result: { mode: "run", configPath }, output: null };
68
- }
69
-
70
- export function helpText(version: string): string {
71
- return [
72
- `glassmkr-crucible v${version} - Bare metal server monitoring agent`,
73
- "",
74
- "Usage:",
75
- " glassmkr-crucible [options]",
76
- " glassmkr-crucible mark-reboot [--reason TEXT] [--ttl DURATION]",
77
- " glassmkr-crucible reboot [--reason TEXT] [--ttl DURATION]",
78
- "",
79
- "Options:",
80
- " -v, --version Print version and exit",
81
- " -h, --help Print this help and exit",
82
- ` -c, --config Path to config file (default: ${DEFAULT_CONFIG_PATH})`,
83
- "",
84
- "Subcommands:",
85
- " mark-reboot Write a planned-reboot marker so the next boot",
86
- " does not fire `server_rebooted_unexpectedly`.",
87
- " You run the reboot yourself afterwards.",
88
- " reboot Write the marker, then invoke `systemctl reboot`.",
89
- "",
90
- "Without options, starts the collector daemon using the config file.",
91
- "Docs: https://github.com/glassmkr/crucible",
92
- ].join("\n");
93
- }
94
-
95
- function subcommandHelp(mode: "mark-reboot" | "reboot", version: string): string {
96
- const action = mode === "reboot"
97
- ? "Write a planned-reboot marker and invoke `systemctl reboot`."
98
- : "Write a planned-reboot marker; operator triggers the reboot.";
99
- return [
100
- `glassmkr-crucible ${mode} - ${action}`,
101
- "",
102
- "Usage:",
103
- ` glassmkr-crucible ${mode} [--reason TEXT] [--ttl DURATION]`,
104
- "",
105
- "Options:",
106
- ' --reason TEXT Free-text reason (e.g. "kernel update")',
107
- " --ttl DURATION Expiry window; e.g. 5m, 10m, 1h (default 10m)",
108
- "",
109
- `Marker path: /var/lib/crucible/reboot-expected (requires root).`,
110
- `v${version}`,
111
- ].join("\n");
112
- }
@@ -1,96 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { classifySensor, deriveSelSeverity, parseSelTimestamp, parseFanStatus } from "../ipmi.js";
3
-
4
- describe("classifySensor", () => {
5
- it("recognizes memory sensors", () => {
6
- expect(classifySensor("DIMM_A1")).toBe("memory");
7
- expect(classifySensor("Memory ECC")).toBe("memory");
8
- });
9
- it("recognizes power supplies", () => {
10
- expect(classifySensor("PSU1 Status")).toBe("power");
11
- expect(classifySensor("Power Supply 1")).toBe("power");
12
- });
13
- it("recognizes fans, watchdog, processors, temps, voltage, storage, chassis", () => {
14
- expect(classifySensor("Fan1")).toBe("fan");
15
- expect(classifySensor("Watchdog")).toBe("watchdog");
16
- expect(classifySensor("Processor 0")).toBe("processor");
17
- // CPU-named temperature sensors classify as processor (cpu check wins over temp).
18
- expect(classifySensor("CPU1 Temp")).toBe("processor");
19
- expect(classifySensor("Inlet Temp")).toBe("temperature");
20
- expect(classifySensor("VCore Voltage")).toBe("voltage");
21
- expect(classifySensor("Drive Slot 1")).toBe("storage");
22
- expect(classifySensor("Chassis Intrusion")).toBe("chassis");
23
- });
24
- it("falls back to 'other'", () => {
25
- expect(classifySensor("Weird Sensor")).toBe("other");
26
- });
27
- });
28
-
29
- describe("deriveSelSeverity", () => {
30
- it("treats uncorrectable, thermal trip, AC lost as critical", () => {
31
- expect(deriveSelSeverity("Uncorrectable ECC", "memory")).toBe("critical");
32
- expect(deriveSelSeverity("Thermal trip", "processor")).toBe("critical");
33
- expect(deriveSelSeverity("AC lost", "power")).toBe("critical");
34
- expect(deriveSelSeverity("Machine check", "processor")).toBe("critical");
35
- });
36
- it("treats correctable ECC and redundancy lost as warning", () => {
37
- expect(deriveSelSeverity("Correctable ECC", "memory")).toBe("warning");
38
- expect(deriveSelSeverity("Redundancy lost", "power")).toBe("warning");
39
- });
40
- it("treats presence detected as info", () => {
41
- expect(deriveSelSeverity("Presence detected", "memory")).toBe("info");
42
- });
43
- it("defaults to warning for memory/power/fan/processor sensor types", () => {
44
- expect(deriveSelSeverity("Some odd event", "memory")).toBe("warning");
45
- expect(deriveSelSeverity("Some odd event", "fan")).toBe("warning");
46
- });
47
- it("defaults to info for other sensor types", () => {
48
- expect(deriveSelSeverity("Some odd event", "other")).toBe("info");
49
- });
50
- });
51
-
52
- describe("parseSelTimestamp", () => {
53
- it("formats a known date/time", () => {
54
- expect(parseSelTimestamp("04/05/2026", "14:23:05")).toBe("2026-04-05T14:23:05Z");
55
- });
56
- it("pads single digit month/day", () => {
57
- expect(parseSelTimestamp("4/5/2026", "09:00:00")).toBe("2026-04-05T09:00:00Z");
58
- });
59
- it("returns an ISO string for bad input (does not crash)", () => {
60
- const out = parseSelTimestamp("", "");
61
- expect(typeof out).toBe("string");
62
- expect(out.length).toBeGreaterThan(10);
63
- });
64
- });
65
-
66
- describe("parseFanStatus", () => {
67
- it("parses healthy fan output", () => {
68
- const raw = [
69
- "FAN1 | 30h | ok | 7.1 | 5000 RPM",
70
- "FAN2 | 31h | ok | 7.2 | 5100 RPM",
71
- ].join("\n");
72
- const fans = parseFanStatus(raw);
73
- expect(fans).toHaveLength(2);
74
- expect(fans[0]).toMatchObject({ name: "FAN1", rpm: 5000, status: "ok" });
75
- expect(fans[1].rpm).toBe(5100);
76
- });
77
-
78
- it("marks critical fans (cr/nr) as critical", () => {
79
- const raw = "FAN1 | 30h | cr | 7.1 | 0 RPM";
80
- const fans = parseFanStatus(raw);
81
- expect(fans[0].status).toBe("critical");
82
- });
83
-
84
- it("marks absent/no-reading fans as absent", () => {
85
- const raw = "FAN3 | 30h | ns | 7.1 | no reading";
86
- const fans = parseFanStatus(raw);
87
- expect(fans[0].status).toBe("absent");
88
- expect(fans[0].rpm).toBe(0);
89
- });
90
-
91
- it("treats 0 RPM with no explicit status as critical", () => {
92
- const raw = "FAN1 | 30h | 7.1 | 0 RPM";
93
- const fans = parseFanStatus(raw);
94
- expect(fans[0].status).toBe("critical");
95
- });
96
- });
@@ -1,68 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parseSmartctlJson } from "../smart.js";
3
-
4
- describe("parseSmartctlJson", () => {
5
- it("parses a healthy SATA SSD", () => {
6
- const data = {
7
- model_name: "Samsung SSD 970 EVO 1TB",
8
- smart_status: { passed: true },
9
- temperature: { current: 38 },
10
- power_on_time: { hours: 9000 },
11
- ata_smart_attributes: {
12
- table: [
13
- { id: 5, name: "Reallocated_Sector_Ct", raw: { value: 0 } },
14
- { id: 197, name: "Current_Pending_Sector", raw: { value: 0 } },
15
- ],
16
- },
17
- };
18
- const info = parseSmartctlJson(data, "/dev/sda");
19
- expect(info).toMatchObject({
20
- device: "/dev/sda",
21
- model: "Samsung SSD 970 EVO 1TB",
22
- health: "PASSED",
23
- temperature_c: 38,
24
- power_on_hours: 9000,
25
- reallocated_sectors: 0,
26
- pending_sectors: 0,
27
- });
28
- });
29
-
30
- it("parses a failing SATA drive with reallocated sectors", () => {
31
- const data = {
32
- model_name: "WD Red 4TB",
33
- smart_status: { passed: false },
34
- ata_smart_attributes: {
35
- table: [
36
- { id: 5, raw: { value: 12 } },
37
- { id: 197, raw: { value: 3 } },
38
- ],
39
- },
40
- };
41
- const info = parseSmartctlJson(data, "/dev/sdb");
42
- expect(info.health).toBe("FAILED");
43
- expect(info.reallocated_sectors).toBe(12);
44
- expect(info.pending_sectors).toBe(3);
45
- });
46
-
47
- it("parses an NVMe drive with percentage_used", () => {
48
- const data = {
49
- model_name: "Samsung 980 PRO",
50
- smart_status: { passed: true },
51
- nvme_smart_health_information_log: { percentage_used: 22, temperature: 41 },
52
- };
53
- const info = parseSmartctlJson(data, "/dev/nvme0n1");
54
- expect(info.percentage_used).toBe(22);
55
- expect(info.temperature_c).toBe(41);
56
- expect(info.health).toBe("PASSED");
57
- });
58
-
59
- it("falls back to 'unknown' model when absent", () => {
60
- const info = parseSmartctlJson({ smart_status: { passed: true } }, "/dev/sdc");
61
- expect(info.model).toBe("unknown");
62
- });
63
-
64
- it("treats missing smart_status as FAILED (safer default)", () => {
65
- const info = parseSmartctlJson({}, "/dev/sdd");
66
- expect(info.health).toBe("FAILED");
67
- });
68
- });
@@ -1,29 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { readOsReleaseField } from "../system.js";
3
-
4
- describe("readOsReleaseField", () => {
5
- it("parses unquoted Ubuntu values", () => {
6
- const s = 'NAME="Ubuntu"\nID=ubuntu\nID_LIKE=debian\nVERSION_ID="24.04"';
7
- expect(readOsReleaseField(s, "ID")).toBe("ubuntu");
8
- expect(readOsReleaseField(s, "ID_LIKE")).toBe("debian");
9
- });
10
-
11
- it("parses quoted RHEL-family values", () => {
12
- const s = 'NAME="Rocky Linux"\nID="rocky"\nID_LIKE="rhel centos fedora"';
13
- expect(readOsReleaseField(s, "ID")).toBe("rocky");
14
- expect(readOsReleaseField(s, "ID_LIKE")).toBe("rhel centos fedora");
15
- });
16
-
17
- it("lowercases the result (some distros uppercase their ID)", () => {
18
- expect(readOsReleaseField("ID=Alpine", "ID")).toBe("alpine");
19
- });
20
-
21
- it("returns undefined for a missing key", () => {
22
- expect(readOsReleaseField("ID=arch", "ID_LIKE")).toBeUndefined();
23
- });
24
-
25
- it("does not confuse ID with VERSION_ID", () => {
26
- const s = 'VERSION_ID="24.04"\nID=ubuntu';
27
- expect(readOsReleaseField(s, "ID")).toBe("ubuntu");
28
- });
29
- });
@@ -1,72 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parseZpoolStatus } from "../zfs.js";
3
-
4
- describe("parseZpoolStatus", () => {
5
- it("parses a healthy pool", () => {
6
- const raw = ` pool: tank
7
- state: ONLINE
8
- scan: scrub repaired 0B in 01:23:45 with 0 errors on Sun Apr 5 12:34:56 2026
9
- config:
10
-
11
- NAME STATE READ WRITE CKSUM
12
- tank ONLINE 0 0 0
13
- mirror-0 ONLINE 0 0 0
14
-
15
- errors: No known data errors
16
- `;
17
- const pools = parseZpoolStatus(raw);
18
- expect(pools).toHaveLength(1);
19
- expect(pools[0]).toMatchObject({
20
- name: "tank",
21
- state: "ONLINE",
22
- errors_text: "No known data errors",
23
- scrub_errors: 0,
24
- scrub_repaired: "0B",
25
- });
26
- expect(pools[0].last_scrub_date).toContain("2026");
27
- });
28
-
29
- it("parses a DEGRADED pool", () => {
30
- const raw = ` pool: tank
31
- state: DEGRADED
32
- scan: scrub repaired 16K in 02:00:00 with 3 errors on Sun Apr 5 12:34:56 2026
33
-
34
- errors: 3 data errors, use '-v' for a list
35
- `;
36
- const [p] = parseZpoolStatus(raw);
37
- expect(p.state).toBe("DEGRADED");
38
- expect(p.scrub_errors).toBe(3);
39
- expect(p.scrub_repaired).toBe("16K");
40
- });
41
-
42
- it("flags never-scrubbed pools", () => {
43
- const raw = ` pool: tank
44
- state: ONLINE
45
- scan: none requested
46
-
47
- errors: No known data errors
48
- `;
49
- const [p] = parseZpoolStatus(raw);
50
- expect(p.scrub_never_run).toBe(true);
51
- expect(p.scrub_errors).toBeUndefined();
52
- });
53
-
54
- it("returns empty for no pools", () => {
55
- expect(parseZpoolStatus("no pools available")).toEqual([]);
56
- });
57
-
58
- it("parses multiple pools", () => {
59
- const raw = ` pool: tank
60
- state: ONLINE
61
- scan: none requested
62
- errors: No known data errors
63
- pool: data
64
- state: FAULTED
65
- scan: none requested
66
- errors: 2 data errors
67
- `;
68
- const pools = parseZpoolStatus(raw);
69
- expect(pools.map((p) => p.name)).toEqual(["tank", "data"]);
70
- expect(pools[1].state).toBe("FAULTED");
71
- });
72
- });
@@ -1,27 +0,0 @@
1
- import { readProcFile } from "../lib/parse.js";
2
-
3
- export interface ConntrackData {
4
- available: boolean;
5
- count: number;
6
- max: number;
7
- percent: number;
8
- }
9
-
10
- export function collectConntrack(): ConntrackData {
11
- const countRaw = readProcFile("/proc/sys/net/netfilter/nf_conntrack_count");
12
- const maxRaw = readProcFile("/proc/sys/net/netfilter/nf_conntrack_max");
13
-
14
- if (!countRaw || !maxRaw) {
15
- return { available: false, count: 0, max: 0, percent: 0 };
16
- }
17
-
18
- const count = parseInt(countRaw.trim(), 10);
19
- const max = parseInt(maxRaw.trim(), 10);
20
-
21
- if (isNaN(count) || isNaN(max) || max === 0) {
22
- return { available: false, count: 0, max: 0, percent: 0 };
23
- }
24
-
25
- const percent = Math.round(((count / max) * 100) * 10) / 10;
26
- return { available: true, count, max, percent };
27
- }
@@ -1,92 +0,0 @@
1
- import { readProcFile, sleep } from "../lib/parse.js";
2
- import type { CpuInfo, CpuCoreInfo } from "../lib/types.js";
3
-
4
- interface CpuStat {
5
- user: number; nice: number; system: number; idle: number;
6
- iowait: number; irq: number; softirq: number; steal: number;
7
- }
8
-
9
- function parseLine(line: string): CpuStat {
10
- const parts = line.split(/\s+/).slice(1).map(Number);
11
- return {
12
- user: parts[0] || 0, nice: parts[1] || 0, system: parts[2] || 0, idle: parts[3] || 0,
13
- iowait: parts[4] || 0, irq: parts[5] || 0, softirq: parts[6] || 0, steal: parts[7] || 0,
14
- };
15
- }
16
-
17
- function parseProcStat(): { aggregate: CpuStat; cores: CpuStat[] } {
18
- const raw = readProcFile("/proc/stat") || "";
19
- const lines = raw.split("\n");
20
- const aggLine = lines.find((l) => l.startsWith("cpu "));
21
- const aggregate = aggLine ? parseLine(aggLine) : { user: 0, nice: 0, system: 0, idle: 0, iowait: 0, irq: 0, softirq: 0, steal: 0 };
22
-
23
- const cores: CpuStat[] = [];
24
- for (const line of lines) {
25
- if (/^cpu\d+\s/.test(line)) {
26
- cores.push(parseLine(line));
27
- }
28
- }
29
-
30
- return { aggregate, cores };
31
- }
32
-
33
- function calcPercents(d: CpuStat): { user: number; system: number; iowait: number; idle: number; irq: number; softirq: number } {
34
- const total = Object.values(d).reduce((a, b) => a + b, 0) || 1;
35
- const r = (v: number) => Math.round((v / total) * 10000) / 100;
36
- return {
37
- user: r(d.user + d.nice),
38
- system: r(d.system),
39
- iowait: r(d.iowait),
40
- idle: r(d.idle),
41
- irq: r(d.irq),
42
- softirq: r(d.softirq),
43
- };
44
- }
45
-
46
- function delta(a: CpuStat, b: CpuStat): CpuStat {
47
- return {
48
- user: b.user - a.user, nice: b.nice - a.nice,
49
- system: b.system - a.system, idle: b.idle - a.idle,
50
- iowait: b.iowait - a.iowait, irq: b.irq - a.irq,
51
- softirq: b.softirq - a.softirq, steal: b.steal - a.steal,
52
- };
53
- }
54
-
55
- export async function collectCpu(): Promise<CpuInfo> {
56
- const stat1 = parseProcStat();
57
- await sleep(1000);
58
- const stat2 = parseProcStat();
59
-
60
- const aggDelta = delta(stat1.aggregate, stat2.aggregate);
61
- const agg = calcPercents(aggDelta);
62
-
63
- const loadavg = (readProcFile("/proc/loadavg") || "0 0 0").trim().split(" ");
64
-
65
- // Per-core stats
66
- const cores: CpuCoreInfo[] = [];
67
- const coreCount = Math.min(stat1.cores.length, stat2.cores.length);
68
- for (let i = 0; i < coreCount; i++) {
69
- const d = delta(stat1.cores[i], stat2.cores[i]);
70
- const p = calcPercents(d);
71
- cores.push({
72
- core: i,
73
- user_percent: p.user,
74
- system_percent: p.system,
75
- iowait_percent: p.iowait,
76
- idle_percent: p.idle,
77
- irq_percent: p.irq,
78
- softirq_percent: p.softirq,
79
- });
80
- }
81
-
82
- return {
83
- user_percent: agg.user,
84
- system_percent: agg.system,
85
- iowait_percent: agg.iowait,
86
- idle_percent: agg.idle,
87
- load_1m: parseFloat(loadavg[0]) || 0,
88
- load_5m: parseFloat(loadavg[1]) || 0,
89
- load_15m: parseFloat(loadavg[2]) || 0,
90
- cores,
91
- };
92
- }
@@ -1,91 +0,0 @@
1
- import { run } from "../lib/exec.js";
2
- import { readProcFile } from "../lib/parse.js";
3
- import type { DiskInfo } from "../lib/types.js";
4
-
5
- interface MountInfo {
6
- device: string;
7
- mount: string;
8
- fstype: string;
9
- options: string;
10
- }
11
-
12
- function parseMounts(): MountInfo[] {
13
- const raw = readProcFile("/proc/mounts") || "";
14
- const result: MountInfo[] = [];
15
- for (const line of raw.split("\n")) {
16
- const parts = line.split(" ");
17
- if (parts.length < 4) continue;
18
- result.push({
19
- device: parts[0],
20
- mount: parts[1],
21
- fstype: parts[2],
22
- options: parts[3],
23
- });
24
- }
25
- return result;
26
- }
27
-
28
- export async function collectDisks(): Promise<DiskInfo[]> {
29
- const dfOutput = await run("df", ["-B1", "--output=source,target,size,used,avail,pcent", "-x", "tmpfs", "-x", "devtmpfs", "-x", "squashfs"]);
30
- if (!dfOutput) return [];
31
-
32
- // Get inode data (df -i without --output, parse standard columns)
33
- const dfInodeOutput = await run("df", ["-i", "-x", "tmpfs", "-x", "devtmpfs", "-x", "squashfs"]);
34
- const inodeMap = new Map<string, { total: number; used: number; free: number }>();
35
- if (dfInodeOutput) {
36
- // Standard df -i output: Filesystem Inodes IUsed IFree IUse% Mounted_on
37
- for (const line of dfInodeOutput.trim().split("\n").slice(1)) {
38
- const parts = line.trim().split(/\s+/);
39
- if (parts.length < 6) continue;
40
- const mountPoint = parts[5];
41
- inodeMap.set(mountPoint, {
42
- total: parseInt(parts[1]) || 0,
43
- used: parseInt(parts[2]) || 0,
44
- free: parseInt(parts[3]) || 0,
45
- });
46
- }
47
- }
48
-
49
- // Get mount options and fstype from /proc/mounts
50
- const mounts = parseMounts();
51
- const mountMap = new Map<string, MountInfo>();
52
- for (const m of mounts) {
53
- mountMap.set(m.mount, m);
54
- }
55
-
56
- const lines = dfOutput.trim().split("\n").slice(1);
57
- const disks: DiskInfo[] = [];
58
-
59
- for (const line of lines) {
60
- const parts = line.trim().split(/\s+/);
61
- if (parts.length < 6) continue;
62
- const device = parts[0];
63
- const mount = parts[1];
64
- const totalBytes = parseInt(parts[2]) || 0;
65
- const usedBytes = parseInt(parts[3]) || 0;
66
- const availBytes = parseInt(parts[4]) || 0;
67
- const pctStr = parts[5].replace("%", "");
68
- const percent = parseInt(pctStr) || 0;
69
-
70
- if (!device.startsWith("/dev/")) continue;
71
-
72
- const mountInfo = mountMap.get(mount);
73
- const inodes = inodeMap.get(mount);
74
-
75
- disks.push({
76
- device,
77
- mount,
78
- total_gb: Math.round((totalBytes / 1073741824) * 100) / 100,
79
- used_gb: Math.round((usedBytes / 1073741824) * 100) / 100,
80
- available_gb: Math.round((availBytes / 1073741824) * 100) / 100,
81
- percent_used: percent,
82
- fstype: mountInfo?.fstype,
83
- options: mountInfo?.options,
84
- inodes_total: inodes?.total,
85
- inodes_used: inodes?.used,
86
- inodes_free: inodes?.free,
87
- });
88
- }
89
-
90
- return disks;
91
- }