@checkstack/healthcheck-ping-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,35 @@
1
1
  # @checkstack/healthcheck-ping-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
+
3
33
  ## 0.1.13
4
34
 
5
35
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-ping-backend",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,14 +9,14 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.1",
13
- "@checkstack/common": "0.6.0",
14
- "@checkstack/healthcheck-common": "0.7.0"
12
+ "@checkstack/backend-api": "0.5.2",
13
+ "@checkstack/common": "0.6.1",
14
+ "@checkstack/healthcheck-common": "0.8.1"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/bun": "^1.0.0",
18
18
  "typescript": "^5.0.0",
19
- "@checkstack/tsconfig": "0.0.2",
20
- "@checkstack/scripts": "0.1.0"
19
+ "@checkstack/tsconfig": "0.0.3",
20
+ "@checkstack/scripts": "0.1.1"
21
21
  }
22
22
  }
@@ -82,7 +82,7 @@ describe("PingCollector", () => {
82
82
  });
83
83
  });
84
84
 
85
- describe("aggregateResult", () => {
85
+ describe("mergeResult", () => {
86
86
  it("should calculate average packet loss and latency", () => {
87
87
  const collector = new PingCollector();
88
88
  const runs = [
@@ -114,7 +114,8 @@ describe("PingCollector", () => {
114
114
  },
115
115
  ];
116
116
 
117
- const aggregated = collector.aggregateResult(runs);
117
+ let aggregated = collector.mergeResult(undefined, runs[0]);
118
+ aggregated = collector.mergeResult(aggregated, runs[1]);
118
119
 
119
120
  expect(aggregated.avgPacketLoss).toBe(5);
120
121
  expect(aggregated.avgLatency).toBe(15);
@@ -4,6 +4,9 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ averageStateSchema,
9
+ type AverageState,
7
10
  } from "@checkstack/backend-api";
8
11
  import {
9
12
  healthResultNumber,
@@ -71,7 +74,7 @@ const pingResultSchema = healthResultSchema({
71
74
 
72
75
  export type PingResult = z.infer<typeof pingResultSchema>;
73
76
 
74
- const pingAggregatedSchema = healthResultSchema({
77
+ const pingAggregatedDisplaySchema = healthResultSchema({
75
78
  avgPacketLoss: healthResultNumber({
76
79
  "x-chart-type": "gauge",
77
80
  "x-chart-label": "Avg Packet Loss",
@@ -84,6 +87,15 @@ const pingAggregatedSchema = healthResultSchema({
84
87
  }),
85
88
  });
86
89
 
90
+ const pingAggregatedInternalSchema = z.object({
91
+ _packetLoss: averageStateSchema.optional(),
92
+ _latency: averageStateSchema.optional(),
93
+ });
94
+
95
+ const pingAggregatedSchema = pingAggregatedDisplaySchema.merge(
96
+ pingAggregatedInternalSchema,
97
+ );
98
+
87
99
  export type PingAggregatedResult = z.infer<typeof pingAggregatedSchema>;
88
100
 
89
101
  // ============================================================================
@@ -94,15 +106,12 @@ export type PingAggregatedResult = z.infer<typeof pingAggregatedSchema>;
94
106
  * Built-in Ping collector.
95
107
  * Performs ICMP ping and checks latency.
96
108
  */
97
- export class PingCollector
98
- implements
99
- CollectorStrategy<
100
- PingTransportClient,
101
- PingConfig,
102
- PingResult,
103
- PingAggregatedResult
104
- >
105
- {
109
+ export class PingCollector implements CollectorStrategy<
110
+ PingTransportClient,
111
+ PingConfig,
112
+ PingResult,
113
+ PingAggregatedResult
114
+ > {
106
115
  id = "ping";
107
116
  displayName = "ICMP Ping";
108
117
  description = "Ping a host and check latency";
@@ -145,30 +154,27 @@ export class PingCollector
145
154
  };
146
155
  }
147
156
 
148
- aggregateResult(
149
- runs: HealthCheckRunForAggregation<PingResult>[]
157
+ mergeResult(
158
+ existing: PingAggregatedResult | undefined,
159
+ run: HealthCheckRunForAggregation<PingResult>,
150
160
  ): PingAggregatedResult {
151
- const losses = runs
152
- .map((r) => r.metadata?.packetLoss)
153
- .filter((v): v is number => typeof v === "number");
161
+ const metadata = run.metadata;
162
+
163
+ const lossState = mergeAverage(
164
+ existing?._packetLoss as AverageState | undefined,
165
+ metadata?.packetLoss,
166
+ );
154
167
 
155
- const latencies = runs
156
- .map((r) => r.metadata?.avgLatency)
157
- .filter((v): v is number => typeof v === "number");
168
+ const latencyState = mergeAverage(
169
+ existing?._latency as AverageState | undefined,
170
+ metadata?.avgLatency,
171
+ );
158
172
 
159
173
  return {
160
- avgPacketLoss:
161
- losses.length > 0
162
- ? Math.round(
163
- (losses.reduce((a, b) => a + b, 0) / losses.length) * 10
164
- ) / 10
165
- : 0,
166
- avgLatency:
167
- latencies.length > 0
168
- ? Math.round(
169
- (latencies.reduce((a, b) => a + b, 0) / latencies.length) * 10
170
- ) / 10
171
- : 0,
174
+ avgPacketLoss: Math.round(lossState.avg * 10) / 10,
175
+ avgLatency: Math.round(latencyState.avg * 10) / 10,
176
+ _packetLoss: lossState,
177
+ _latency: latencyState,
172
178
  };
173
179
  }
174
180
  }
@@ -14,8 +14,8 @@ const mockSpawn = mock(() => ({
14
14
 
15
15
  --- 8.8.8.8 ping statistics ---
16
16
  3 packets transmitted, 3 packets received, 0.0% packet loss
17
- round-trip min/avg/max/stddev = 10.123/11.456/12.456/0.957 ms`
18
- )
17
+ round-trip min/avg/max/stddev = 10.123/11.456/12.456/0.957 ms`,
18
+ ),
19
19
  );
20
20
  controller.close();
21
21
  },
@@ -83,8 +83,8 @@ describe("PingHealthCheckStrategy", () => {
83
83
  `PING 10.0.0.1 (10.0.0.1): 56 data bytes
84
84
 
85
85
  --- 10.0.0.1 ping statistics ---
86
- 3 packets transmitted, 0 packets received, 100.0% packet loss`
87
- )
86
+ 3 packets transmitted, 0 packets received, 100.0% packet loss`,
87
+ ),
88
88
  );
89
89
  controller.close();
90
90
  },
@@ -139,7 +139,7 @@ describe("PingHealthCheckStrategy", () => {
139
139
  });
140
140
  });
141
141
 
142
- describe("aggregateResult", () => {
142
+ describe("mergeResult", () => {
143
143
  it("should calculate averages correctly", () => {
144
144
  const runs = [
145
145
  {
@@ -172,8 +172,10 @@ describe("PingHealthCheckStrategy", () => {
172
172
  },
173
173
  ];
174
174
 
175
- const aggregated = strategy.aggregateResult(runs);
175
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
176
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
176
177
 
178
+ // (0 + 33) / 2 = 16.5
177
179
  expect(aggregated.avgPacketLoss).toBeCloseTo(16.5, 1);
178
180
  expect(aggregated.avgLatency).toBeCloseTo(15, 1);
179
181
  expect(aggregated.maxLatency).toBe(25);
@@ -181,23 +183,21 @@ describe("PingHealthCheckStrategy", () => {
181
183
  });
182
184
 
183
185
  it("should count errors", () => {
184
- const runs = [
185
- {
186
- id: "1",
187
- status: "unhealthy" as const,
188
- latencyMs: 0,
189
- checkId: "c1",
190
- timestamp: new Date(),
191
- metadata: {
192
- packetsSent: 3,
193
- packetsReceived: 0,
194
- packetLoss: 100,
195
- error: "Timeout",
196
- },
186
+ const run = {
187
+ id: "1",
188
+ status: "unhealthy" as const,
189
+ latencyMs: 0,
190
+ checkId: "c1",
191
+ timestamp: new Date(),
192
+ metadata: {
193
+ packetsSent: 3,
194
+ packetsReceived: 0,
195
+ packetLoss: 100,
196
+ error: "Timeout",
197
197
  },
198
- ];
198
+ };
199
199
 
200
- const aggregated = strategy.aggregateResult(runs);
200
+ const aggregated = strategy.mergeResult(undefined, run);
201
201
 
202
202
  expect(aggregated.errorCount).toBe(1);
203
203
  });
package/src/strategy.ts CHANGED
@@ -4,6 +4,15 @@ import {
4
4
  Versioned,
5
5
  z,
6
6
  type ConnectedClient,
7
+ mergeAverage,
8
+ averageStateSchema,
9
+ mergeCounter,
10
+ counterStateSchema,
11
+ mergeMinMax,
12
+ minMaxStateSchema,
13
+ type AverageState,
14
+ type CounterState,
15
+ type MinMaxState,
7
16
  } from "@checkstack/backend-api";
8
17
  import {
9
18
  healthResultNumber,
@@ -84,7 +93,7 @@ type PingResult = z.infer<typeof pingResultSchema>;
84
93
  /**
85
94
  * Aggregated metadata for buckets.
86
95
  */
87
- const pingAggregatedSchema = healthResultSchema({
96
+ const pingAggregatedDisplaySchema = healthResultSchema({
88
97
  avgPacketLoss: healthResultNumber({
89
98
  "x-chart-type": "gauge",
90
99
  "x-chart-label": "Avg Packet Loss",
@@ -106,21 +115,29 @@ const pingAggregatedSchema = healthResultSchema({
106
115
  }),
107
116
  });
108
117
 
118
+ const pingAggregatedInternalSchema = z.object({
119
+ _packetLoss: averageStateSchema.optional(),
120
+ _latency: averageStateSchema.optional(),
121
+ _maxLatency: minMaxStateSchema.optional(),
122
+ _errors: counterStateSchema.optional(),
123
+ });
124
+
125
+ const pingAggregatedSchema = pingAggregatedDisplaySchema.merge(
126
+ pingAggregatedInternalSchema,
127
+ );
128
+
109
129
  type PingAggregatedResult = z.infer<typeof pingAggregatedSchema>;
110
130
 
111
131
  // ============================================================================
112
132
  // STRATEGY
113
133
  // ============================================================================
114
134
 
115
- export class PingHealthCheckStrategy
116
- implements
117
- HealthCheckStrategy<
118
- PingConfig,
119
- PingTransportClient,
120
- PingResult,
121
- PingAggregatedResult
122
- >
123
- {
135
+ export class PingHealthCheckStrategy implements HealthCheckStrategy<
136
+ PingConfig,
137
+ PingTransportClient,
138
+ PingResult,
139
+ PingAggregatedResult
140
+ > {
124
141
  id = "ping";
125
142
  displayName = "Ping Health Check";
126
143
  description = "ICMP ping check for network reachability and latency";
@@ -158,52 +175,46 @@ export class PingHealthCheckStrategy
158
175
  schema: pingAggregatedSchema,
159
176
  });
160
177
 
161
- aggregateResult(
162
- runs: HealthCheckRunForAggregation<PingResult>[]
178
+ mergeResult(
179
+ existing: PingAggregatedResult | undefined,
180
+ run: HealthCheckRunForAggregation<PingResult>,
163
181
  ): PingAggregatedResult {
164
- const validRuns = runs.filter((r) => r.metadata);
165
-
166
- if (validRuns.length === 0) {
167
- return { avgPacketLoss: 0, avgLatency: 0, maxLatency: 0, errorCount: 0 };
168
- }
169
-
170
- const packetLosses = validRuns
171
- .map((r) => r.metadata?.packetLoss)
172
- .filter((l): l is number => typeof l === "number");
173
-
174
- const avgPacketLoss =
175
- packetLosses.length > 0
176
- ? Math.round(
177
- (packetLosses.reduce((a, b) => a + b, 0) / packetLosses.length) * 10
178
- ) / 10
179
- : 0;
182
+ const metadata = run.metadata;
180
183
 
181
- const latencies = validRuns
182
- .map((r) => r.metadata?.avgLatency)
183
- .filter((l): l is number => typeof l === "number");
184
-
185
- const avgLatency =
186
- latencies.length > 0
187
- ? Math.round(
188
- (latencies.reduce((a, b) => a + b, 0) / latencies.length) * 10
189
- ) / 10
190
- : 0;
184
+ const packetLossState = mergeAverage(
185
+ existing?._packetLoss as AverageState | undefined,
186
+ metadata?.packetLoss,
187
+ );
191
188
 
192
- const maxLatencies = validRuns
193
- .map((r) => r.metadata?.maxLatency)
194
- .filter((l): l is number => typeof l === "number");
189
+ const latencyState = mergeAverage(
190
+ existing?._latency as AverageState | undefined,
191
+ metadata?.avgLatency,
192
+ );
195
193
 
196
- const maxLatency = maxLatencies.length > 0 ? Math.max(...maxLatencies) : 0;
194
+ const maxLatencyState = mergeMinMax(
195
+ existing?._maxLatency as MinMaxState | undefined,
196
+ metadata?.maxLatency,
197
+ );
197
198
 
198
- const errorCount = validRuns.filter(
199
- (r) => r.metadata?.error !== undefined
200
- ).length;
199
+ const errorState = mergeCounter(
200
+ existing?._errors as CounterState | undefined,
201
+ metadata?.error !== undefined,
202
+ );
201
203
 
202
- return { avgPacketLoss, avgLatency, maxLatency, errorCount };
204
+ return {
205
+ avgPacketLoss: Math.round(packetLossState.avg * 10) / 10,
206
+ avgLatency: Math.round(latencyState.avg * 10) / 10,
207
+ maxLatency: maxLatencyState.max,
208
+ errorCount: errorState.count,
209
+ _packetLoss: packetLossState,
210
+ _latency: latencyState,
211
+ _maxLatency: maxLatencyState,
212
+ _errors: errorState,
213
+ };
203
214
  }
204
215
 
205
216
  async createClient(
206
- config: PingConfig
217
+ config: PingConfig,
207
218
  ): Promise<ConnectedClient<PingTransportClient>> {
208
219
  const validatedConfig = this.config.validate(config);
209
220
 
@@ -212,7 +223,7 @@ export class PingHealthCheckStrategy
212
223
  return this.runPing(
213
224
  request.host,
214
225
  request.count,
215
- request.timeout ?? validatedConfig.timeout
226
+ request.timeout ?? validatedConfig.timeout,
216
227
  );
217
228
  },
218
229
  };
@@ -228,7 +239,7 @@ export class PingHealthCheckStrategy
228
239
  private async runPing(
229
240
  host: string,
230
241
  count: number,
231
- timeout: number
242
+ timeout: number,
232
243
  ): Promise<PingResultType> {
233
244
  const isMac = process.platform === "darwin";
234
245
  const args = isMac
@@ -260,11 +271,11 @@ export class PingHealthCheckStrategy
260
271
  private parsePingOutput(
261
272
  output: string,
262
273
  expectedCount: number,
263
- _exitCode: number
274
+ _exitCode: number,
264
275
  ): PingResultType {
265
276
  // Parse packet statistics
266
277
  const statsMatch = output.match(
267
- /(\d+) packets transmitted, (\d+) (?:packets )?received/
278
+ /(\d+) packets transmitted, (\d+) (?:packets )?received/,
268
279
  );
269
280
  const packetsSent = statsMatch
270
281
  ? Number.parseInt(statsMatch[1], 10)
@@ -279,7 +290,7 @@ export class PingHealthCheckStrategy
279
290
  // macOS: round-trip min/avg/max/stddev = 0.043/0.059/0.082/0.016 ms
280
291
  // Linux: rtt min/avg/max/mdev = 0.039/0.049/0.064/0.009 ms
281
292
  const latencyMatch = output.match(
282
- /(?:round-trip|rtt) min\/avg\/max\/(?:stddev|mdev) = ([\d.]+)\/([\d.]+)\/([\d.]+)/
293
+ /(?:round-trip|rtt) min\/avg\/max\/(?:stddev|mdev) = ([\d.]+)\/([\d.]+)\/([\d.]+)/,
283
294
  );
284
295
 
285
296
  let minLatency: number | undefined;