@checkstack/healthcheck-tcp-backend 0.1.13 → 0.2.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 +54 -0
- package/package.json +6 -6
- package/src/banner-collector.test.ts +5 -4
- package/src/banner-collector.ts +32 -35
- package/src/strategy.test.ts +10 -8
- package/src/strategy.ts +36 -45
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @checkstack/healthcheck-tcp-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
8
|
+
|
|
9
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
10
|
+
|
|
11
|
+
**Key changes:**
|
|
12
|
+
|
|
13
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
14
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
15
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
16
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
17
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
18
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
19
|
+
|
|
20
|
+
**Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [3dd1914]
|
|
25
|
+
- @checkstack/backend-api@0.7.0
|
|
26
|
+
|
|
27
|
+
## 0.1.14
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
|
|
32
|
+
|
|
33
|
+
### Breaking Changes (Internal)
|
|
34
|
+
|
|
35
|
+
- Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
|
|
36
|
+
|
|
37
|
+
### New Features
|
|
38
|
+
|
|
39
|
+
- Added incremental aggregation utilities in `@checkstack/backend-api`:
|
|
40
|
+
- `mergeCounter()` - track occurrences
|
|
41
|
+
- `mergeAverage()` - track sum/count, compute avg
|
|
42
|
+
- `mergeRate()` - track success/total, compute %
|
|
43
|
+
- `mergeMinMax()` - track min/max values
|
|
44
|
+
- Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
|
|
45
|
+
|
|
46
|
+
### Improvements
|
|
47
|
+
|
|
48
|
+
- Enables O(1) storage overhead by maintaining incremental aggregation state
|
|
49
|
+
- Prepares for real-time hourly aggregation without batch accumulation
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [f676e11]
|
|
52
|
+
- Updated dependencies [48c2080]
|
|
53
|
+
- @checkstack/common@0.6.2
|
|
54
|
+
- @checkstack/backend-api@0.6.0
|
|
55
|
+
- @checkstack/healthcheck-common@0.8.2
|
|
56
|
+
|
|
3
57
|
## 0.1.13
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-tcp-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
}
|
|
@@ -65,7 +65,7 @@ describe("BannerCollector", () => {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
describe("
|
|
68
|
+
describe("mergeResult", () => {
|
|
69
69
|
it("should calculate average read time and banner rate", () => {
|
|
70
70
|
const collector = new BannerCollector();
|
|
71
71
|
const runs = [
|
|
@@ -87,10 +87,11 @@ describe("BannerCollector", () => {
|
|
|
87
87
|
},
|
|
88
88
|
];
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
91
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
91
92
|
|
|
92
|
-
expect(aggregated.avgReadTimeMs).toBe(75);
|
|
93
|
-
expect(aggregated.bannerRate).toBe(50);
|
|
93
|
+
expect(aggregated.avgReadTimeMs.avg).toBe(75);
|
|
94
|
+
expect(aggregated.bannerRate.rate).toBe(50);
|
|
94
95
|
});
|
|
95
96
|
});
|
|
96
97
|
|
package/src/banner-collector.ts
CHANGED
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type HealthCheckRunForAggregation,
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
|
+
mergeAverage,
|
|
8
|
+
mergeRate,
|
|
9
|
+
VersionedAggregated,
|
|
10
|
+
aggregatedAverage,
|
|
11
|
+
aggregatedRate,
|
|
12
|
+
type InferAggregatedResult,
|
|
7
13
|
} from "@checkstack/backend-api";
|
|
8
14
|
import {
|
|
9
15
|
healthResultNumber,
|
|
@@ -50,20 +56,24 @@ const bannerResultSchema = healthResultSchema({
|
|
|
50
56
|
|
|
51
57
|
export type BannerResult = z.infer<typeof bannerResultSchema>;
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
// Aggregated result fields definition
|
|
60
|
+
const bannerAggregatedFields = {
|
|
61
|
+
avgReadTimeMs: aggregatedAverage({
|
|
55
62
|
"x-chart-type": "line",
|
|
56
63
|
"x-chart-label": "Avg Read Time",
|
|
57
64
|
"x-chart-unit": "ms",
|
|
58
65
|
}),
|
|
59
|
-
bannerRate:
|
|
66
|
+
bannerRate: aggregatedRate({
|
|
60
67
|
"x-chart-type": "gauge",
|
|
61
68
|
"x-chart-label": "Banner Rate",
|
|
62
69
|
"x-chart-unit": "%",
|
|
63
70
|
}),
|
|
64
|
-
}
|
|
71
|
+
};
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
// Type inferred from field definitions
|
|
74
|
+
export type BannerAggregatedResult = InferAggregatedResult<
|
|
75
|
+
typeof bannerAggregatedFields
|
|
76
|
+
>;
|
|
67
77
|
|
|
68
78
|
// ============================================================================
|
|
69
79
|
// BANNER COLLECTOR
|
|
@@ -73,15 +83,12 @@ export type BannerAggregatedResult = z.infer<typeof bannerAggregatedSchema>;
|
|
|
73
83
|
* Built-in TCP banner collector.
|
|
74
84
|
* Reads the initial banner/greeting from a TCP server.
|
|
75
85
|
*/
|
|
76
|
-
export class BannerCollector
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
BannerAggregatedResult
|
|
83
|
-
>
|
|
84
|
-
{
|
|
86
|
+
export class BannerCollector implements CollectorStrategy<
|
|
87
|
+
TcpTransportClient,
|
|
88
|
+
BannerConfig,
|
|
89
|
+
BannerResult,
|
|
90
|
+
BannerAggregatedResult
|
|
91
|
+
> {
|
|
85
92
|
id = "banner";
|
|
86
93
|
displayName = "TCP Banner";
|
|
87
94
|
description = "Read the initial banner/greeting from the server";
|
|
@@ -92,9 +99,9 @@ export class BannerCollector
|
|
|
92
99
|
|
|
93
100
|
config = new Versioned({ version: 1, schema: bannerConfigSchema });
|
|
94
101
|
result = new Versioned({ version: 1, schema: bannerResultSchema });
|
|
95
|
-
aggregatedResult = new
|
|
102
|
+
aggregatedResult = new VersionedAggregated({
|
|
96
103
|
version: 1,
|
|
97
|
-
|
|
104
|
+
fields: bannerAggregatedFields,
|
|
98
105
|
});
|
|
99
106
|
|
|
100
107
|
async execute({
|
|
@@ -123,28 +130,18 @@ export class BannerCollector
|
|
|
123
130
|
};
|
|
124
131
|
}
|
|
125
132
|
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
mergeResult(
|
|
134
|
+
existing: BannerAggregatedResult | undefined,
|
|
135
|
+
run: HealthCheckRunForAggregation<BannerResult>,
|
|
128
136
|
): BannerAggregatedResult {
|
|
129
|
-
const
|
|
130
|
-
.map((r) => r.metadata?.readTimeMs)
|
|
131
|
-
.filter((v): v is number => typeof v === "number");
|
|
132
|
-
|
|
133
|
-
const hasBanners = runs
|
|
134
|
-
.map((r) => r.metadata?.hasBanner)
|
|
135
|
-
.filter((v): v is boolean => typeof v === "boolean");
|
|
136
|
-
|
|
137
|
-
const bannerCount = hasBanners.filter(Boolean).length;
|
|
137
|
+
const metadata = run.metadata;
|
|
138
138
|
|
|
139
139
|
return {
|
|
140
|
-
avgReadTimeMs:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
bannerRate:
|
|
145
|
-
hasBanners.length > 0
|
|
146
|
-
? Math.round((bannerCount / hasBanners.length) * 100)
|
|
147
|
-
: 0,
|
|
140
|
+
avgReadTimeMs: mergeAverage(
|
|
141
|
+
existing?.avgReadTimeMs,
|
|
142
|
+
metadata?.readTimeMs,
|
|
143
|
+
),
|
|
144
|
+
bannerRate: mergeRate(existing?.bannerRate, metadata?.hasBanner),
|
|
148
145
|
};
|
|
149
146
|
}
|
|
150
147
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -90,7 +90,7 @@ describe("TcpHealthCheckStrategy", () => {
|
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
describe("
|
|
93
|
+
describe("mergeResult", () => {
|
|
94
94
|
it("should calculate averages correctly", () => {
|
|
95
95
|
const strategy = new TcpHealthCheckStrategy();
|
|
96
96
|
const runs = [
|
|
@@ -118,11 +118,12 @@ describe("TcpHealthCheckStrategy", () => {
|
|
|
118
118
|
},
|
|
119
119
|
];
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
122
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
122
123
|
|
|
123
|
-
expect(aggregated.avgConnectionTime).toBe(15);
|
|
124
|
-
expect(aggregated.successRate).toBe(100);
|
|
125
|
-
expect(aggregated.errorCount).toBe(0);
|
|
124
|
+
expect(aggregated.avgConnectionTime.avg).toBe(15);
|
|
125
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
126
|
+
expect(aggregated.errorCount.count).toBe(0);
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
it("should count errors and calculate success rate", () => {
|
|
@@ -153,10 +154,11 @@ describe("TcpHealthCheckStrategy", () => {
|
|
|
153
154
|
},
|
|
154
155
|
];
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
158
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
157
159
|
|
|
158
|
-
expect(aggregated.successRate).toBe(50);
|
|
159
|
-
expect(aggregated.errorCount).toBe(1);
|
|
160
|
+
expect(aggregated.successRate.rate).toBe(50);
|
|
161
|
+
expect(aggregated.errorCount.count).toBe(1);
|
|
160
162
|
});
|
|
161
163
|
});
|
|
162
164
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -2,8 +2,16 @@ import {
|
|
|
2
2
|
HealthCheckStrategy,
|
|
3
3
|
HealthCheckRunForAggregation,
|
|
4
4
|
Versioned,
|
|
5
|
+
VersionedAggregated,
|
|
6
|
+
aggregatedAverage,
|
|
7
|
+
aggregatedRate,
|
|
8
|
+
aggregatedCounter,
|
|
9
|
+
mergeAverage,
|
|
10
|
+
mergeRate,
|
|
11
|
+
mergeCounter,
|
|
5
12
|
z,
|
|
6
13
|
type ConnectedClient,
|
|
14
|
+
type InferAggregatedResult,
|
|
7
15
|
} from "@checkstack/backend-api";
|
|
8
16
|
import {
|
|
9
17
|
healthResultBoolean,
|
|
@@ -70,27 +78,25 @@ const tcpResultSchema = healthResultSchema({
|
|
|
70
78
|
|
|
71
79
|
type TcpResult = z.infer<typeof tcpResultSchema>;
|
|
72
80
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const tcpAggregatedSchema = healthResultSchema({
|
|
77
|
-
avgConnectionTime: healthResultNumber({
|
|
81
|
+
/** Aggregated field definitions for bucket merging */
|
|
82
|
+
const tcpAggregatedFields = {
|
|
83
|
+
avgConnectionTime: aggregatedAverage({
|
|
78
84
|
"x-chart-type": "line",
|
|
79
85
|
"x-chart-label": "Avg Connection Time",
|
|
80
86
|
"x-chart-unit": "ms",
|
|
81
87
|
}),
|
|
82
|
-
successRate:
|
|
88
|
+
successRate: aggregatedRate({
|
|
83
89
|
"x-chart-type": "gauge",
|
|
84
90
|
"x-chart-label": "Success Rate",
|
|
85
91
|
"x-chart-unit": "%",
|
|
86
92
|
}),
|
|
87
|
-
errorCount:
|
|
93
|
+
errorCount: aggregatedCounter({
|
|
88
94
|
"x-chart-type": "counter",
|
|
89
95
|
"x-chart-label": "Errors",
|
|
90
96
|
}),
|
|
91
|
-
}
|
|
97
|
+
};
|
|
92
98
|
|
|
93
|
-
type TcpAggregatedResult =
|
|
99
|
+
type TcpAggregatedResult = InferAggregatedResult<typeof tcpAggregatedFields>;
|
|
94
100
|
|
|
95
101
|
// ============================================================================
|
|
96
102
|
// SOCKET INTERFACE (for testability)
|
|
@@ -159,15 +165,12 @@ const defaultSocketFactory: SocketFactory = () => {
|
|
|
159
165
|
// STRATEGY
|
|
160
166
|
// ============================================================================
|
|
161
167
|
|
|
162
|
-
export class TcpHealthCheckStrategy
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
TcpAggregatedResult
|
|
169
|
-
>
|
|
170
|
-
{
|
|
168
|
+
export class TcpHealthCheckStrategy implements HealthCheckStrategy<
|
|
169
|
+
TcpConfig,
|
|
170
|
+
TcpTransportClient,
|
|
171
|
+
TcpResult,
|
|
172
|
+
typeof tcpAggregatedFields
|
|
173
|
+
> {
|
|
171
174
|
id = "tcp";
|
|
172
175
|
displayName = "TCP Health Check";
|
|
173
176
|
description = "TCP port connectivity check with optional banner grab";
|
|
@@ -208,45 +211,33 @@ export class TcpHealthCheckStrategy
|
|
|
208
211
|
],
|
|
209
212
|
});
|
|
210
213
|
|
|
211
|
-
aggregatedResult
|
|
214
|
+
aggregatedResult = new VersionedAggregated({
|
|
212
215
|
version: 1,
|
|
213
|
-
|
|
216
|
+
fields: tcpAggregatedFields,
|
|
214
217
|
});
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
mergeResult(
|
|
220
|
+
existing: TcpAggregatedResult | undefined,
|
|
221
|
+
run: HealthCheckRunForAggregation<TcpResult>,
|
|
218
222
|
): TcpAggregatedResult {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
if (validRuns.length === 0) {
|
|
222
|
-
return { avgConnectionTime: 0, successRate: 0, errorCount: 0 };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const connectionTimes = validRuns
|
|
226
|
-
.map((r) => r.metadata?.connectionTimeMs)
|
|
227
|
-
.filter((t): t is number => typeof t === "number");
|
|
223
|
+
const metadata = run.metadata;
|
|
228
224
|
|
|
229
|
-
const avgConnectionTime =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
: 0;
|
|
225
|
+
const avgConnectionTime = mergeAverage(
|
|
226
|
+
existing?.avgConnectionTime,
|
|
227
|
+
metadata?.connectionTimeMs,
|
|
228
|
+
);
|
|
235
229
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
).length;
|
|
239
|
-
const successRate = Math.round((successCount / validRuns.length) * 100);
|
|
230
|
+
const isSuccess = metadata?.connected ?? false;
|
|
231
|
+
const successRate = mergeRate(existing?.successRate, isSuccess);
|
|
240
232
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
).length;
|
|
233
|
+
const hasError = metadata?.error !== undefined;
|
|
234
|
+
const errorCount = mergeCounter(existing?.errorCount, hasError);
|
|
244
235
|
|
|
245
236
|
return { avgConnectionTime, successRate, errorCount };
|
|
246
237
|
}
|
|
247
238
|
|
|
248
239
|
async createClient(
|
|
249
|
-
config: TcpConfig
|
|
240
|
+
config: TcpConfig,
|
|
250
241
|
): Promise<ConnectedClient<TcpTransportClient>> {
|
|
251
242
|
const validatedConfig = this.config.validate(config);
|
|
252
243
|
const socket = this.socketFactory();
|