@checkstack/collector-hardware-backend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/package.json +24 -0
- package/src/collectors/cpu.test.ts +138 -0
- package/src/collectors/cpu.ts +199 -0
- package/src/collectors/disk.test.ts +121 -0
- package/src/collectors/disk.ts +171 -0
- package/src/collectors/index.ts +12 -0
- package/src/collectors/memory.test.ts +128 -0
- package/src/collectors/memory.ts +206 -0
- package/src/index.ts +26 -0
- package/src/plugin-metadata.ts +5 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @checkstack/collector-hardware-backend
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f5b1f49: Added CPU, Disk, and Memory hardware collectors for SSH-based system monitoring.
|
|
8
|
+
|
|
9
|
+
- `CpuCollector`: Monitors CPU usage, load averages (1m, 5m, 15m), and core count
|
|
10
|
+
- `DiskCollector`: Monitors disk usage for configurable mount points
|
|
11
|
+
- `MemoryCollector`: Monitors RAM usage and optional swap metrics
|
|
12
|
+
- All collectors work via SSH transport for remote system monitoring
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [f5b1f49]
|
|
17
|
+
- Updated dependencies [f5b1f49]
|
|
18
|
+
- Updated dependencies [f5b1f49]
|
|
19
|
+
- Updated dependencies [f5b1f49]
|
|
20
|
+
- Updated dependencies [f5b1f49]
|
|
21
|
+
- @checkstack/backend-api@0.1.0
|
|
22
|
+
- @checkstack/healthcheck-common@0.1.0
|
|
23
|
+
- @checkstack/healthcheck-ssh-common@0.1.0
|
|
24
|
+
- @checkstack/common@0.0.3
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/collector-hardware-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"lint": "bun run lint:code",
|
|
9
|
+
"lint:code": "eslint . --max-warnings 0",
|
|
10
|
+
"test": "bun test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@checkstack/backend-api": "workspace:*",
|
|
14
|
+
"@checkstack/common": "workspace:*",
|
|
15
|
+
"@checkstack/healthcheck-common": "workspace:*",
|
|
16
|
+
"@checkstack/healthcheck-ssh-common": "workspace:*"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/bun": "^1.0.0",
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
22
|
+
"@checkstack/scripts": "workspace:*"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { CpuCollector, type CpuConfig } from "./cpu";
|
|
3
|
+
import type { SshTransportClient } from "@checkstack/healthcheck-ssh-common";
|
|
4
|
+
|
|
5
|
+
describe("CpuCollector", () => {
|
|
6
|
+
const createMockClient = (
|
|
7
|
+
responses: {
|
|
8
|
+
stat1?: { stdout: string };
|
|
9
|
+
stat2?: { stdout: string };
|
|
10
|
+
loadavg?: { stdout: string };
|
|
11
|
+
nproc?: { stdout: string };
|
|
12
|
+
} = {}
|
|
13
|
+
): SshTransportClient => {
|
|
14
|
+
let callCount = 0;
|
|
15
|
+
return {
|
|
16
|
+
exec: mock((cmd: string) => {
|
|
17
|
+
if (cmd.includes("/proc/stat")) {
|
|
18
|
+
callCount++;
|
|
19
|
+
if (callCount === 1) {
|
|
20
|
+
return Promise.resolve({
|
|
21
|
+
exitCode: 0,
|
|
22
|
+
stdout:
|
|
23
|
+
responses.stat1?.stdout ?? "cpu 100 0 50 800 50 0 0 0 0 0",
|
|
24
|
+
stderr: "",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
exitCode: 0,
|
|
29
|
+
stdout: responses.stat2?.stdout ?? "cpu 150 0 100 850 50 0 0 0 0 0",
|
|
30
|
+
stderr: "",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (cmd.includes("/proc/loadavg")) {
|
|
34
|
+
return Promise.resolve({
|
|
35
|
+
exitCode: 0,
|
|
36
|
+
stdout: responses.loadavg?.stdout ?? "0.50 0.75 1.00 1/100 12345",
|
|
37
|
+
stderr: "",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (cmd.includes("nproc")) {
|
|
41
|
+
return Promise.resolve({
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
stdout: responses.nproc?.stdout ?? "4\n",
|
|
44
|
+
stderr: "",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" });
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe("execute", () => {
|
|
53
|
+
it("should collect CPU usage", async () => {
|
|
54
|
+
const collector = new CpuCollector();
|
|
55
|
+
const client = createMockClient();
|
|
56
|
+
|
|
57
|
+
const result = await collector.execute({
|
|
58
|
+
config: { includeLoadAverage: false, includeCoreCount: false },
|
|
59
|
+
client,
|
|
60
|
+
pluginId: "test",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.result.usagePercent).toBeDefined();
|
|
64
|
+
expect(result.result.usagePercent).toBeGreaterThanOrEqual(0);
|
|
65
|
+
expect(result.result.usagePercent).toBeLessThanOrEqual(100);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should include load averages when configured", async () => {
|
|
69
|
+
const collector = new CpuCollector();
|
|
70
|
+
const client = createMockClient({
|
|
71
|
+
loadavg: { stdout: "1.50 2.00 3.00 1/200 12345" },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result = await collector.execute({
|
|
75
|
+
config: { includeLoadAverage: true, includeCoreCount: false },
|
|
76
|
+
client,
|
|
77
|
+
pluginId: "test",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result.result.loadAvg1m).toBe(1.5);
|
|
81
|
+
expect(result.result.loadAvg5m).toBe(2);
|
|
82
|
+
expect(result.result.loadAvg15m).toBe(3);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should include core count when configured", async () => {
|
|
86
|
+
const collector = new CpuCollector();
|
|
87
|
+
const client = createMockClient({ nproc: { stdout: "8\n" } });
|
|
88
|
+
|
|
89
|
+
const result = await collector.execute({
|
|
90
|
+
config: { includeLoadAverage: false, includeCoreCount: true },
|
|
91
|
+
client,
|
|
92
|
+
pluginId: "test",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(result.result.coreCount).toBe(8);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("aggregateResult", () => {
|
|
100
|
+
it("should calculate average and max CPU usage", () => {
|
|
101
|
+
const collector = new CpuCollector();
|
|
102
|
+
const runs = [
|
|
103
|
+
{
|
|
104
|
+
id: "1",
|
|
105
|
+
status: "healthy" as const,
|
|
106
|
+
latencyMs: 100,
|
|
107
|
+
checkId: "c1",
|
|
108
|
+
timestamp: new Date(),
|
|
109
|
+
metadata: { usagePercent: 25, loadAvg1m: 1.0 },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "2",
|
|
113
|
+
status: "healthy" as const,
|
|
114
|
+
latencyMs: 100,
|
|
115
|
+
checkId: "c1",
|
|
116
|
+
timestamp: new Date(),
|
|
117
|
+
metadata: { usagePercent: 75, loadAvg1m: 2.0 },
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const aggregated = collector.aggregateResult(runs);
|
|
122
|
+
|
|
123
|
+
expect(aggregated.avgUsagePercent).toBe(50);
|
|
124
|
+
expect(aggregated.maxUsagePercent).toBe(75);
|
|
125
|
+
expect(aggregated.avgLoadAvg1m).toBe(1.5);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("metadata", () => {
|
|
130
|
+
it("should have correct static properties", () => {
|
|
131
|
+
const collector = new CpuCollector();
|
|
132
|
+
|
|
133
|
+
expect(collector.id).toBe("cpu");
|
|
134
|
+
expect(collector.displayName).toBe("CPU Metrics");
|
|
135
|
+
expect(collector.supportedPlugins).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
type HealthCheckRunForAggregation,
|
|
5
|
+
type CollectorResult,
|
|
6
|
+
type CollectorStrategy,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import { healthResultNumber } from "@checkstack/healthcheck-common";
|
|
9
|
+
import {
|
|
10
|
+
pluginMetadata as sshPluginMetadata,
|
|
11
|
+
type SshTransportClient,
|
|
12
|
+
} from "@checkstack/healthcheck-ssh-common";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// CONFIGURATION SCHEMA
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const cpuConfigSchema = z.object({
|
|
19
|
+
includeLoadAverage: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.default(true)
|
|
22
|
+
.describe("Include 1m, 5m, 15m load averages"),
|
|
23
|
+
includeCoreCount: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.default(true)
|
|
26
|
+
.describe("Include number of CPU cores"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type CpuConfig = z.infer<typeof cpuConfigSchema>;
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// RESULT SCHEMAS
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const cpuResultSchema = z.object({
|
|
36
|
+
usagePercent: healthResultNumber({
|
|
37
|
+
"x-chart-type": "line",
|
|
38
|
+
"x-chart-label": "CPU Usage",
|
|
39
|
+
"x-chart-unit": "%",
|
|
40
|
+
}),
|
|
41
|
+
loadAvg1m: healthResultNumber({
|
|
42
|
+
"x-chart-type": "line",
|
|
43
|
+
"x-chart-label": "Load (1m)",
|
|
44
|
+
}).optional(),
|
|
45
|
+
loadAvg5m: healthResultNumber({
|
|
46
|
+
"x-chart-type": "line",
|
|
47
|
+
"x-chart-label": "Load (5m)",
|
|
48
|
+
}).optional(),
|
|
49
|
+
loadAvg15m: healthResultNumber({
|
|
50
|
+
"x-chart-type": "line",
|
|
51
|
+
"x-chart-label": "Load (15m)",
|
|
52
|
+
}).optional(),
|
|
53
|
+
coreCount: healthResultNumber({
|
|
54
|
+
"x-chart-type": "counter",
|
|
55
|
+
"x-chart-label": "CPU Cores",
|
|
56
|
+
}).optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type CpuResult = z.infer<typeof cpuResultSchema>;
|
|
60
|
+
|
|
61
|
+
const cpuAggregatedSchema = z.object({
|
|
62
|
+
avgUsagePercent: healthResultNumber({
|
|
63
|
+
"x-chart-type": "line",
|
|
64
|
+
"x-chart-label": "Avg CPU Usage",
|
|
65
|
+
"x-chart-unit": "%",
|
|
66
|
+
}),
|
|
67
|
+
maxUsagePercent: healthResultNumber({
|
|
68
|
+
"x-chart-type": "line",
|
|
69
|
+
"x-chart-label": "Max CPU Usage",
|
|
70
|
+
"x-chart-unit": "%",
|
|
71
|
+
}),
|
|
72
|
+
avgLoadAvg1m: healthResultNumber({
|
|
73
|
+
"x-chart-type": "line",
|
|
74
|
+
"x-chart-label": "Avg Load (1m)",
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type CpuAggregatedResult = z.infer<typeof cpuAggregatedSchema>;
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// CPU COLLECTOR
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
export class CpuCollector
|
|
85
|
+
implements
|
|
86
|
+
CollectorStrategy<
|
|
87
|
+
SshTransportClient,
|
|
88
|
+
CpuConfig,
|
|
89
|
+
CpuResult,
|
|
90
|
+
CpuAggregatedResult
|
|
91
|
+
>
|
|
92
|
+
{
|
|
93
|
+
id = "cpu";
|
|
94
|
+
displayName = "CPU Metrics";
|
|
95
|
+
description = "Collects CPU usage, load averages, and core count via SSH";
|
|
96
|
+
|
|
97
|
+
supportedPlugins = [sshPluginMetadata];
|
|
98
|
+
|
|
99
|
+
config = new Versioned({ version: 1, schema: cpuConfigSchema });
|
|
100
|
+
result = new Versioned({ version: 1, schema: cpuResultSchema });
|
|
101
|
+
aggregatedResult = new Versioned({ version: 1, schema: cpuAggregatedSchema });
|
|
102
|
+
|
|
103
|
+
async execute({
|
|
104
|
+
config,
|
|
105
|
+
client,
|
|
106
|
+
}: {
|
|
107
|
+
config: CpuConfig;
|
|
108
|
+
client: SshTransportClient;
|
|
109
|
+
pluginId: string;
|
|
110
|
+
}): Promise<CollectorResult<CpuResult>> {
|
|
111
|
+
// Get CPU usage from /proc/stat (two samples to calculate delta)
|
|
112
|
+
const stat1 = await client.exec("cat /proc/stat | head -1");
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms sample
|
|
114
|
+
const stat2 = await client.exec("cat /proc/stat | head -1");
|
|
115
|
+
|
|
116
|
+
const usagePercent = this.calculateCpuUsage(stat1.stdout, stat2.stdout);
|
|
117
|
+
|
|
118
|
+
const result: CpuResult = { usagePercent };
|
|
119
|
+
|
|
120
|
+
// Get load averages
|
|
121
|
+
if (config.includeLoadAverage) {
|
|
122
|
+
const uptime = await client.exec("cat /proc/loadavg");
|
|
123
|
+
const loads = this.parseLoadAvg(uptime.stdout);
|
|
124
|
+
result.loadAvg1m = loads.load1m;
|
|
125
|
+
result.loadAvg5m = loads.load5m;
|
|
126
|
+
result.loadAvg15m = loads.load15m;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get core count
|
|
130
|
+
if (config.includeCoreCount) {
|
|
131
|
+
const nproc = await client.exec("nproc");
|
|
132
|
+
result.coreCount = Number.parseInt(nproc.stdout.trim(), 10) || undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { result };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
aggregateResult(
|
|
139
|
+
runs: HealthCheckRunForAggregation<CpuResult>[]
|
|
140
|
+
): CpuAggregatedResult {
|
|
141
|
+
const usages = runs
|
|
142
|
+
.map((r) => r.metadata?.usagePercent)
|
|
143
|
+
.filter((v): v is number => typeof v === "number");
|
|
144
|
+
|
|
145
|
+
const loads = runs
|
|
146
|
+
.map((r) => r.metadata?.loadAvg1m)
|
|
147
|
+
.filter((v): v is number => typeof v === "number");
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
avgUsagePercent: usages.length > 0 ? this.avg(usages) : 0,
|
|
151
|
+
maxUsagePercent: usages.length > 0 ? Math.max(...usages) : 0,
|
|
152
|
+
avgLoadAvg1m: loads.length > 0 ? this.avg(loads) : 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// PARSING HELPERS
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
private parseCpuStat(line: string): { idle: number; total: number } {
|
|
161
|
+
// Format: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
|
162
|
+
const parts = line.trim().split(/\s+/).slice(1).map(Number);
|
|
163
|
+
const idle = parts[3] + parts[4]; // idle + iowait
|
|
164
|
+
const total = parts.reduce((a, b) => a + b, 0);
|
|
165
|
+
return { idle, total };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private calculateCpuUsage(stat1: string, stat2: string): number {
|
|
169
|
+
const s1 = this.parseCpuStat(stat1);
|
|
170
|
+
const s2 = this.parseCpuStat(stat2);
|
|
171
|
+
|
|
172
|
+
const idleDelta = s2.idle - s1.idle;
|
|
173
|
+
const totalDelta = s2.total - s1.total;
|
|
174
|
+
|
|
175
|
+
if (totalDelta === 0) return 0;
|
|
176
|
+
|
|
177
|
+
return Math.round(((totalDelta - idleDelta) / totalDelta) * 100 * 10) / 10;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private parseLoadAvg(output: string): {
|
|
181
|
+
load1m?: number;
|
|
182
|
+
load5m?: number;
|
|
183
|
+
load15m?: number;
|
|
184
|
+
} {
|
|
185
|
+
// Format: 0.00 0.01 0.05 1/234 5678
|
|
186
|
+
const parts = output.trim().split(/\s+/);
|
|
187
|
+
return {
|
|
188
|
+
load1m: Number.parseFloat(parts[0]) || undefined,
|
|
189
|
+
load5m: Number.parseFloat(parts[1]) || undefined,
|
|
190
|
+
load15m: Number.parseFloat(parts[2]) || undefined,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private avg(nums: number[]): number {
|
|
195
|
+
return (
|
|
196
|
+
Math.round((nums.reduce((a, b) => a + b, 0) / nums.length) * 10) / 10
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { DiskCollector, type DiskConfig } from "./disk";
|
|
3
|
+
import type { SshTransportClient } from "@checkstack/healthcheck-ssh-common";
|
|
4
|
+
|
|
5
|
+
describe("DiskCollector", () => {
|
|
6
|
+
const createMockClient = (
|
|
7
|
+
dfOutput: string = "/dev/sda1 100G 45G 55G 45% /"
|
|
8
|
+
): SshTransportClient => ({
|
|
9
|
+
exec: mock(() =>
|
|
10
|
+
Promise.resolve({
|
|
11
|
+
exitCode: 0,
|
|
12
|
+
stdout: dfOutput,
|
|
13
|
+
stderr: "",
|
|
14
|
+
})
|
|
15
|
+
),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("execute", () => {
|
|
19
|
+
it("should collect disk usage for mount point", async () => {
|
|
20
|
+
const collector = new DiskCollector();
|
|
21
|
+
const client = createMockClient("/dev/sda1 100G 45G 55G 45% /");
|
|
22
|
+
|
|
23
|
+
const result = await collector.execute({
|
|
24
|
+
config: { mountPoint: "/" },
|
|
25
|
+
client,
|
|
26
|
+
pluginId: "test",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result.result.filesystem).toBe("/dev/sda1");
|
|
30
|
+
expect(result.result.totalGb).toBe(100);
|
|
31
|
+
expect(result.result.usedGb).toBe(45);
|
|
32
|
+
expect(result.result.availableGb).toBe(55);
|
|
33
|
+
expect(result.result.usedPercent).toBe(45);
|
|
34
|
+
expect(result.result.mountPoint).toBe("/");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should handle different mount points", async () => {
|
|
38
|
+
const collector = new DiskCollector();
|
|
39
|
+
const client = createMockClient(
|
|
40
|
+
"/dev/sdb1 500G 200G 300G 40% /data"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const result = await collector.execute({
|
|
44
|
+
config: { mountPoint: "/data" },
|
|
45
|
+
client,
|
|
46
|
+
pluginId: "test",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.result.filesystem).toBe("/dev/sdb1");
|
|
50
|
+
expect(result.result.totalGb).toBe(500);
|
|
51
|
+
expect(result.result.usedPercent).toBe(40);
|
|
52
|
+
expect(result.result.mountPoint).toBe("/data");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should pass mount point to df command", async () => {
|
|
56
|
+
const collector = new DiskCollector();
|
|
57
|
+
const client = createMockClient();
|
|
58
|
+
|
|
59
|
+
await collector.execute({
|
|
60
|
+
config: { mountPoint: "/var" },
|
|
61
|
+
client,
|
|
62
|
+
pluginId: "test",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(client.exec).toHaveBeenCalledWith("df -BG /var | tail -1");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("aggregateResult", () => {
|
|
70
|
+
it("should calculate average and max disk usage", () => {
|
|
71
|
+
const collector = new DiskCollector();
|
|
72
|
+
const runs = [
|
|
73
|
+
{
|
|
74
|
+
id: "1",
|
|
75
|
+
status: "healthy" as const,
|
|
76
|
+
latencyMs: 100,
|
|
77
|
+
checkId: "c1",
|
|
78
|
+
timestamp: new Date(),
|
|
79
|
+
metadata: {
|
|
80
|
+
filesystem: "/dev/sda1",
|
|
81
|
+
totalGb: 100,
|
|
82
|
+
usedGb: 30,
|
|
83
|
+
availableGb: 70,
|
|
84
|
+
usedPercent: 30,
|
|
85
|
+
mountPoint: "/",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "2",
|
|
90
|
+
status: "healthy" as const,
|
|
91
|
+
latencyMs: 100,
|
|
92
|
+
checkId: "c1",
|
|
93
|
+
timestamp: new Date(),
|
|
94
|
+
metadata: {
|
|
95
|
+
filesystem: "/dev/sda1",
|
|
96
|
+
totalGb: 100,
|
|
97
|
+
usedGb: 50,
|
|
98
|
+
availableGb: 50,
|
|
99
|
+
usedPercent: 50,
|
|
100
|
+
mountPoint: "/",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const aggregated = collector.aggregateResult(runs);
|
|
106
|
+
|
|
107
|
+
expect(aggregated.avgUsedPercent).toBe(40);
|
|
108
|
+
expect(aggregated.maxUsedPercent).toBe(50);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("metadata", () => {
|
|
113
|
+
it("should have correct static properties", () => {
|
|
114
|
+
const collector = new DiskCollector();
|
|
115
|
+
|
|
116
|
+
expect(collector.id).toBe("disk");
|
|
117
|
+
expect(collector.displayName).toBe("Disk Metrics");
|
|
118
|
+
expect(collector.supportedPlugins).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
type HealthCheckRunForAggregation,
|
|
5
|
+
type CollectorResult,
|
|
6
|
+
type CollectorStrategy,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import {
|
|
9
|
+
healthResultNumber,
|
|
10
|
+
healthResultString,
|
|
11
|
+
} from "@checkstack/healthcheck-common";
|
|
12
|
+
import {
|
|
13
|
+
pluginMetadata as sshPluginMetadata,
|
|
14
|
+
type SshTransportClient,
|
|
15
|
+
} from "@checkstack/healthcheck-ssh-common";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// CONFIGURATION SCHEMA
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const diskConfigSchema = z.object({
|
|
22
|
+
mountPoint: z
|
|
23
|
+
.string()
|
|
24
|
+
.default("/")
|
|
25
|
+
.describe("Mount point to monitor (e.g., /, /home, /var)"),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type DiskConfig = z.infer<typeof diskConfigSchema>;
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// RESULT SCHEMAS
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
const diskResultSchema = z.object({
|
|
35
|
+
filesystem: healthResultString({
|
|
36
|
+
"x-chart-type": "text",
|
|
37
|
+
"x-chart-label": "Filesystem",
|
|
38
|
+
}),
|
|
39
|
+
totalGb: healthResultNumber({
|
|
40
|
+
"x-chart-type": "counter",
|
|
41
|
+
"x-chart-label": "Total Disk",
|
|
42
|
+
"x-chart-unit": "GB",
|
|
43
|
+
}),
|
|
44
|
+
usedGb: healthResultNumber({
|
|
45
|
+
"x-chart-type": "line",
|
|
46
|
+
"x-chart-label": "Used Disk",
|
|
47
|
+
"x-chart-unit": "GB",
|
|
48
|
+
}),
|
|
49
|
+
availableGb: healthResultNumber({
|
|
50
|
+
"x-chart-type": "line",
|
|
51
|
+
"x-chart-label": "Available Disk",
|
|
52
|
+
"x-chart-unit": "GB",
|
|
53
|
+
}),
|
|
54
|
+
usedPercent: healthResultNumber({
|
|
55
|
+
"x-chart-type": "gauge",
|
|
56
|
+
"x-chart-label": "Disk Usage",
|
|
57
|
+
"x-chart-unit": "%",
|
|
58
|
+
}),
|
|
59
|
+
mountPoint: healthResultString({
|
|
60
|
+
"x-chart-type": "text",
|
|
61
|
+
"x-chart-label": "Mount Point",
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export type DiskResult = z.infer<typeof diskResultSchema>;
|
|
66
|
+
|
|
67
|
+
const diskAggregatedSchema = z.object({
|
|
68
|
+
avgUsedPercent: healthResultNumber({
|
|
69
|
+
"x-chart-type": "line",
|
|
70
|
+
"x-chart-label": "Avg Disk Usage",
|
|
71
|
+
"x-chart-unit": "%",
|
|
72
|
+
}),
|
|
73
|
+
maxUsedPercent: healthResultNumber({
|
|
74
|
+
"x-chart-type": "line",
|
|
75
|
+
"x-chart-label": "Max Disk Usage",
|
|
76
|
+
"x-chart-unit": "%",
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type DiskAggregatedResult = z.infer<typeof diskAggregatedSchema>;
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// DISK COLLECTOR
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
export class DiskCollector
|
|
87
|
+
implements
|
|
88
|
+
CollectorStrategy<
|
|
89
|
+
SshTransportClient,
|
|
90
|
+
DiskConfig,
|
|
91
|
+
DiskResult,
|
|
92
|
+
DiskAggregatedResult
|
|
93
|
+
>
|
|
94
|
+
{
|
|
95
|
+
id = "disk";
|
|
96
|
+
displayName = "Disk Metrics";
|
|
97
|
+
description = "Collects disk usage for a specific mount point via SSH";
|
|
98
|
+
|
|
99
|
+
supportedPlugins = [sshPluginMetadata];
|
|
100
|
+
|
|
101
|
+
config = new Versioned({ version: 1, schema: diskConfigSchema });
|
|
102
|
+
result = new Versioned({ version: 1, schema: diskResultSchema });
|
|
103
|
+
aggregatedResult = new Versioned({
|
|
104
|
+
version: 1,
|
|
105
|
+
schema: diskAggregatedSchema,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
async execute({
|
|
109
|
+
config,
|
|
110
|
+
client,
|
|
111
|
+
}: {
|
|
112
|
+
config: DiskConfig;
|
|
113
|
+
client: SshTransportClient;
|
|
114
|
+
pluginId: string;
|
|
115
|
+
}): Promise<CollectorResult<DiskResult>> {
|
|
116
|
+
// Use df with specific mount point, output in 1G blocks
|
|
117
|
+
const dfResult = await client.exec(`df -BG ${config.mountPoint} | tail -1`);
|
|
118
|
+
const parsed = this.parseDfOutput(dfResult.stdout, config.mountPoint);
|
|
119
|
+
|
|
120
|
+
return { result: parsed };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
aggregateResult(
|
|
124
|
+
runs: HealthCheckRunForAggregation<DiskResult>[]
|
|
125
|
+
): DiskAggregatedResult {
|
|
126
|
+
const usedPercents = runs
|
|
127
|
+
.map((r) => r.metadata?.usedPercent)
|
|
128
|
+
.filter((v): v is number => typeof v === "number");
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
avgUsedPercent: usedPercents.length > 0 ? this.avg(usedPercents) : 0,
|
|
132
|
+
maxUsedPercent: usedPercents.length > 0 ? Math.max(...usedPercents) : 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// PARSING HELPERS
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
private parseGb(val: string): number {
|
|
141
|
+
// Remove 'G' suffix and parse
|
|
142
|
+
return Number.parseInt(val.replace(/G$/i, ""), 10) || 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private parseDfOutput(output: string, mountPoint: string): DiskResult {
|
|
146
|
+
// Format: Filesystem 1G-blocks Used Available Use% Mounted on
|
|
147
|
+
// /dev/sda1 100G 45G 55G 45% /
|
|
148
|
+
const parts = output.trim().split(/\s+/);
|
|
149
|
+
|
|
150
|
+
const filesystem = parts[0] || "unknown";
|
|
151
|
+
const totalGb = this.parseGb(parts[1]);
|
|
152
|
+
const usedGb = this.parseGb(parts[2]);
|
|
153
|
+
const availableGb = this.parseGb(parts[3]);
|
|
154
|
+
const usedPercent = Number.parseInt(parts[4]?.replace(/%$/, ""), 10) || 0;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
filesystem,
|
|
158
|
+
totalGb,
|
|
159
|
+
usedGb,
|
|
160
|
+
availableGb,
|
|
161
|
+
usedPercent,
|
|
162
|
+
mountPoint,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private avg(nums: number[]): number {
|
|
167
|
+
return (
|
|
168
|
+
Math.round((nums.reduce((a, b) => a + b, 0) / nums.length) * 10) / 10
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { CpuCollector } from "./cpu";
|
|
2
|
+
export type { CpuConfig, CpuResult, CpuAggregatedResult } from "./cpu";
|
|
3
|
+
|
|
4
|
+
export { MemoryCollector } from "./memory";
|
|
5
|
+
export type {
|
|
6
|
+
MemoryConfig,
|
|
7
|
+
MemoryResult,
|
|
8
|
+
MemoryAggregatedResult,
|
|
9
|
+
} from "./memory";
|
|
10
|
+
|
|
11
|
+
export { DiskCollector } from "./disk";
|
|
12
|
+
export type { DiskConfig, DiskResult, DiskAggregatedResult } from "./disk";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { MemoryCollector, type MemoryConfig } from "./memory";
|
|
3
|
+
import type { SshTransportClient } from "@checkstack/healthcheck-ssh-common";
|
|
4
|
+
|
|
5
|
+
describe("MemoryCollector", () => {
|
|
6
|
+
const createMockClient = (
|
|
7
|
+
freeOutput: string = ` total used free shared buff/cache available
|
|
8
|
+
Mem: 16000 4000 8000 500 4000 12000
|
|
9
|
+
Swap: 4096 512 3584`
|
|
10
|
+
): SshTransportClient => ({
|
|
11
|
+
exec: mock(() =>
|
|
12
|
+
Promise.resolve({
|
|
13
|
+
exitCode: 0,
|
|
14
|
+
stdout: freeOutput,
|
|
15
|
+
stderr: "",
|
|
16
|
+
})
|
|
17
|
+
),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("execute", () => {
|
|
21
|
+
it("should collect memory usage", async () => {
|
|
22
|
+
const collector = new MemoryCollector();
|
|
23
|
+
const client = createMockClient();
|
|
24
|
+
|
|
25
|
+
const result = await collector.execute({
|
|
26
|
+
config: { includeSwap: false, includeBuffersCache: false },
|
|
27
|
+
client,
|
|
28
|
+
pluginId: "test",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(result.result.totalMb).toBe(16000);
|
|
32
|
+
expect(result.result.usedMb).toBe(4000);
|
|
33
|
+
expect(result.result.freeMb).toBe(8000);
|
|
34
|
+
expect(result.result.usedPercent).toBe(25);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should include swap when configured", async () => {
|
|
38
|
+
const collector = new MemoryCollector();
|
|
39
|
+
const client = createMockClient();
|
|
40
|
+
|
|
41
|
+
const result = await collector.execute({
|
|
42
|
+
config: { includeSwap: true, includeBuffersCache: false },
|
|
43
|
+
client,
|
|
44
|
+
pluginId: "test",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.result.swapTotalMb).toBe(4096);
|
|
48
|
+
expect(result.result.swapUsedMb).toBe(512);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should not include swap when not configured", async () => {
|
|
52
|
+
const collector = new MemoryCollector();
|
|
53
|
+
const client = createMockClient();
|
|
54
|
+
|
|
55
|
+
const result = await collector.execute({
|
|
56
|
+
config: { includeSwap: false, includeBuffersCache: false },
|
|
57
|
+
client,
|
|
58
|
+
pluginId: "test",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.result.swapTotalMb).toBeUndefined();
|
|
62
|
+
expect(result.result.swapUsedMb).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should call free -m command", async () => {
|
|
66
|
+
const collector = new MemoryCollector();
|
|
67
|
+
const client = createMockClient();
|
|
68
|
+
|
|
69
|
+
await collector.execute({
|
|
70
|
+
config: { includeSwap: false, includeBuffersCache: false },
|
|
71
|
+
client,
|
|
72
|
+
pluginId: "test",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(client.exec).toHaveBeenCalledWith("free -m");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("aggregateResult", () => {
|
|
80
|
+
it("should calculate average and max memory usage", () => {
|
|
81
|
+
const collector = new MemoryCollector();
|
|
82
|
+
const runs = [
|
|
83
|
+
{
|
|
84
|
+
id: "1",
|
|
85
|
+
status: "healthy" as const,
|
|
86
|
+
latencyMs: 100,
|
|
87
|
+
checkId: "c1",
|
|
88
|
+
timestamp: new Date(),
|
|
89
|
+
metadata: {
|
|
90
|
+
totalMb: 16000,
|
|
91
|
+
usedMb: 4000,
|
|
92
|
+
freeMb: 12000,
|
|
93
|
+
usedPercent: 25,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "2",
|
|
98
|
+
status: "healthy" as const,
|
|
99
|
+
latencyMs: 100,
|
|
100
|
+
checkId: "c1",
|
|
101
|
+
timestamp: new Date(),
|
|
102
|
+
metadata: {
|
|
103
|
+
totalMb: 16000,
|
|
104
|
+
usedMb: 12000,
|
|
105
|
+
freeMb: 4000,
|
|
106
|
+
usedPercent: 75,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const aggregated = collector.aggregateResult(runs);
|
|
112
|
+
|
|
113
|
+
expect(aggregated.avgUsedPercent).toBe(50);
|
|
114
|
+
expect(aggregated.maxUsedPercent).toBe(75);
|
|
115
|
+
expect(aggregated.avgUsedMb).toBe(8000);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("metadata", () => {
|
|
120
|
+
it("should have correct static properties", () => {
|
|
121
|
+
const collector = new MemoryCollector();
|
|
122
|
+
|
|
123
|
+
expect(collector.id).toBe("memory");
|
|
124
|
+
expect(collector.displayName).toBe("Memory Metrics");
|
|
125
|
+
expect(collector.supportedPlugins).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
type HealthCheckRunForAggregation,
|
|
5
|
+
type CollectorResult,
|
|
6
|
+
type CollectorStrategy,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import { healthResultNumber } from "@checkstack/healthcheck-common";
|
|
9
|
+
import {
|
|
10
|
+
pluginMetadata as sshPluginMetadata,
|
|
11
|
+
type SshTransportClient,
|
|
12
|
+
} from "@checkstack/healthcheck-ssh-common";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// CONFIGURATION SCHEMA
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const memoryConfigSchema = z.object({
|
|
19
|
+
includeSwap: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.default(true)
|
|
22
|
+
.describe("Include swap usage in results"),
|
|
23
|
+
includeBuffersCache: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.default(false)
|
|
26
|
+
.describe("Include buffers/cache breakdown"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type MemoryConfig = z.infer<typeof memoryConfigSchema>;
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// RESULT SCHEMAS
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const memoryResultSchema = z.object({
|
|
36
|
+
totalMb: healthResultNumber({
|
|
37
|
+
"x-chart-type": "counter",
|
|
38
|
+
"x-chart-label": "Total Memory",
|
|
39
|
+
"x-chart-unit": "MB",
|
|
40
|
+
}),
|
|
41
|
+
usedMb: healthResultNumber({
|
|
42
|
+
"x-chart-type": "line",
|
|
43
|
+
"x-chart-label": "Used Memory",
|
|
44
|
+
"x-chart-unit": "MB",
|
|
45
|
+
}),
|
|
46
|
+
freeMb: healthResultNumber({
|
|
47
|
+
"x-chart-type": "line",
|
|
48
|
+
"x-chart-label": "Free Memory",
|
|
49
|
+
"x-chart-unit": "MB",
|
|
50
|
+
}),
|
|
51
|
+
usedPercent: healthResultNumber({
|
|
52
|
+
"x-chart-type": "gauge",
|
|
53
|
+
"x-chart-label": "Memory Usage",
|
|
54
|
+
"x-chart-unit": "%",
|
|
55
|
+
}),
|
|
56
|
+
swapUsedMb: healthResultNumber({
|
|
57
|
+
"x-chart-type": "line",
|
|
58
|
+
"x-chart-label": "Swap Used",
|
|
59
|
+
"x-chart-unit": "MB",
|
|
60
|
+
}).optional(),
|
|
61
|
+
swapTotalMb: healthResultNumber({
|
|
62
|
+
"x-chart-type": "counter",
|
|
63
|
+
"x-chart-label": "Swap Total",
|
|
64
|
+
"x-chart-unit": "MB",
|
|
65
|
+
}).optional(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type MemoryResult = z.infer<typeof memoryResultSchema>;
|
|
69
|
+
|
|
70
|
+
const memoryAggregatedSchema = z.object({
|
|
71
|
+
avgUsedPercent: healthResultNumber({
|
|
72
|
+
"x-chart-type": "line",
|
|
73
|
+
"x-chart-label": "Avg Memory Usage",
|
|
74
|
+
"x-chart-unit": "%",
|
|
75
|
+
}),
|
|
76
|
+
maxUsedPercent: healthResultNumber({
|
|
77
|
+
"x-chart-type": "line",
|
|
78
|
+
"x-chart-label": "Max Memory Usage",
|
|
79
|
+
"x-chart-unit": "%",
|
|
80
|
+
}),
|
|
81
|
+
avgUsedMb: healthResultNumber({
|
|
82
|
+
"x-chart-type": "line",
|
|
83
|
+
"x-chart-label": "Avg Memory Used",
|
|
84
|
+
"x-chart-unit": "MB",
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export type MemoryAggregatedResult = z.infer<typeof memoryAggregatedSchema>;
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// MEMORY COLLECTOR
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export class MemoryCollector
|
|
95
|
+
implements
|
|
96
|
+
CollectorStrategy<
|
|
97
|
+
SshTransportClient,
|
|
98
|
+
MemoryConfig,
|
|
99
|
+
MemoryResult,
|
|
100
|
+
MemoryAggregatedResult
|
|
101
|
+
>
|
|
102
|
+
{
|
|
103
|
+
id = "memory";
|
|
104
|
+
displayName = "Memory Metrics";
|
|
105
|
+
description = "Collects RAM and swap usage via SSH";
|
|
106
|
+
|
|
107
|
+
supportedPlugins = [sshPluginMetadata];
|
|
108
|
+
|
|
109
|
+
config = new Versioned({ version: 1, schema: memoryConfigSchema });
|
|
110
|
+
result = new Versioned({ version: 1, schema: memoryResultSchema });
|
|
111
|
+
aggregatedResult = new Versioned({
|
|
112
|
+
version: 1,
|
|
113
|
+
schema: memoryAggregatedSchema,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
async execute({
|
|
117
|
+
config,
|
|
118
|
+
client,
|
|
119
|
+
}: {
|
|
120
|
+
config: MemoryConfig;
|
|
121
|
+
client: SshTransportClient;
|
|
122
|
+
pluginId: string;
|
|
123
|
+
}): Promise<CollectorResult<MemoryResult>> {
|
|
124
|
+
// Use free -m for memory in megabytes
|
|
125
|
+
const freeResult = await client.exec("free -m");
|
|
126
|
+
const parsed = this.parseFreeOutput(freeResult.stdout);
|
|
127
|
+
|
|
128
|
+
const result: MemoryResult = {
|
|
129
|
+
totalMb: parsed.totalMb,
|
|
130
|
+
usedMb: parsed.usedMb,
|
|
131
|
+
freeMb: parsed.freeMb,
|
|
132
|
+
usedPercent: parsed.usedPercent,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (config.includeSwap && parsed.swapTotalMb > 0) {
|
|
136
|
+
result.swapTotalMb = parsed.swapTotalMb;
|
|
137
|
+
result.swapUsedMb = parsed.swapUsedMb;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { result };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
aggregateResult(
|
|
144
|
+
runs: HealthCheckRunForAggregation<MemoryResult>[]
|
|
145
|
+
): MemoryAggregatedResult {
|
|
146
|
+
const usedPercents = runs
|
|
147
|
+
.map((r) => r.metadata?.usedPercent)
|
|
148
|
+
.filter((v): v is number => typeof v === "number");
|
|
149
|
+
|
|
150
|
+
const usedMbs = runs
|
|
151
|
+
.map((r) => r.metadata?.usedMb)
|
|
152
|
+
.filter((v): v is number => typeof v === "number");
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
avgUsedPercent: usedPercents.length > 0 ? this.avg(usedPercents) : 0,
|
|
156
|
+
maxUsedPercent: usedPercents.length > 0 ? Math.max(...usedPercents) : 0,
|
|
157
|
+
avgUsedMb: usedMbs.length > 0 ? this.avg(usedMbs) : 0,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// PARSING HELPERS
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
private parseFreeOutput(output: string): {
|
|
166
|
+
totalMb: number;
|
|
167
|
+
usedMb: number;
|
|
168
|
+
freeMb: number;
|
|
169
|
+
usedPercent: number;
|
|
170
|
+
swapTotalMb: number;
|
|
171
|
+
swapUsedMb: number;
|
|
172
|
+
} {
|
|
173
|
+
// Format:
|
|
174
|
+
// total used free shared buff/cache available
|
|
175
|
+
// Mem: 15896 5234 1234 123 9428 10234
|
|
176
|
+
// Swap: 4096 512 3584
|
|
177
|
+
|
|
178
|
+
const lines = output.trim().split("\n");
|
|
179
|
+
const memLine = lines.find((l) => l.startsWith("Mem:"));
|
|
180
|
+
const swapLine = lines.find((l) => l.startsWith("Swap:"));
|
|
181
|
+
|
|
182
|
+
const memParts = memLine?.split(/\s+/).map(Number) ?? [];
|
|
183
|
+
const swapParts = swapLine?.split(/\s+/).map(Number) ?? [];
|
|
184
|
+
|
|
185
|
+
const totalMb = memParts[1] || 0;
|
|
186
|
+
const usedMb = memParts[2] || 0;
|
|
187
|
+
const freeMb = memParts[3] || 0;
|
|
188
|
+
const usedPercent =
|
|
189
|
+
totalMb > 0 ? Math.round((usedMb / totalMb) * 100 * 10) / 10 : 0;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
totalMb,
|
|
193
|
+
usedMb,
|
|
194
|
+
freeMb,
|
|
195
|
+
usedPercent,
|
|
196
|
+
swapTotalMb: swapParts[1] || 0,
|
|
197
|
+
swapUsedMb: swapParts[2] || 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private avg(nums: number[]): number {
|
|
202
|
+
return (
|
|
203
|
+
Math.round((nums.reduce((a, b) => a + b, 0) / nums.length) * 10) / 10
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
2
|
+
import { CpuCollector, MemoryCollector, DiskCollector } from "./collectors";
|
|
3
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
4
|
+
|
|
5
|
+
export default createBackendPlugin({
|
|
6
|
+
metadata: pluginMetadata,
|
|
7
|
+
register(env) {
|
|
8
|
+
env.registerInit({
|
|
9
|
+
deps: {
|
|
10
|
+
collectorRegistry: coreServices.collectorRegistry,
|
|
11
|
+
logger: coreServices.logger,
|
|
12
|
+
},
|
|
13
|
+
init: async ({ collectorRegistry, logger }) => {
|
|
14
|
+
logger.debug("🔌 Registering hardware collectors...");
|
|
15
|
+
|
|
16
|
+
// Register all hardware collectors
|
|
17
|
+
// Owner plugin metadata is auto-injected via scoped factory
|
|
18
|
+
collectorRegistry.register(new CpuCollector());
|
|
19
|
+
collectorRegistry.register(new MemoryCollector());
|
|
20
|
+
collectorRegistry.register(new DiskCollector());
|
|
21
|
+
|
|
22
|
+
logger.info("✅ Hardware collectors registered (CPU, Memory, Disk)");
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
});
|