@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
package/src/collect/fd.ts DELETED
@@ -1,31 +0,0 @@
1
- import { readProcFile } from "../lib/parse.js";
2
-
3
- export interface FileDescriptorData {
4
- allocated: number;
5
- free: number;
6
- max: number;
7
- percent: number;
8
- }
9
-
10
- export function collectFileDescriptors(): FileDescriptorData {
11
- const raw = readProcFile("/proc/sys/fs/file-nr");
12
- if (!raw) {
13
- return { allocated: 0, free: 0, max: 0, percent: 0 };
14
- }
15
-
16
- const parts = raw.trim().split(/\s+/);
17
- if (parts.length < 3) {
18
- return { allocated: 0, free: 0, max: 0, percent: 0 };
19
- }
20
-
21
- const allocated = parseInt(parts[0], 10);
22
- const free = parseInt(parts[1], 10);
23
- const max = parseInt(parts[2], 10);
24
-
25
- if (isNaN(allocated) || isNaN(max) || max === 0) {
26
- return { allocated: 0, free: 0, max: 0, percent: 0 };
27
- }
28
-
29
- const percent = Math.round(((allocated / max) * 100) * 10) / 10;
30
- return { allocated, free: isNaN(free) ? 0 : free, max, percent };
31
- }
@@ -1,23 +0,0 @@
1
- import { run } from "../lib/exec.js";
2
-
3
- export async function collectIoErrors(): Promise<{ count: number; devices: string[] } | null> {
4
- // Parse dmesg for recent I/O errors (last 10 minutes covers the 5-min collection interval)
5
- const output = await run("bash", ["-c", 'dmesg -T --since "10 minutes ago" 2>/dev/null | grep -i "I/O error\\|Buffer I/O error\\|blk_update_request.*error"'], 5000);
6
- if (!output || !output.trim()) return null;
7
-
8
- const lines = output.trim().split("\n").filter((l) => l.trim());
9
- if (lines.length === 0) return null;
10
-
11
- // Extract device names from error messages
12
- const deviceSet = new Set<string>();
13
- for (const line of lines) {
14
- // "blk_update_request: I/O error, dev sda, sector 12345"
15
- const devMatch = line.match(/dev\s+(\w+)/);
16
- if (devMatch) deviceSet.add(devMatch[1]);
17
- // "Buffer I/O error on device sda1"
18
- const bufMatch = line.match(/on device\s+(\w+)/);
19
- if (bufMatch) deviceSet.add(bufMatch[1]);
20
- }
21
-
22
- return { count: lines.length, devices: Array.from(deviceSet) };
23
- }
@@ -1,103 +0,0 @@
1
- import { readProcFile } from "../lib/parse.js";
2
-
3
- export interface IoLatencyInfo {
4
- device: string;
5
- avg_read_latency_ms: number | null;
6
- avg_write_latency_ms: number | null;
7
- read_iops: number;
8
- write_iops: number;
9
- }
10
-
11
- interface DiskstatsCounters {
12
- reads_completed: number;
13
- read_time_ms: number;
14
- writes_completed: number;
15
- write_time_ms: number;
16
- }
17
-
18
- // Previous cumulative counters for delta computation
19
- const previousCounters = new Map<string, DiskstatsCounters>();
20
-
21
- // Match physical block devices, not partitions or virtual devices
22
- function isPhysicalDevice(name: string): boolean {
23
- // sd*, vd*, xvd* without trailing partition number
24
- if (/^(sd|vd|xvd)[a-z]+$/.test(name)) return true;
25
- // nvme*n* without partition suffix (nvme0n1 yes, nvme0n1p1 no)
26
- if (/^nvme\d+n\d+$/.test(name)) return true;
27
- // md* (RAID arrays)
28
- if (/^md\d+$/.test(name)) return true;
29
- return false;
30
- }
31
-
32
- function parseDiskstats(): Record<string, DiskstatsCounters> {
33
- const raw = readProcFile("/proc/diskstats") || "";
34
- const result: Record<string, DiskstatsCounters> = {};
35
-
36
- for (const line of raw.split("\n")) {
37
- const parts = line.trim().split(/\s+/);
38
- if (parts.length < 11) continue;
39
-
40
- const name = parts[2];
41
- if (!isPhysicalDevice(name)) continue;
42
-
43
- result[name] = {
44
- reads_completed: Number(parts[3]) || 0,
45
- read_time_ms: Number(parts[6]) || 0,
46
- writes_completed: Number(parts[7]) || 0,
47
- write_time_ms: Number(parts[10]) || 0,
48
- };
49
- }
50
-
51
- return result;
52
- }
53
-
54
- function delta(current: number, previous: number): number {
55
- if (current >= previous) return current - previous;
56
- return current; // counter wrapped or reset
57
- }
58
-
59
- export function collectIoLatency(): IoLatencyInfo[] {
60
- const current = parseDiskstats();
61
- const results: IoLatencyInfo[] = [];
62
- const currentDevices = new Set<string>();
63
-
64
- for (const [name, counters] of Object.entries(current)) {
65
- currentDevices.add(name);
66
- const prev = previousCounters.get(name);
67
-
68
- // Store current for next cycle
69
- previousCounters.set(name, { ...counters });
70
-
71
- if (!prev) {
72
- // First cycle: no delta, report null latency
73
- results.push({
74
- device: name,
75
- avg_read_latency_ms: null,
76
- avg_write_latency_ms: null,
77
- read_iops: 0,
78
- write_iops: 0,
79
- });
80
- continue;
81
- }
82
-
83
- const deltaReads = delta(counters.reads_completed, prev.reads_completed);
84
- const deltaReadTime = delta(counters.read_time_ms, prev.read_time_ms);
85
- const deltaWrites = delta(counters.writes_completed, prev.writes_completed);
86
- const deltaWriteTime = delta(counters.write_time_ms, prev.write_time_ms);
87
-
88
- results.push({
89
- device: name,
90
- avg_read_latency_ms: deltaReads > 0 ? Math.round((deltaReadTime / deltaReads) * 100) / 100 : null,
91
- avg_write_latency_ms: deltaWrites > 0 ? Math.round((deltaWriteTime / deltaWrites) * 100) / 100 : null,
92
- read_iops: deltaReads,
93
- write_iops: deltaWrites,
94
- });
95
- }
96
-
97
- // Remove stale devices
98
- for (const name of previousCounters.keys()) {
99
- if (!currentDevices.has(name)) previousCounters.delete(name);
100
- }
101
-
102
- return results;
103
- }
@@ -1,207 +0,0 @@
1
- import { run } from "../lib/exec.js";
2
- import type { IpmiInfo, SelEvent, FanStatus } from "../lib/types.js";
3
-
4
- export async function collectIpmi(): Promise<IpmiInfo> {
5
- const sensorRaw = await run("ipmitool", ["sensor"]);
6
- if (!sensorRaw) {
7
- return { available: false, sensors: [], ecc_errors: { correctable: 0, uncorrectable: 0 }, sel_entries_count: 0, sel_events_recent: [], fans: [] };
8
- }
9
-
10
- // Parse sensor readings
11
- const sensors: IpmiInfo["sensors"] = [];
12
- for (const line of sensorRaw.split("\n")) {
13
- const parts = line.split("|").map((s) => s.trim());
14
- if (parts.length < 4) continue;
15
- const name = parts[0];
16
- const rawValue = parts[1];
17
- const unit = parts[2];
18
- const status = parts[3];
19
-
20
- const numValue = parseFloat(rawValue);
21
- const value: number | string = isNaN(numValue) ? rawValue : numValue;
22
-
23
- let upperCritical: number | undefined;
24
- if (parts[8]) {
25
- const uc = parseFloat(parts[8]);
26
- if (!isNaN(uc)) upperCritical = uc;
27
- }
28
-
29
- sensors.push({ name, value, unit, status, upper_critical: upperCritical });
30
- }
31
-
32
- // ECC errors from memory-type sensors
33
- let correctable = 0;
34
- let uncorrectable = 0;
35
- for (const sensor of sensors) {
36
- const name = sensor.name.toLowerCase();
37
- if (name.includes("correctable") && typeof sensor.value === "number") {
38
- correctable += sensor.value;
39
- }
40
- if (name.includes("uncorrectable") && typeof sensor.value === "number") {
41
- uncorrectable += sensor.value;
42
- }
43
- }
44
-
45
- // SEL entry count
46
- let selCount = 0;
47
- const selInfo = await run("ipmitool", ["sel", "info"]);
48
- if (selInfo) {
49
- const match = selInfo.match(/Entries\s*:\s*(\d+)/i);
50
- if (match) selCount = parseInt(match[1], 10);
51
- }
52
-
53
- // SEL recent events
54
- const selEvents = await collectSelEvents();
55
-
56
- // Fan status
57
- const fans = await collectFanStatus();
58
-
59
- return {
60
- available: true,
61
- sensors,
62
- ecc_errors: { correctable, uncorrectable },
63
- sel_entries_count: selCount,
64
- sel_events_recent: selEvents,
65
- fans,
66
- };
67
- }
68
-
69
- async function collectSelEvents(): Promise<SelEvent[]> {
70
- const output = await run("ipmitool", ["sel", "elist"]);
71
- if (!output) return [];
72
-
73
- const events: SelEvent[] = [];
74
- const lines = output.trim().split("\n");
75
- const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000);
76
-
77
- for (const line of lines) {
78
- const parts = line.split("|").map((s) => s.trim());
79
- if (parts.length < 5) continue;
80
-
81
- const [idStr, date, time, sensor, event, direction] = parts;
82
-
83
- const timestamp = parseSelTimestamp(date, time);
84
- const tsDate = new Date(timestamp);
85
-
86
- // Only include events from the last 5 minutes on subsequent runs
87
- // On first run this will include everything (fiveMinAgo is always recent)
88
- // We keep last 20 events max regardless
89
- const sensorType = classifySensor(sensor);
90
- const severity = deriveSelSeverity(event, sensorType);
91
-
92
- events.push({
93
- id: parseInt(idStr) || 0,
94
- timestamp,
95
- sensor,
96
- sensor_type: sensorType,
97
- event,
98
- direction: direction || "Asserted",
99
- severity,
100
- });
101
- }
102
-
103
- // Return last 20 events, most recent first
104
- return events.slice(-20).reverse();
105
- }
106
-
107
- export function parseSelTimestamp(date: string, time: string): string {
108
- if (!date || !time) return new Date().toISOString();
109
- // Format: "04/05/2026" and "14:23:05"
110
- const parts = date.split("/");
111
- if (parts.length !== 3) return new Date().toISOString();
112
- const [month, day, year] = parts;
113
- return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}T${time}Z`;
114
- }
115
-
116
- export function classifySensor(sensor: string): string {
117
- const lower = sensor.toLowerCase();
118
- if (lower.includes("memory") || lower.includes("dimm")) return "memory";
119
- if (lower.includes("power supply") || lower.includes("psu")) return "power";
120
- if (lower.includes("fan")) return "fan";
121
- if (lower.includes("watchdog")) return "watchdog";
122
- if (lower.includes("processor") || lower.includes("cpu")) return "processor";
123
- if (lower.includes("temperature") || lower.includes("temp")) return "temperature";
124
- if (lower.includes("voltage")) return "voltage";
125
- if (lower.includes("drive") || lower.includes("disk")) return "storage";
126
- if (lower.includes("chassis") || lower.includes("intrusion")) return "chassis";
127
- return "other";
128
- }
129
-
130
- export function deriveSelSeverity(event: string, sensorType: string): string {
131
- const lower = event.toLowerCase();
132
-
133
- // Critical events
134
- if (lower.includes("uncorrectable")) return "critical";
135
- if (lower.includes("failure detected")) return "critical";
136
- if (lower.includes("ac lost")) return "critical";
137
- if (lower.includes("hard reset")) return "critical";
138
- if (lower.includes("power off")) return "critical";
139
- if (lower.includes("critical")) return "critical";
140
- if (lower.includes("non-recoverable")) return "critical";
141
- if (lower.includes("thermal trip")) return "critical";
142
- if (lower.includes("processor disabled")) return "critical";
143
- if (lower.includes("machine check")) return "critical";
144
-
145
- // Warning events
146
- if (lower.includes("correctable ecc")) return "warning";
147
- if (lower.includes("logging limit")) return "warning";
148
- if (lower.includes("lower critical going low")) return "warning";
149
- if (lower.includes("upper critical going high")) return "warning";
150
- if (lower.includes("redundancy lost")) return "warning";
151
- if (lower.includes("predictive failure")) return "warning";
152
- if (lower.includes("degraded")) return "warning";
153
-
154
- // Info events
155
- if (lower.includes("presence detected")) return "info";
156
- if (lower.includes("power cycle")) return "info";
157
- if (lower.includes("oem")) return "info";
158
-
159
- if (["memory", "power", "fan", "processor"].includes(sensorType)) return "warning";
160
- return "info";
161
- }
162
-
163
- async function collectFanStatus(): Promise<FanStatus[]> {
164
- const output = await run("ipmitool", ["sdr", "type", "Fan"]);
165
- if (!output) return [];
166
- return parseFanStatus(output);
167
- }
168
-
169
- export function parseFanStatus(output: string): FanStatus[] {
170
- const fans: FanStatus[] = [];
171
- const lines = output.trim().split("\n");
172
-
173
- for (const line of lines) {
174
- const parts = line.split("|").map((s) => s.trim());
175
- if (parts.length < 3) continue;
176
-
177
- const name = parts[0];
178
- const fullLine = parts.join(" ");
179
-
180
- let rpm = 0;
181
- let status = "ok";
182
-
183
- // Search all fields for RPM value (format varies by BMC)
184
- const rpmMatch = fullLine.match(/(\d+)\s*RPM/i);
185
- if (rpmMatch) {
186
- rpm = parseInt(rpmMatch[1]);
187
- }
188
-
189
- // Check status codes across all fields
190
- const hasNoReading = fullLine.toLowerCase().includes("no reading");
191
- const statusCodes = parts.slice(1).map((p) => p.toLowerCase());
192
- const hasCritical = statusCodes.some((s) => s === "cr" || s === "nr");
193
- const hasWarning = statusCodes.some((s) => s === "nc");
194
- const hasAbsent = statusCodes.some((s) => s === "ns") || hasNoReading;
195
- const hasOk = statusCodes.some((s) => s === "ok");
196
-
197
- if (hasCritical) status = "critical";
198
- else if (hasWarning) status = "warning";
199
- else if (hasAbsent) status = "absent";
200
- else if (hasOk) status = "ok";
201
- else if (rpm === 0 && !hasNoReading) status = "critical";
202
-
203
- fans.push({ name, rpm, status });
204
- }
205
-
206
- return fans;
207
- }
@@ -1,30 +0,0 @@
1
- import { readProcFile, parseKb } from "../lib/parse.js";
2
- import type { MemoryInfo } from "../lib/types.js";
3
-
4
- export async function collectMemory(): Promise<MemoryInfo> {
5
- const raw = readProcFile("/proc/meminfo") || "";
6
- const kv: Record<string, string> = {};
7
- for (const line of raw.split("\n")) {
8
- const match = line.match(/^(\w+):\s+(.+)/);
9
- if (match) kv[match[1]] = match[2];
10
- }
11
-
12
- const totalKb = parseKb(kv["MemTotal"]);
13
- const availableKb = parseKb(kv["MemAvailable"]);
14
- const swapTotalKb = parseKb(kv["SwapTotal"]);
15
- const swapFreeKb = parseKb(kv["SwapFree"]);
16
-
17
- const totalMb = Math.round(totalKb / 1024);
18
- const availableMb = Math.round(availableKb / 1024);
19
- const usedMb = totalMb - availableMb;
20
- const swapTotalMb = Math.round(swapTotalKb / 1024);
21
- const swapUsedMb = Math.round((swapTotalKb - swapFreeKb) / 1024);
22
-
23
- return {
24
- total_mb: totalMb,
25
- used_mb: usedMb,
26
- available_mb: availableMb,
27
- swap_total_mb: swapTotalMb,
28
- swap_used_mb: swapUsedMb,
29
- };
30
- }
@@ -1,193 +0,0 @@
1
- import { readProcFile, sleep } from "../lib/parse.js";
2
- import { readFileSync, readdirSync } from "fs";
3
- import type { NetworkInfo } from "../lib/types.js";
4
-
5
- interface IfaceStats {
6
- rx_bytes: number; rx_packets: number; rx_errors: number; rx_drops: number;
7
- tx_bytes: number; tx_packets: number; tx_errors: number; tx_drops: number;
8
- }
9
-
10
- // Previous cumulative counters for delta computation (persists in process memory across cycles)
11
- interface PreviousCounters {
12
- rx_errors: number;
13
- tx_errors: number;
14
- rx_drops: number;
15
- tx_drops: number;
16
- rx_packets: number;
17
- tx_packets: number;
18
- rx_crc_errors?: number;
19
- rx_frame_errors?: number;
20
- rx_length_errors?: number;
21
- tx_carrier_errors?: number;
22
- }
23
-
24
- const previousCounters = new Map<string, PreviousCounters>();
25
-
26
- function readStatCounter(iface: string, name: string): number | undefined {
27
- try {
28
- const raw = readFileSync(`/sys/class/net/${iface}/statistics/${name}`, "utf-8").trim();
29
- const val = parseInt(raw, 10);
30
- return Number.isFinite(val) ? val : undefined;
31
- } catch {
32
- return undefined;
33
- }
34
- }
35
-
36
- function parseNetDev(): Record<string, IfaceStats> {
37
- const raw = readProcFile("/proc/net/dev") || "";
38
- const result: Record<string, IfaceStats> = {};
39
- for (const line of raw.split("\n").slice(2)) {
40
- const match = line.match(/^\s*(\S+):\s+(.*)/);
41
- if (!match) continue;
42
- const name = match[1];
43
- // Skip virtual interfaces
44
- if (name === "lo" || name.startsWith("veth") || name.startsWith("docker") || name.startsWith("br-") || name.startsWith("virbr")) continue;
45
- const parts = match[2].trim().split(/\s+/).map(Number);
46
- result[name] = {
47
- rx_bytes: parts[0] || 0, rx_packets: parts[1] || 0, rx_errors: parts[2] || 0, rx_drops: parts[3] || 0,
48
- tx_bytes: parts[8] || 0, tx_packets: parts[9] || 0, tx_errors: parts[10] || 0, tx_drops: parts[11] || 0,
49
- };
50
- }
51
- return result;
52
- }
53
-
54
- function getSpeed(iface: string): number {
55
- try {
56
- const speed = readFileSync(`/sys/class/net/${iface}/speed`, "utf-8").trim();
57
- const val = parseInt(speed, 10);
58
- return isNaN(val) || val <= 0 ? 0 : val;
59
- } catch {
60
- return 0;
61
- }
62
- }
63
-
64
- function getOperstate(iface: string): string {
65
- try {
66
- return readFileSync(`/sys/class/net/${iface}/operstate`, "utf-8").trim();
67
- } catch {
68
- return "unknown";
69
- }
70
- }
71
-
72
- function getBondMaster(iface: string): string | undefined {
73
- try {
74
- const bonds = readdirSync("/proc/net/bonding/");
75
- for (const bond of bonds) {
76
- const content = readFileSync(`/proc/net/bonding/${bond}`, "utf-8");
77
- if (content.includes(`Slave Interface: ${iface}`)) return bond;
78
- }
79
- } catch {
80
- // No bonds or /proc/net/bonding doesn't exist
81
- }
82
- return undefined;
83
- }
84
-
85
- function isBondMaster(iface: string): boolean {
86
- try {
87
- return readdirSync("/proc/net/bonding/").includes(iface);
88
- } catch {
89
- return false;
90
- }
91
- }
92
-
93
- // Compute delta, handling counter wraps (current < previous means reset, use current as delta)
94
- function delta(current: number, previous: number): number {
95
- if (current >= previous) return current - previous;
96
- return current; // counter wrapped or reset
97
- }
98
-
99
- export async function collectNetwork(): Promise<NetworkInfo[]> {
100
- const stats1 = parseNetDev();
101
- await sleep(1000);
102
- const stats2 = parseNetDev();
103
-
104
- const currentIfaces = new Set<string>();
105
- const results: NetworkInfo[] = [];
106
-
107
- for (const [name, s2] of Object.entries(stats2)) {
108
- const s1 = stats1[name];
109
- if (!s1) continue;
110
- currentIfaces.add(name);
111
-
112
- const prev = previousCounters.get(name);
113
-
114
- // /sys/class/net/*/statistics/ exposes finer-grained RX/TX subtype
115
- // counters than /proc/net/dev. Read cumulative values here; delta is
116
- // derived below against the previous cycle's snapshot.
117
- const rxCrcCum = readStatCounter(name, "rx_crc_errors");
118
- const rxFrameCum = readStatCounter(name, "rx_frame_errors");
119
- const rxLenCum = readStatCounter(name, "rx_length_errors");
120
- const txCarrierCum = readStatCounter(name, "tx_carrier_errors");
121
-
122
- // Compute error/drop deltas (0 on first cycle after start or new interface)
123
- let rxErrorsDelta = 0;
124
- let txErrorsDelta = 0;
125
- let rxDropsDelta = 0;
126
- let txDropsDelta = 0;
127
- let rxPacketsDelta = 0;
128
- let txPacketsDelta = 0;
129
- let rxCrcDelta: number | undefined;
130
- let rxFrameDelta: number | undefined;
131
- let rxLenDelta: number | undefined;
132
- let txCarrierDelta: number | undefined;
133
-
134
- if (prev) {
135
- rxErrorsDelta = delta(s2.rx_errors, prev.rx_errors);
136
- txErrorsDelta = delta(s2.tx_errors, prev.tx_errors);
137
- rxDropsDelta = delta(s2.rx_drops, prev.rx_drops);
138
- txDropsDelta = delta(s2.tx_drops, prev.tx_drops);
139
- rxPacketsDelta = delta(s2.rx_packets, prev.rx_packets);
140
- txPacketsDelta = delta(s2.tx_packets, prev.tx_packets);
141
- if (rxCrcCum != null && prev.rx_crc_errors != null) rxCrcDelta = delta(rxCrcCum, prev.rx_crc_errors);
142
- if (rxFrameCum != null && prev.rx_frame_errors != null) rxFrameDelta = delta(rxFrameCum, prev.rx_frame_errors);
143
- if (rxLenCum != null && prev.rx_length_errors != null) rxLenDelta = delta(rxLenCum, prev.rx_length_errors);
144
- if (txCarrierCum != null && prev.tx_carrier_errors != null) txCarrierDelta = delta(txCarrierCum, prev.tx_carrier_errors);
145
- }
146
-
147
- // Store current cumulative values for next cycle
148
- previousCounters.set(name, {
149
- rx_errors: s2.rx_errors,
150
- tx_errors: s2.tx_errors,
151
- rx_drops: s2.rx_drops,
152
- tx_drops: s2.tx_drops,
153
- rx_packets: s2.rx_packets,
154
- tx_packets: s2.tx_packets,
155
- rx_crc_errors: rxCrcCum,
156
- rx_frame_errors: rxFrameCum,
157
- rx_length_errors: rxLenCum,
158
- tx_carrier_errors: txCarrierCum,
159
- });
160
-
161
- const entry: NetworkInfo = {
162
- interface: name,
163
- speed_mbps: getSpeed(name),
164
- rx_bytes_sec: s2.rx_bytes - s1.rx_bytes, // already a 1-second delta
165
- tx_bytes_sec: s2.tx_bytes - s1.tx_bytes,
166
- rx_errors: rxErrorsDelta,
167
- tx_errors: txErrorsDelta,
168
- rx_drops: rxDropsDelta,
169
- tx_drops: txDropsDelta,
170
- rx_packets: rxPacketsDelta,
171
- tx_packets: txPacketsDelta,
172
- operstate: getOperstate(name),
173
- };
174
- if (rxCrcDelta !== undefined) entry.rx_crc_errors = rxCrcDelta;
175
- if (rxFrameDelta !== undefined) entry.rx_frame_errors = rxFrameDelta;
176
- if (rxLenDelta !== undefined) entry.rx_length_errors = rxLenDelta;
177
- if (txCarrierDelta !== undefined) entry.tx_carrier_errors = txCarrierDelta;
178
- const master = getBondMaster(name);
179
- if (master) entry.bond_master = master;
180
- // Identify bond masters (have at least one slave pointing at them).
181
- if (isBondMaster(name)) entry.is_bond_master = true;
182
- results.push(entry);
183
- }
184
-
185
- // Remove stale interfaces that disappeared
186
- for (const name of previousCounters.keys()) {
187
- if (!currentIfaces.has(name)) {
188
- previousCounters.delete(name);
189
- }
190
- }
191
-
192
- return results;
193
- }