@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 +30 -0
- package/package.json +6 -6
- package/src/ping-collector.test.ts +3 -2
- package/src/ping-collector.ts +36 -30
- package/src/strategy.test.ts +21 -21
- package/src/strategy.ts +63 -52
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.
|
|
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.
|
|
13
|
-
"@checkstack/common": "0.6.
|
|
14
|
-
"@checkstack/healthcheck-common": "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.
|
|
20
|
-
"@checkstack/scripts": "0.1.
|
|
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("
|
|
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
|
-
|
|
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);
|
package/src/ping-collector.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
149
|
-
|
|
157
|
+
mergeResult(
|
|
158
|
+
existing: PingAggregatedResult | undefined,
|
|
159
|
+
run: HealthCheckRunForAggregation<PingResult>,
|
|
150
160
|
): PingAggregatedResult {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
const metadata = run.metadata;
|
|
162
|
+
|
|
163
|
+
const lossState = mergeAverage(
|
|
164
|
+
existing?._packetLoss as AverageState | undefined,
|
|
165
|
+
metadata?.packetLoss,
|
|
166
|
+
);
|
|
154
167
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
const latencyState = mergeAverage(
|
|
169
|
+
existing?._latency as AverageState | undefined,
|
|
170
|
+
metadata?.avgLatency,
|
|
171
|
+
);
|
|
158
172
|
|
|
159
173
|
return {
|
|
160
|
-
avgPacketLoss:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
162
|
-
|
|
178
|
+
mergeResult(
|
|
179
|
+
existing: PingAggregatedResult | undefined,
|
|
180
|
+
run: HealthCheckRunForAggregation<PingResult>,
|
|
163
181
|
): PingAggregatedResult {
|
|
164
|
-
const
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
const latencyState = mergeAverage(
|
|
190
|
+
existing?._latency as AverageState | undefined,
|
|
191
|
+
metadata?.avgLatency,
|
|
192
|
+
);
|
|
195
193
|
|
|
196
|
-
const
|
|
194
|
+
const maxLatencyState = mergeMinMax(
|
|
195
|
+
existing?._maxLatency as MinMaxState | undefined,
|
|
196
|
+
metadata?.maxLatency,
|
|
197
|
+
);
|
|
197
198
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const errorState = mergeCounter(
|
|
200
|
+
existing?._errors as CounterState | undefined,
|
|
201
|
+
metadata?.error !== undefined,
|
|
202
|
+
);
|
|
201
203
|
|
|
202
|
-
return {
|
|
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;
|