@checkstack/collector-hardware-backend 0.1.13 → 0.1.14

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # @checkstack/collector-hardware-backend
2
2
 
3
+ ## 0.1.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
8
+
9
+ ### Breaking Changes (Internal)
10
+
11
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
12
+
13
+ ### New Features
14
+
15
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
16
+ - `mergeCounter()` - track occurrences
17
+ - `mergeAverage()` - track sum/count, compute avg
18
+ - `mergeRate()` - track success/total, compute %
19
+ - `mergeMinMax()` - track min/max values
20
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
21
+
22
+ ### Improvements
23
+
24
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
25
+ - Prepares for real-time hourly aggregation without batch accumulation
26
+
27
+ - Updated dependencies [f676e11]
28
+ - Updated dependencies [48c2080]
29
+ - @checkstack/common@0.6.2
30
+ - @checkstack/backend-api@0.6.0
31
+ - @checkstack/healthcheck-common@0.8.2
32
+ - @checkstack/healthcheck-ssh-common@0.1.8
33
+
3
34
  ## 0.1.13
4
35
 
5
36
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/collector-hardware-backend",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -10,15 +10,15 @@
10
10
  "test": "bun test"
11
11
  },
12
12
  "dependencies": {
13
- "@checkstack/backend-api": "0.5.1",
14
- "@checkstack/common": "0.6.0",
15
- "@checkstack/healthcheck-common": "0.7.0",
16
- "@checkstack/healthcheck-ssh-common": "0.1.6"
13
+ "@checkstack/backend-api": "0.5.2",
14
+ "@checkstack/common": "0.6.1",
15
+ "@checkstack/healthcheck-common": "0.8.1",
16
+ "@checkstack/healthcheck-ssh-common": "0.1.7"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/bun": "^1.0.0",
20
20
  "typescript": "^5.0.0",
21
- "@checkstack/tsconfig": "0.0.2",
22
- "@checkstack/scripts": "0.1.0"
21
+ "@checkstack/tsconfig": "0.0.3",
22
+ "@checkstack/scripts": "0.1.1"
23
23
  }
24
24
  }
@@ -9,7 +9,7 @@ describe("CpuCollector", () => {
9
9
  stat2?: { stdout: string };
10
10
  loadavg?: { stdout: string };
11
11
  nproc?: { stdout: string };
12
- } = {}
12
+ } = {},
13
13
  ): SshTransportClient => {
14
14
  let callCount = 0;
15
15
  return {
@@ -96,7 +96,7 @@ describe("CpuCollector", () => {
96
96
  });
97
97
  });
98
98
 
