@glassmkr/crucible 0.6.0 → 0.6.2
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/.github/workflows/publish.yml +25 -0
- package/README.md +83 -45
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +64 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +53 -0
- package/dist/cli.js.map +1 -0
- package/dist/collect/__tests__/ipmi.test.d.ts +1 -0
- package/dist/collect/__tests__/ipmi.test.js +90 -0
- package/dist/collect/__tests__/ipmi.test.js.map +1 -0
- package/dist/collect/__tests__/smart.test.d.ts +1 -0
- package/dist/collect/__tests__/smart.test.js +64 -0
- package/dist/collect/__tests__/smart.test.js.map +1 -0
- package/dist/collect/__tests__/zfs.test.d.ts +1 -0
- package/dist/collect/__tests__/zfs.test.js +68 -0
- package/dist/collect/__tests__/zfs.test.js.map +1 -0
- package/dist/collect/ipmi.d.ts +5 -1
- package/dist/collect/ipmi.js +6 -3
- package/dist/collect/ipmi.js.map +1 -1
- package/dist/collect/ntp.js +42 -35
- package/dist/collect/ntp.js.map +1 -1
- package/dist/collect/smart.d.ts +26 -0
- package/dist/collect/smart.js +28 -25
- package/dist/collect/smart.js.map +1 -1
- package/dist/collect/zfs.d.ts +2 -1
- package/dist/collect/zfs.js +7 -3
- package/dist/collect/zfs.js.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/__tests__/parse.test.d.ts +1 -0
- package/dist/lib/__tests__/parse.test.js +27 -0
- package/dist/lib/__tests__/parse.test.js.map +1 -0
- package/package.json +12 -3
- package/src/__tests__/cli.test.ts +74 -0
- package/src/cli.ts +62 -0
- package/src/collect/__tests__/ipmi.test.ts +96 -0
- package/src/collect/__tests__/smart.test.ts +68 -0
- package/src/collect/__tests__/zfs.test.ts +72 -0
- package/src/collect/ipmi.ts +6 -3
- package/src/collect/ntp.ts +40 -33
- package/src/collect/smart.ts +40 -28
- package/src/collect/zfs.ts +7 -2
- package/src/index.ts +13 -3
- package/src/lib/__tests__/parse.test.ts +28 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,25 @@
|
|
|
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/README.md
CHANGED
|
@@ -3,77 +3,115 @@
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://www.npmjs.com/package/@glassmkr/crucible)
|
|
5
5
|
|
|
6
|
-
Lightweight bare metal server monitoring agent. Collects hardware and OS health
|
|
6
|
+
Lightweight bare metal server monitoring agent. Collects hardware and OS health every 5 minutes and pushes snapshots to a [Forge](https://forge.glassmkr.com) dashboard, which evaluates 36 alert rules and sends notifications.
|
|
7
7
|
|
|
8
|
-
Open source. MIT licensed. Built by [Glassmkr](https://glassmkr.com).
|
|
8
|
+
Open source. MIT licensed. Built by [Glassmkr](https://glassmkr.com). See also [Bench](https://github.com/glassmkr/bench), the MCP server collection.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
13
|
+
npm install -g @glassmkr/crucible
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Or
|
|
16
|
+
Or use the bootstrap script:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
curl -sf https://forge.glassmkr.com/install | bash
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Quick Start
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
1. Create an API key in the Forge dashboard (Servers, then Add server).
|
|
25
|
+
2. Drop a config at `/etc/glassmkr/collector.yaml`:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
server_name: "web-01"
|
|
29
|
+
collection:
|
|
30
|
+
interval_seconds: 300
|
|
31
|
+
ipmi: true
|
|
32
|
+
smart: true
|
|
33
|
+
forge:
|
|
34
|
+
enabled: true
|
|
35
|
+
url: "https://forge.glassmkr.com"
|
|
36
|
+
api_key: "col_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. Run as a systemd service (recommended) or directly:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
sudo glassmkr-crucible
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Snapshots appear in the Forge dashboard within seconds of the first push.
|
|
46
|
+
|
|
47
|
+
## CLI Reference
|
|
34
48
|
|
|
35
|
-
|
|
49
|
+
```
|
|
50
|
+
glassmkr-crucible [options]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
-v, --version Print version and exit
|
|
54
|
+
-h, --help Print this help and exit
|
|
55
|
+
-c, --config Path to config file (default: /etc/glassmkr/collector.yaml)
|
|
56
|
+
```
|
|
36
57
|
|
|
37
|
-
|
|
58
|
+
`--config=PATH` and the legacy positional form `glassmkr-crucible /path/to.yaml` both work. Without options, Crucible runs as a long-lived collector daemon.
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
## Systemd Service
|
|
40
61
|
|
|
41
|
-
|
|
62
|
+
Create `/etc/systemd/system/glassmkr-collector.service`:
|
|
42
63
|
|
|
43
|
-
|
|
64
|
+
```ini
|
|
65
|
+
[Unit]
|
|
66
|
+
Description=Glassmkr Crucible - Bare Metal Monitoring
|
|
67
|
+
After=network.target
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
[Service]
|
|
70
|
+
Type=simple
|
|
71
|
+
User=root
|
|
72
|
+
ExecStart=/usr/bin/glassmkr-crucible /etc/glassmkr/collector.yaml
|
|
73
|
+
Restart=on-failure
|
|
74
|
+
RestartSec=10
|
|
46
75
|
|
|
47
|
-
|
|
76
|
+
[Install]
|
|
77
|
+
WantedBy=multi-user.target
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Enable and start:
|
|
48
81
|
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ipmi: true
|
|
54
|
-
smart: true
|
|
55
|
-
forge:
|
|
56
|
-
enabled: true
|
|
57
|
-
url: "https://forge.glassmkr.com"
|
|
58
|
-
api_key: "col_xxx"
|
|
82
|
+
```bash
|
|
83
|
+
sudo systemctl daemon-reload
|
|
84
|
+
sudo systemctl enable --now glassmkr-collector
|
|
85
|
+
sudo systemctl status glassmkr-collector
|
|
59
86
|
```
|
|
60
87
|
|
|
61
|
-
|
|
88
|
+
## What It Collects
|
|
89
|
+
|
|
90
|
+
| Module | Data |
|
|
91
|
+
|--------|------|
|
|
92
|
+
| CPU | Aggregate and per-core utilization (user, system, iowait, idle) |
|
|
93
|
+
| Memory | RAM usage, swap usage |
|
|
94
|
+
| Disks | Space per mount point, inode counts, mount options, filesystem type |
|
|
95
|
+
| SMART | Drive health, model, temperature, power-on hours, reallocated sectors, NVMe wear |
|
|
96
|
+
| Network | Interface traffic, delta error/drop counters, link speed |
|
|
97
|
+
| RAID | mdadm array status, degraded detection |
|
|
98
|
+
| IPMI | Sensor readings, ECC errors, SEL events, fan RPM |
|
|
99
|
+
| Security | SSH config, firewall status, pending updates, kernel vulnerabilities, kernel-needs-reboot |
|
|
100
|
+
| ZFS | Pool state, scrub age, scrub errors |
|
|
101
|
+
| I/O | Per-device latency, IOPS, dmesg I/O errors |
|
|
102
|
+
| Conntrack | nf_conntrack table usage |
|
|
103
|
+
| Systemd | Failed unit count |
|
|
104
|
+
| NTP | Sync state and source |
|
|
105
|
+
| File descriptors | System-wide allocation |
|
|
106
|
+
|
|
107
|
+
Forge evaluates 36 alert rules server-side across OS, Storage, Network, Hardware, ZFS, Security, and Service Health, with priorities P1 Urgent through P4 Low. Full list: [forge.glassmkr.com/docs/alerts](https://forge.glassmkr.com/docs/alerts).
|
|
62
108
|
|
|
63
109
|
## Requirements
|
|
64
110
|
|
|
65
111
|
- Linux (any distribution: Ubuntu, Debian, RHEL, Rocky, Alma, Arch, Alpine)
|
|
66
112
|
- Node.js 18+
|
|
67
|
-
- Root access (for SMART, IPMI, and
|
|
68
|
-
- Optional: `smartmontools`
|
|
69
|
-
|
|
70
|
-
## How It Works
|
|
71
|
-
|
|
72
|
-
1. Crucible runs as a systemd service
|
|
73
|
-
2. Every 5 minutes, it collects a complete health snapshot
|
|
74
|
-
3. The snapshot is pushed to Forge via HTTPS (POST /api/v1/ingest)
|
|
75
|
-
4. Forge evaluates alert rules and sends notifications
|
|
76
|
-
5. Data appears in the Forge dashboard within seconds of push
|
|
113
|
+
- Root access (for SMART, IPMI, dmesg, and `/proc` access)
|
|
114
|
+
- Optional: `smartmontools` for SMART data, `ipmitool` for IPMI data, `zfsutils-linux` for ZFS pools
|
|
77
115
|
|
|
78
116
|
## Documentation
|
|
79
117
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseCliArgs, helpText, DEFAULT_CONFIG_PATH } from "../cli.js";
|
|
3
|
+
describe("parseCliArgs", () => {
|
|
4
|
+
it("--version returns version string and mode=version", () => {
|
|
5
|
+
const { result, output } = parseCliArgs(["--version"], "1.2.3");
|
|
6
|
+
expect(result.mode).toBe("version");
|
|
7
|
+
expect(output).toBe("glassmkr-crucible v1.2.3");
|
|
8
|
+
});
|
|
9
|
+
it("-v aliases --version", () => {
|
|
10
|
+
const { result, output } = parseCliArgs(["-v"], "1.2.3");
|
|
11
|
+
expect(result.mode).toBe("version");
|
|
12
|
+
expect(output).toBe("glassmkr-crucible v1.2.3");
|
|
13
|
+
});
|
|
14
|
+
it("--help returns help text and mode=help", () => {
|
|
15
|
+
const { result, output } = parseCliArgs(["--help"], "1.2.3");
|
|
16
|
+
expect(result.mode).toBe("help");
|
|
17
|
+
expect(output).toContain("glassmkr-crucible v1.2.3");
|
|
18
|
+
expect(output).toContain("Usage:");
|
|
19
|
+
expect(output).toContain("--version");
|
|
20
|
+
expect(output).toContain("--help");
|
|
21
|
+
expect(output).toContain("--config");
|
|
22
|
+
});
|
|
23
|
+
it("-h aliases --help", () => {
|
|
24
|
+
const { result } = parseCliArgs(["-h"], "1.2.3");
|
|
25
|
+
expect(result.mode).toBe("help");
|
|
26
|
+
});
|
|
27
|
+
it("no args returns mode=run with the default config path", () => {
|
|
28
|
+
const { result, output } = parseCliArgs([], "1.2.3");
|
|
29
|
+
expect(result.mode).toBe("run");
|
|
30
|
+
expect(result.configPath).toBe(DEFAULT_CONFIG_PATH);
|
|
31
|
+
expect(output).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
it("-c accepts a path in the next argument", () => {
|
|
34
|
+
const { result } = parseCliArgs(["-c", "/tmp/a.yaml"], "1.2.3");
|
|
35
|
+
expect(result.configPath).toBe("/tmp/a.yaml");
|
|
36
|
+
});
|
|
37
|
+
it("--config accepts a path in the next argument", () => {
|
|
38
|
+
const { result } = parseCliArgs(["--config", "/tmp/b.yaml"], "1.2.3");
|
|
39
|
+
expect(result.configPath).toBe("/tmp/b.yaml");
|
|
40
|
+
});
|
|
41
|
+
it("--config=PATH form works", () => {
|
|
42
|
+
const { result } = parseCliArgs(["--config=/tmp/c.yaml"], "1.2.3");
|
|
43
|
+
expect(result.configPath).toBe("/tmp/c.yaml");
|
|
44
|
+
});
|
|
45
|
+
it("legacy positional argument still sets config path", () => {
|
|
46
|
+
const { result } = parseCliArgs(["/tmp/legacy.yaml"], "1.2.3");
|
|
47
|
+
expect(result.configPath).toBe("/tmp/legacy.yaml");
|
|
48
|
+
});
|
|
49
|
+
it("--version wins over a provided config path (no collector start)", () => {
|
|
50
|
+
const { result } = parseCliArgs(["--config", "/tmp/x.yaml", "--version"], "1.2.3");
|
|
51
|
+
expect(result.mode).toBe("version");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("helpText", () => {
|
|
55
|
+
it("mentions the binary name, default config path, and both flags", () => {
|
|
56
|
+
const txt = helpText("0.6.1");
|
|
57
|
+
expect(txt).toContain("glassmkr-crucible v0.6.1");
|
|
58
|
+
expect(txt).toContain(DEFAULT_CONFIG_PATH);
|
|
59
|
+
expect(txt).toContain("-v, --version");
|
|
60
|
+
expect(txt).toContain("-h, --help");
|
|
61
|
+
expect(txt).toContain("-c, --config");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=cli.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../../src/__tests__/cli.test.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CliArgs {
|
|
2
|
+
mode: "version" | "help" | "run";
|
|
3
|
+
configPath: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
|
|
6
|
+
export declare function parseCliArgs(argv: string[], version: string): {
|
|
7
|
+
result: CliArgs;
|
|
8
|
+
output: string | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function helpText(version: string): string;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
export const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
|
|
5
|
+
export function parseCliArgs(argv, version) {
|
|
6
|
+
// argv is typically process.argv.slice(2)
|
|
7
|
+
let configPath = DEFAULT_CONFIG_PATH;
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (arg === "--version" || arg === "-v") {
|
|
11
|
+
return { result: { mode: "version", configPath: "" }, output: `glassmkr-crucible v${version}` };
|
|
12
|
+
}
|
|
13
|
+
if (arg === "--help" || arg === "-h") {
|
|
14
|
+
return { result: { mode: "help", configPath: "" }, output: helpText(version) };
|
|
15
|
+
}
|
|
16
|
+
// -c <path> or --config <path>
|
|
17
|
+
if (arg === "-c" || arg === "--config") {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next) {
|
|
20
|
+
configPath = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// --config=<path>
|
|
26
|
+
if (arg.startsWith("--config=")) {
|
|
27
|
+
configPath = arg.slice("--config=".length);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Legacy positional argument: first non-flag token
|
|
31
|
+
if (!arg.startsWith("-")) {
|
|
32
|
+
configPath = arg;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { result: { mode: "run", configPath }, output: null };
|
|
36
|
+
}
|
|
37
|
+
export function helpText(version) {
|
|
38
|
+
return [
|
|
39
|
+
`glassmkr-crucible v${version} - Bare metal server monitoring agent`,
|
|
40
|
+
"",
|
|
41
|
+
"Usage:",
|
|
42
|
+
" glassmkr-crucible [options]",
|
|
43
|
+
"",
|
|
44
|
+
"Options:",
|
|
45
|
+
" -v, --version Print version and exit",
|
|
46
|
+
" -h, --help Print this help and exit",
|
|
47
|
+
` -c, --config Path to config file (default: ${DEFAULT_CONFIG_PATH})`,
|
|
48
|
+
"",
|
|
49
|
+
"Without options, starts the collector daemon using the config file.",
|
|
50
|
+
"Docs: https://github.com/glassmkr/crucible",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAO9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,8BAA8B,CAAC;AAElE,MAAM,UAAU,YAAY,CAAC,IAAc,EAAE,OAAe;IAC1D,0CAA0C;IAC1C,IAAI,UAAU,GAAG,mBAAmB,CAAC;IAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACxC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,sBAAsB,OAAO,EAAE,EAAE,CAAC;QAClG,CAAC;QACD,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACrC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACjF,CAAC;QACD,+BAA+B;QAC/B,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACzB,IAAI,IAAI,EAAE,CAAC;gBACT,UAAU,GAAG,IAAI,CAAC;gBAClB,CAAC,EAAE,CAAC;YACN,CAAC;YACD,SAAS;QACX,CAAC;QACD,kBAAkB;QAClB,IAAI,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAChC,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAC3C,SAAS;QACX,CAAC;QACD,mDAAmD;QACnD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,UAAU,GAAG,GAAG,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO;QACL,sBAAsB,OAAO,uCAAuC;QACpE,EAAE;QACF,QAAQ;QACR,+BAA+B;QAC/B,EAAE;QACF,UAAU;QACV,2CAA2C;QAC3C,6CAA6C;QAC7C,oDAAoD,mBAAmB,GAAG;QAC1E,EAAE;QACF,qEAAqE;QACrE,4CAA4C;KAC7C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { classifySensor, deriveSelSeverity, parseSelTimestamp, parseFanStatus } from "../ipmi.js";
|
|
3
|
+
describe("classifySensor", () => {
|
|
4
|
+
it("recognizes memory sensors", () => {
|
|
5
|
+
expect(classifySensor("DIMM_A1")).toBe("memory");
|
|
6
|
+
expect(classifySensor("Memory ECC")).toBe("memory");
|
|
7
|
+
});
|
|
8
|
+
it("recognizes power supplies", () => {
|
|
9
|
+
expect(classifySensor("PSU1 Status")).toBe("power");
|
|
10
|
+
expect(classifySensor("Power Supply 1")).toBe("power");
|
|
11
|
+
});
|
|
12
|
+
it("recognizes fans, watchdog, processors, temps, voltage, storage, chassis", () => {
|
|
13
|
+
expect(classifySensor("Fan1")).toBe("fan");
|
|
14
|
+
expect(classifySensor("Watchdog")).toBe("watchdog");
|
|
15
|
+
expect(classifySensor("Processor 0")).toBe("processor");
|
|
16
|
+
// CPU-named temperature sensors classify as processor (cpu check wins over temp).
|
|
17
|
+
expect(classifySensor("CPU1 Temp")).toBe("processor");
|
|
18
|
+
expect(classifySensor("Inlet Temp")).toBe("temperature");
|
|
19
|
+
expect(classifySensor("VCore Voltage")).toBe("voltage");
|
|
20
|
+
expect(classifySensor("Drive Slot 1")).toBe("storage");
|
|
21
|
+
expect(classifySensor("Chassis Intrusion")).toBe("chassis");
|
|
22
|
+
});
|
|
23
|
+
it("falls back to 'other'", () => {
|
|
24
|
+
expect(classifySensor("Weird Sensor")).toBe("other");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("deriveSelSeverity", () => {
|
|
28
|
+
it("treats uncorrectable, thermal trip, AC lost as critical", () => {
|
|
29
|
+
expect(deriveSelSeverity("Uncorrectable ECC", "memory")).toBe("critical");
|
|
30
|
+
expect(deriveSelSeverity("Thermal trip", "processor")).toBe("critical");
|
|
31
|
+
expect(deriveSelSeverity("AC lost", "power")).toBe("critical");
|
|
32
|
+
expect(deriveSelSeverity("Machine check", "processor")).toBe("critical");
|
|
33
|
+
});
|
|
34
|
+
it("treats correctable ECC and redundancy lost as warning", () => {
|
|
35
|
+
expect(deriveSelSeverity("Correctable ECC", "memory")).toBe("warning");
|
|
36
|
+
expect(deriveSelSeverity("Redundancy lost", "power")).toBe("warning");
|
|
37
|
+
});
|
|
38
|
+
it("treats presence detected as info", () => {
|
|
39
|
+
expect(deriveSelSeverity("Presence detected", "memory")).toBe("info");
|
|
40
|
+
});
|
|
41
|
+
it("defaults to warning for memory/power/fan/processor sensor types", () => {
|
|
42
|
+
expect(deriveSelSeverity("Some odd event", "memory")).toBe("warning");
|
|
43
|
+
expect(deriveSelSeverity("Some odd event", "fan")).toBe("warning");
|
|
44
|
+
});
|
|
45
|
+
it("defaults to info for other sensor types", () => {
|
|
46
|
+
expect(deriveSelSeverity("Some odd event", "other")).toBe("info");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("parseSelTimestamp", () => {
|
|
50
|
+
it("formats a known date/time", () => {
|
|
51
|
+
expect(parseSelTimestamp("04/05/2026", "14:23:05")).toBe("2026-04-05T14:23:05Z");
|
|
52
|
+
});
|
|
53
|
+
it("pads single digit month/day", () => {
|
|
54
|
+
expect(parseSelTimestamp("4/5/2026", "09:00:00")).toBe("2026-04-05T09:00:00Z");
|
|
55
|
+
});
|
|
56
|
+
it("returns an ISO string for bad input (does not crash)", () => {
|
|
57
|
+
const out = parseSelTimestamp("", "");
|
|
58
|
+
expect(typeof out).toBe("string");
|
|
59
|
+
expect(out.length).toBeGreaterThan(10);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("parseFanStatus", () => {
|
|
63
|
+
it("parses healthy fan output", () => {
|
|
64
|
+
const raw = [
|
|
65
|
+
"FAN1 | 30h | ok | 7.1 | 5000 RPM",
|
|
66
|
+
"FAN2 | 31h | ok | 7.2 | 5100 RPM",
|
|
67
|
+
].join("\n");
|
|
68
|
+
const fans = parseFanStatus(raw);
|
|
69
|
+
expect(fans).toHaveLength(2);
|
|
70
|
+
expect(fans[0]).toMatchObject({ name: "FAN1", rpm: 5000, status: "ok" });
|
|
71
|
+
expect(fans[1].rpm).toBe(5100);
|
|
72
|
+
});
|
|
73
|
+
it("marks critical fans (cr/nr) as critical", () => {
|
|
74
|
+
const raw = "FAN1 | 30h | cr | 7.1 | 0 RPM";
|
|
75
|
+
const fans = parseFanStatus(raw);
|
|
76
|
+
expect(fans[0].status).toBe("critical");
|
|
77
|
+
});
|
|
78
|
+
it("marks absent/no-reading fans as absent", () => {
|
|
79
|
+
const raw = "FAN3 | 30h | ns | 7.1 | no reading";
|
|
80
|
+
const fans = parseFanStatus(raw);
|
|
81
|
+
expect(fans[0].status).toBe("absent");
|
|
82
|
+
expect(fans[0].rpm).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
it("treats 0 RPM with no explicit status as critical", () => {
|
|
85
|
+
const raw = "FAN1 | 30h | 7.1 | 0 RPM";
|
|
86
|
+
const fans = parseFanStatus(raw);
|
|
87
|
+
expect(fans[0].status).toBe("critical");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=ipmi.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipmi.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/ipmi.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseSmartctlJson } from "../smart.js";
|
|
3
|
+
describe("parseSmartctlJson", () => {
|
|
4
|
+
it("parses a healthy SATA SSD", () => {
|
|
5
|
+
const data = {
|
|
6
|
+
model_name: "Samsung SSD 970 EVO 1TB",
|
|
7
|
+
smart_status: { passed: true },
|
|
8
|
+
temperature: { current: 38 },
|
|
9
|
+
power_on_time: { hours: 9000 },
|
|
10
|
+
ata_smart_attributes: {
|
|
11
|
+
table: [
|
|
12
|
+
{ id: 5, name: "Reallocated_Sector_Ct", raw: { value: 0 } },
|
|
13
|
+
{ id: 197, name: "Current_Pending_Sector", raw: { value: 0 } },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
const info = parseSmartctlJson(data, "/dev/sda");
|
|
18
|
+
expect(info).toMatchObject({
|
|
19
|
+
device: "/dev/sda",
|
|
20
|
+
model: "Samsung SSD 970 EVO 1TB",
|
|
21
|
+
health: "PASSED",
|
|
22
|
+
temperature_c: 38,
|
|
23
|
+
power_on_hours: 9000,
|
|
24
|
+
reallocated_sectors: 0,
|
|
25
|
+
pending_sectors: 0,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it("parses a failing SATA drive with reallocated sectors", () => {
|
|
29
|
+
const data = {
|
|
30
|
+
model_name: "WD Red 4TB",
|
|
31
|
+
smart_status: { passed: false },
|
|
32
|
+
ata_smart_attributes: {
|
|
33
|
+
table: [
|
|
34
|
+
{ id: 5, raw: { value: 12 } },
|
|
35
|
+
{ id: 197, raw: { value: 3 } },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const info = parseSmartctlJson(data, "/dev/sdb");
|
|
40
|
+
expect(info.health).toBe("FAILED");
|
|
41
|
+
expect(info.reallocated_sectors).toBe(12);
|
|
42
|
+
expect(info.pending_sectors).toBe(3);
|
|
43
|
+
});
|
|
44
|
+
it("parses an NVMe drive with percentage_used", () => {
|
|
45
|
+
const data = {
|
|
46
|
+
model_name: "Samsung 980 PRO",
|
|
47
|
+
smart_status: { passed: true },
|
|
48
|
+
nvme_smart_health_information_log: { percentage_used: 22, temperature: 41 },
|
|
49
|
+
};
|
|
50
|
+
const info = parseSmartctlJson(data, "/dev/nvme0n1");
|
|
51
|
+
expect(info.percentage_used).toBe(22);
|
|
52
|
+
expect(info.temperature_c).toBe(41);
|
|
53
|
+
expect(info.health).toBe("PASSED");
|
|
54
|
+
});
|
|
55
|
+
it("falls back to 'unknown' model when absent", () => {
|
|
56
|
+
const info = parseSmartctlJson({ smart_status: { passed: true } }, "/dev/sdc");
|
|
57
|
+
expect(info.model).toBe("unknown");
|
|
58
|
+
});
|
|
59
|
+
it("treats missing smart_status as FAILED (safer default)", () => {
|
|
60
|
+
const info = parseSmartctlJson({}, "/dev/sdd");
|
|
61
|
+
expect(info.health).toBe("FAILED");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=smart.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smart.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/smart.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseZpoolStatus } from "../zfs.js";
|
|
3
|
+
describe("parseZpoolStatus", () => {
|
|
4
|
+
it("parses a healthy pool", () => {
|
|
5
|
+
const raw = ` pool: tank
|
|
6
|
+
state: ONLINE
|
|
7
|
+
scan: scrub repaired 0B in 01:23:45 with 0 errors on Sun Apr 5 12:34:56 2026
|
|
8
|
+
config:
|
|
9
|
+
|
|
10
|
+
NAME STATE READ WRITE CKSUM
|
|
11
|
+
tank ONLINE 0 0 0
|
|
12
|
+
mirror-0 ONLINE 0 0 0
|
|
13
|
+
|
|
14
|
+
errors: No known data errors
|
|
15
|
+
`;
|
|
16
|
+
const pools = parseZpoolStatus(raw);
|
|
17
|
+
expect(pools).toHaveLength(1);
|
|
18
|
+
expect(pools[0]).toMatchObject({
|
|
19
|
+
name: "tank",
|
|
20
|
+
state: "ONLINE",
|
|
21
|
+
errors_text: "No known data errors",
|
|
22
|
+
scrub_errors: 0,
|
|
23
|
+
scrub_repaired: "0B",
|
|
24
|
+
});
|
|
25
|
+
expect(pools[0].last_scrub_date).toContain("2026");
|
|
26
|
+
});
|
|
27
|
+
it("parses a DEGRADED pool", () => {
|
|
28
|
+
const raw = ` pool: tank
|
|
29
|
+
state: DEGRADED
|
|
30
|
+
scan: scrub repaired 16K in 02:00:00 with 3 errors on Sun Apr 5 12:34:56 2026
|
|
31
|
+
|
|
32
|
+
errors: 3 data errors, use '-v' for a list
|
|
33
|
+
`;
|
|
34
|
+
const [p] = parseZpoolStatus(raw);
|
|
35
|
+
expect(p.state).toBe("DEGRADED");
|
|
36
|
+
expect(p.scrub_errors).toBe(3);
|
|
37
|
+
expect(p.scrub_repaired).toBe("16K");
|
|
38
|
+
});
|
|
39
|
+
it("flags never-scrubbed pools", () => {
|
|
40
|
+
const raw = ` pool: tank
|
|
41
|
+
state: ONLINE
|
|
42
|
+
scan: none requested
|
|
43
|
+
|
|
44
|
+
errors: No known data errors
|
|
45
|
+
`;
|
|
46
|
+
const [p] = parseZpoolStatus(raw);
|
|
47
|
+
expect(p.scrub_never_run).toBe(true);
|
|
48
|
+
expect(p.scrub_errors).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
it("returns empty for no pools", () => {
|
|
51
|
+
expect(parseZpoolStatus("no pools available")).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it("parses multiple pools", () => {
|
|
54
|
+
const raw = ` pool: tank
|
|
55
|
+
state: ONLINE
|
|
56
|
+
scan: none requested
|
|
57
|
+
errors: No known data errors
|
|
58
|
+
pool: data
|
|
59
|
+
state: FAULTED
|
|
60
|
+
scan: none requested
|
|
61
|
+
errors: 2 data errors
|
|
62
|
+
`;
|
|
63
|
+
const pools = parseZpoolStatus(raw);
|
|
64
|
+
expect(pools.map((p) => p.name)).toEqual(["tank", "data"]);
|
|
65
|
+
expect(pools[1].state).toBe("FAULTED");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=zfs.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zfs.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/zfs.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,GAAG,GAAG;;;;;;;;;;CAUf,CAAC;QACE,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YAC7B,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,QAAQ;YACf,WAAW,EAAE,sBAAsB;YACnC,YAAY,EAAE,CAAC;YACf,cAAc,EAAE,IAAI;SACrB,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,GAAG,GAAG;;;;;CAKf,CAAC;QACE,MAAM,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,GAAG,GAAG;;;;;CAKf,CAAC;QACE,MAAM,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,aAAa,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,gBAAgB,CAAC,oBAAoB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,GAAG,GAAG;;;;;;;;CAQf,CAAC;QACE,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/collect/ipmi.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
import type { IpmiInfo } from "../lib/types.js";
|
|
1
|
+
import type { IpmiInfo, FanStatus } from "../lib/types.js";
|
|
2
2
|
export declare function collectIpmi(): Promise<IpmiInfo>;
|
|
3
|
+
export declare function parseSelTimestamp(date: string, time: string): string;
|
|
4
|
+
export declare function classifySensor(sensor: string): string;
|
|
5
|
+
export declare function deriveSelSeverity(event: string, sensorType: string): string;
|
|
6
|
+
export declare function parseFanStatus(output: string): FanStatus[];
|
package/dist/collect/ipmi.js
CHANGED
|
@@ -89,7 +89,7 @@ async function collectSelEvents() {
|
|
|
89
89
|
// Return last 20 events, most recent first
|
|
90
90
|
return events.slice(-20).reverse();
|
|
91
91
|
}
|
|
92
|
-
function parseSelTimestamp(date, time) {
|
|
92
|
+
export function parseSelTimestamp(date, time) {
|
|
93
93
|
if (!date || !time)
|
|
94
94
|
return new Date().toISOString();
|
|
95
95
|
// Format: "04/05/2026" and "14:23:05"
|
|
@@ -99,7 +99,7 @@ function parseSelTimestamp(date, time) {
|
|
|
99
99
|
const [month, day, year] = parts;
|
|
100
100
|
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}T${time}Z`;
|
|
101
101
|
}
|
|
102
|
-
function classifySensor(sensor) {
|
|
102
|
+
export function classifySensor(sensor) {
|
|
103
103
|
const lower = sensor.toLowerCase();
|
|
104
104
|
if (lower.includes("memory") || lower.includes("dimm"))
|
|
105
105
|
return "memory";
|
|
@@ -121,7 +121,7 @@ function classifySensor(sensor) {
|
|
|
121
121
|
return "chassis";
|
|
122
122
|
return "other";
|
|
123
123
|
}
|
|
124
|
-
function deriveSelSeverity(event, sensorType) {
|
|
124
|
+
export function deriveSelSeverity(event, sensorType) {
|
|
125
125
|
const lower = event.toLowerCase();
|
|
126
126
|
// Critical events
|
|
127
127
|
if (lower.includes("uncorrectable"))
|
|
@@ -174,6 +174,9 @@ async function collectFanStatus() {
|
|
|
174
174
|
const output = await run("ipmitool", ["sdr", "type", "Fan"]);
|
|
175
175
|
if (!output)
|
|
176
176
|
return [];
|
|
177
|
+
return parseFanStatus(output);
|
|
178
|
+
}
|
|
179
|
+
export function parseFanStatus(output) {
|
|
177
180
|
const fans = [];
|
|
178
181
|
const lines = output.trim().split("\n");
|
|
179
182
|
for (const line of lines) {
|