99
- describe("aggregateResult", () => {
99
+ describe("mergeResult", () => {
100
100
  it("should calculate average and max CPU usage", () => {
101
101
  const collector = new CpuCollector();
102
102
  const runs = [
@@ -118,10 +118,12 @@ describe("CpuCollector", () => {
118
118
  },
119
119
  ];
120
120
 
121
- const aggregated = collector.aggregateResult(runs);
121
+ let aggregated = collector.mergeResult(undefined, runs[0]);
122
+ aggregated = collector.mergeResult(aggregated, runs[1]);
122
123
 
123
124
  expect(aggregated.avgUsagePercent).toBe(50);
124
125
  expect(aggregated.maxUsagePercent).toBe(75);
126
+ // (1.0 + 2.0) / 2 = 1.5
125
127
  expect(aggregated.avgLoadAvg1m).toBe(1.5);
126
128
  });
127
129
  });
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ mergeMinMax,
9
+ averageStateSchema,
10
+ minMaxStateSchema,
11
+ type AverageState,
12
+ type MinMaxState,
7
13
  } from "@checkstack/backend-api";
8
14
  import { healthResultNumber } from "@checkstack/healthcheck-common";
9
15
  import {
@@ -58,7 +64,7 @@ const cpuResultSchema = z.object({
58
64
 
59
65
  export type CpuResult = z.infer<typeof cpuResultSchema>;
60
66
 
61
- const cpuAggregatedSchema = z.object({
67
+ const cpuAggregatedDisplaySchema = z.object({
62
68
  avgUsagePercent: healthResultNumber({
63
69
  "x-chart-type": "line",
64
70
  "x-chart-label": "Avg CPU Usage",
@@ -75,21 +81,28 @@ const cpuAggregatedSchema = z.object({
75
81
  }),
76
82
  });
77
83
 
84
+ const cpuAggregatedInternalSchema = z.object({
85
+ _usage: averageStateSchema.optional(),
86
+ _maxUsage: minMaxStateSchema.optional(),
87
+ _load: averageStateSchema.optional(),
88
+ });
89
+
90
+ const cpuAggregatedSchema = cpuAggregatedDisplaySchema.merge(
91
+ cpuAggregatedInternalSchema,
92
+ );
93
+
78
94
  export type CpuAggregatedResult = z.infer<typeof cpuAggregatedSchema>;
79
95
 
80
96
  // ============================================================================
81
97
  // CPU COLLECTOR
82
98
  // ============================================================================
83
99
 
84
- export class CpuCollector
85
- implements
86
- CollectorStrategy<
87
- SshTransportClient,
88
- CpuConfig,
89
- CpuResult,
90
- CpuAggregatedResult
91
- >
92
- {
100
+ export class CpuCollector implements CollectorStrategy<
101
+ SshTransportClient,
102
+ CpuConfig,
103
+ CpuResult,
104
+ CpuAggregatedResult
105
+ > {
93
106
  id = "cpu";
94
107
  displayName = "CPU Metrics";
95
108
  description = "Collects CPU usage, load averages, and core count via SSH";
@@ -135,21 +148,34 @@ export class CpuCollector
135
148
  return { result };
136
149
  }
137
150
 
138
- aggregateResult(
139
- runs: HealthCheckRunForAggregation<CpuResult>[]
151
+ mergeResult(
152
+ existing: CpuAggregatedResult | undefined,
153
+ run: HealthCheckRunForAggregation<CpuResult>,
140
154
  ): CpuAggregatedResult {
141
- const usages = runs
142
- .map((r) => r.metadata?.usagePercent)
143
- .filter((v): v is number => typeof v === "number");
155
+ const metadata = run.metadata;
156
+
157
+ const usageState = mergeAverage(
158
+ existing?._usage as AverageState | undefined,
159
+ metadata?.usagePercent,
160
+ );
161
+
162
+ const maxUsageState = mergeMinMax(
163
+ existing?._maxUsage as MinMaxState | undefined,
164
+ metadata?.usagePercent,
165
+ );
144
166
 
145
- const loads = runs
146
- .map((r) => r.metadata?.loadAvg1m)
147
- .filter((v): v is number => typeof v === "number");
167
+ const loadState = mergeAverage(
168
+ existing?._load as AverageState | undefined,
169
+ metadata?.loadAvg1m,
170
+ );
148
171
 
149
172
  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,
173
+ avgUsagePercent: usageState.avg,
174
+ maxUsagePercent: maxUsageState.max,
175
+ avgLoadAvg1m: loadState.avg,
176
+ _usage: usageState,
177
+ _maxUsage: maxUsageState,
178
+ _load: loadState,
153
179
  };
154
180
  }
155
181
 
@@ -66,7 +66,7 @@ describe("DiskCollector", () => {
66
66
  });
67
67
  });
68
68
 
69
- describe("aggregateResult", () => {
69
+ describe("mergeResult", () => {
70
70
  it("should calculate average and max disk usage", () => {
71
71
  const collector = new DiskCollector();
72
72
  const runs = [
@@ -102,7 +102,8 @@ describe("DiskCollector", () => {
102
102
  },
103
103
  ];
104
104
 
105
- const aggregated = collector.aggregateResult(runs);
105
+ let aggregated = collector.mergeResult(undefined, runs[0]);
106
+ aggregated = collector.mergeResult(aggregated, runs[1]);
106
107
 
107
108
  expect(aggregated.avgUsedPercent).toBe(40);
108
109
  expect(aggregated.maxUsedPercent).toBe(50);
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ mergeMinMax,
9
+ averageStateSchema,
10
+ minMaxStateSchema,
11
+ type AverageState,
12
+ type MinMaxState,
7
13
  } from "@checkstack/backend-api";
8
14
  import {
9
15
  healthResultNumber,
@@ -64,7 +70,7 @@ const diskResultSchema = z.object({
64
70
 
65
71
  export type DiskResult = z.infer<typeof diskResultSchema>;
66
72
 
67
- const diskAggregatedSchema = z.object({
73
+ const diskAggregatedDisplaySchema = z.object({
68
74
  avgUsedPercent: healthResultNumber({
69
75
  "x-chart-type": "line",
70
76
  "x-chart-label": "Avg Disk Usage",
@@ -77,21 +83,27 @@ const diskAggregatedSchema = z.object({
77
83
  }),
78
84
  });
79
85
 
86
+ const diskAggregatedInternalSchema = z.object({
87
+ _usage: averageStateSchema.optional(),
88
+ _maxUsage: minMaxStateSchema.optional(),
89
+ });
90
+
91
+ const diskAggregatedSchema = diskAggregatedDisplaySchema.merge(
92
+ diskAggregatedInternalSchema,
93
+ );
94
+
80
95
  export type DiskAggregatedResult = z.infer<typeof diskAggregatedSchema>;
81
96
 
82
97
  // ============================================================================
83
98
  // DISK COLLECTOR
84
99
  // ============================================================================
85
100
 
86
- export class DiskCollector
87
- implements
88
- CollectorStrategy<
89
- SshTransportClient,
90
- DiskConfig,
91
- DiskResult,
92
- DiskAggregatedResult
93
- >
94
- {
101
+ export class DiskCollector implements CollectorStrategy<
102
+ SshTransportClient,
103
+ DiskConfig,
104
+ DiskResult,
105
+ DiskAggregatedResult
106
+ > {
95
107
  id = "disk";
96
108
  displayName = "Disk Metrics";
97
109
  description = "Collects disk usage for a specific mount point via SSH";
@@ -120,16 +132,27 @@ export class DiskCollector
120
132
  return { result: parsed };
121
133
  }
122
134
 
123
- aggregateResult(
124
- runs: HealthCheckRunForAggregation<DiskResult>[]
135
+ mergeResult(
136
+ existing: DiskAggregatedResult | undefined,
137
+ run: HealthCheckRunForAggregation<DiskResult>,
125
138
  ): DiskAggregatedResult {
126
- const usedPercents = runs
127
- .map((r) => r.metadata?.usedPercent)
128
- .filter((v): v is number => typeof v === "number");
139
+ const metadata = run.metadata;
140
+
141
+ const usageState = mergeAverage(
142
+ existing?._usage as AverageState | undefined,
143
+ metadata?.usedPercent,
144
+ );
145
+
146
+ const maxUsageState = mergeMinMax(
147
+ existing?._maxUsage as MinMaxState | undefined,
148
+ metadata?.usedPercent,
149
+ );
129
150
 
130
151
  return {
131
- avgUsedPercent: usedPercents.length > 0 ? this.avg(usedPercents) : 0,
132
- maxUsedPercent: usedPercents.length > 0 ? Math.max(...usedPercents) : 0,
152
+ avgUsedPercent: usageState.avg,
153
+ maxUsedPercent: maxUsageState.max,
154
+ _usage: usageState,
155
+ _maxUsage: maxUsageState,
133
156
  };
134
157
  }
135
158
 
@@ -76,7 +76,7 @@ Swap: 4096 512 3584`
76
76
  });
77
77
  });
78
78
 
79
- describe("aggregateResult", () => {
79
+ describe("mergeResult", () => {
80
80
  it("should calculate average and max memory usage", () => {
81
81
  const collector = new MemoryCollector();
82
82
  const runs = [
@@ -108,7 +108,8 @@ Swap: 4096 512 3584`
108
108
  },
109
109
  ];
110
110
 
111
- const aggregated = collector.aggregateResult(runs);
111
+ let aggregated = collector.mergeResult(undefined, runs[0]);
112
+ aggregated = collector.mergeResult(aggregated, runs[1]);
112
113
 
113
114
  expect(aggregated.avgUsedPercent).toBe(50);
114
115
  expect(aggregated.maxUsedPercent).toBe(75);
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ mergeMinMax,
9
+ averageStateSchema,
10
+ minMaxStateSchema,
11
+ type AverageState,
12
+ type MinMaxState,
7
13
  } from "@checkstack/backend-api";
8
14
  import { healthResultNumber } from "@checkstack/healthcheck-common";
9
15
  import {
@@ -67,7 +73,7 @@ const memoryResultSchema = z.object({
67
73
 
68
74
  export type MemoryResult = z.infer<typeof memoryResultSchema>;
69
75
 
70
- const memoryAggregatedSchema = z.object({
76
+ const memoryAggregatedDisplaySchema = z.object({
71
77
  avgUsedPercent: healthResultNumber({
72
78
  "x-chart-type": "line",
73
79
  "x-chart-label": "Avg Memory Usage",
@@ -85,21 +91,28 @@ const memoryAggregatedSchema = z.object({
85
91
  }),
86
92
  });
87
93
 
94
+ const memoryAggregatedInternalSchema = z.object({
95
+ _usedPercent: averageStateSchema.optional(),
96
+ _maxUsedPercent: minMaxStateSchema.optional(),
97
+ _usedMb: averageStateSchema.optional(),
98
+ });
99
+
100
+ const memoryAggregatedSchema = memoryAggregatedDisplaySchema.merge(
101
+ memoryAggregatedInternalSchema,
102
+ );
103
+
88
104
  export type MemoryAggregatedResult = z.infer<typeof memoryAggregatedSchema>;
89
105
 
90
106
  // ============================================================================
91
107
  // MEMORY COLLECTOR
92
108
  // ============================================================================
93
109
 
94
- export class MemoryCollector
95
- implements
96
- CollectorStrategy<
97
- SshTransportClient,
98
- MemoryConfig,
99
- MemoryResult,
100
- MemoryAggregatedResult
101
- >
102
- {
110
+ export class MemoryCollector implements CollectorStrategy<
111
+ SshTransportClient,
112
+ MemoryConfig,
113
+ MemoryResult,
114
+ MemoryAggregatedResult
115
+ > {
103
116
  id = "memory";
104
117
  displayName = "Memory Metrics";
105
118
  description = "Collects RAM and swap usage via SSH";
@@ -140,21 +153,34 @@ export class MemoryCollector
140
153
  return { result };
141
154
  }
142
155
 
143
- aggregateResult(
144
- runs: HealthCheckRunForAggregation<MemoryResult>[]
156
+ mergeResult(
157
+ existing: MemoryAggregatedResult | undefined,
158
+ run: HealthCheckRunForAggregation<MemoryResult>,
145
159
  ): MemoryAggregatedResult {
146
- const usedPercents = runs
147
- .map((r) => r.metadata?.usedPercent)
148
- .filter((v): v is number => typeof v === "number");
160
+ const metadata = run.metadata;
161
+
162
+ const usedPercentState = mergeAverage(
163
+ existing?._usedPercent as AverageState | undefined,
164
+ metadata?.usedPercent,
165
+ );
166
+
167
+ const maxUsedPercentState = mergeMinMax(
168
+ existing?._maxUsedPercent as MinMaxState | undefined,
169
+ metadata?.usedPercent,
170
+ );
149
171
 
150
- const usedMbs = runs
151
- .map((r) => r.metadata?.usedMb)
152
- .filter((v): v is number => typeof v === "number");
172
+ const usedMbState = mergeAverage(
173
+ existing?._usedMb as AverageState | undefined,
174
+ metadata?.usedMb,
175
+ );
153
176
 
154
177
  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,
178
+ avgUsedPercent: usedPercentState.avg,
179
+ maxUsedPercent: maxUsedPercentState.max,
180
+ avgUsedMb: usedMbState.avg,
181
+ _usedPercent: usedPercentState,
182
+ _maxUsedPercent: maxUsedPercentState,
183
+ _usedMb: usedMbState,
158
184
  };
159
185
  }
160
186