@checkstack/backend-api 0.5.2 → 0.7.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 +7 -6
- package/src/aggregated-result.test.ts +277 -0
- package/src/aggregated-result.ts +473 -0
- package/src/collector-strategy.ts +17 -6
- package/src/health-check.ts +27 -18
- package/src/incremental-aggregation.test.ts +243 -0
- package/src/incremental-aggregation.ts +308 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe aggregated result schema factories.
|
|
3
|
+
*
|
|
4
|
+
* These factories create aggregated result fields that automatically pair
|
|
5
|
+
* display fields with their internal state, enabling proper merging when
|
|
6
|
+
* combining buckets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z, type ZodTypeAny } from "zod";
|
|
10
|
+
import type { HealthResultMeta } from "@checkstack/common";
|
|
11
|
+
import { healthResultRegistry } from "@checkstack/healthcheck-common";
|
|
12
|
+
import {
|
|
13
|
+
mergeAverageStates,
|
|
14
|
+
mergeRateStates,
|
|
15
|
+
mergeCounterStates,
|
|
16
|
+
mergeMinMaxStates,
|
|
17
|
+
type AverageState,
|
|
18
|
+
type AverageStateInput,
|
|
19
|
+
type RateState,
|
|
20
|
+
type RateStateInput,
|
|
21
|
+
type CounterState,
|
|
22
|
+
type CounterStateInput,
|
|
23
|
+
type MinMaxState,
|
|
24
|
+
type MinMaxStateInput,
|
|
25
|
+
} from "./incremental-aggregation";
|
|
26
|
+
import { Versioned, type Migration } from "./config-versioning";
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// AGGREGATION TYPE ENUM
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Types of aggregation supported by the system.
|
|
34
|
+
* Each type has a corresponding internal state schema and merge function.
|
|
35
|
+
*/
|
|
36
|
+
export type AggregationType = "average" | "rate" | "counter" | "minmax";
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// AGGREGATED FIELD TYPES
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/** Base metadata for chart annotations (excludes x-jsonpath) */
|
|
43
|
+
type ChartMeta = Omit<HealthResultMeta, "x-jsonpath">;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Base interface for aggregated field definitions.
|
|
47
|
+
* TState is the output type (with _type), TStateInput is the input type (without _type).
|
|
48
|
+
*/
|
|
49
|
+
interface AggregatedFieldBase<TState, TStateInput> {
|
|
50
|
+
type: AggregationType;
|
|
51
|
+
stateSchema: ZodTypeAny;
|
|
52
|
+
meta: ChartMeta;
|
|
53
|
+
mergeStates: (a: TStateInput, b: TStateInput) => TState;
|
|
54
|
+
getDisplayValue: (state: TStateInput) => number;
|
|
55
|
+
getInitialState: () => TState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AggregatedAverageField extends AggregatedFieldBase<AverageState, AverageStateInput> {
|
|
59
|
+
type: "average";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AggregatedRateField extends AggregatedFieldBase<RateState, RateStateInput> {
|
|
63
|
+
type: "rate";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AggregatedCounterField extends AggregatedFieldBase<CounterState, CounterStateInput> {
|
|
67
|
+
type: "counter";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AggregatedMinMaxField extends AggregatedFieldBase<MinMaxState, MinMaxStateInput> {
|
|
71
|
+
type: "minmax";
|
|
72
|
+
getMinValue: (state: MinMaxStateInput) => number;
|
|
73
|
+
getMaxValue: (state: MinMaxStateInput) => number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type AggregatedField =
|
|
77
|
+
| AggregatedAverageField
|
|
78
|
+
| AggregatedRateField
|
|
79
|
+
| AggregatedCounterField
|
|
80
|
+
| AggregatedMinMaxField;
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// TYPE INFERENCE UTILITIES
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the internal state type for a field.
|
|
88
|
+
*/
|
|
89
|
+
type InferFieldState<T extends AggregatedField> =
|
|
90
|
+
T extends AggregatedAverageField
|
|
91
|
+
? AverageState
|
|
92
|
+
: T extends AggregatedRateField
|
|
93
|
+
? RateState
|
|
94
|
+
: T extends AggregatedCounterField
|
|
95
|
+
? CounterState
|
|
96
|
+
: T extends AggregatedMinMaxField
|
|
97
|
+
? MinMaxState
|
|
98
|
+
: never;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Infer the aggregated result type from field definitions.
|
|
102
|
+
* Each field key maps directly to its state type.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const fields = {
|
|
107
|
+
* avgResponseTimeMs: aggregatedAverage({...}),
|
|
108
|
+
* successRate: aggregatedRate({...}),
|
|
109
|
+
* };
|
|
110
|
+
* type Result = InferAggregatedResult<typeof fields>;
|
|
111
|
+
* // Result = {
|
|
112
|
+
* // avgResponseTimeMs: AverageState;
|
|
113
|
+
* // successRate: RateState;
|
|
114
|
+
* // }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export type InferAggregatedResult<T extends AggregatedResultShape> = {
|
|
118
|
+
[K in keyof T]: InferFieldState<T[K]>;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create an aggregated average field.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const fields = {
|
|
127
|
+
* avgResponseTimeMs: aggregatedAverage({
|
|
128
|
+
* "x-chart-type": "line",
|
|
129
|
+
* "x-chart-label": "Avg Response Time",
|
|
130
|
+
* "x-chart-unit": "ms",
|
|
131
|
+
* }),
|
|
132
|
+
* };
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function aggregatedAverage(meta: ChartMeta): AggregatedAverageField {
|
|
136
|
+
// Create fresh schema instance so each field gets its own chart metadata
|
|
137
|
+
const stateSchema = z.object({
|
|
138
|
+
_type: z.literal("average"),
|
|
139
|
+
_sum: z.number(),
|
|
140
|
+
_count: z.number(),
|
|
141
|
+
avg: z.number(),
|
|
142
|
+
});
|
|
143
|
+
// Register chart metadata for this field
|
|
144
|
+
stateSchema.register(healthResultRegistry, meta);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type: "average",
|
|
148
|
+
stateSchema,
|
|
149
|
+
meta,
|
|
150
|
+
mergeStates: mergeAverageStates,
|
|
151
|
+
getDisplayValue: (state) => state.avg,
|
|
152
|
+
getInitialState: () => ({
|
|
153
|
+
_type: "average" as const,
|
|
154
|
+
_sum: 0,
|
|
155
|
+
_count: 0,
|
|
156
|
+
avg: 0,
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create an aggregated rate/percentage field.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const fields = {
|
|
167
|
+
* successRate: aggregatedRate({
|
|
168
|
+
* "x-chart-type": "gauge",
|
|
169
|
+
* "x-chart-label": "Success Rate",
|
|
170
|
+
* "x-chart-unit": "%",
|
|
171
|
+
* }),
|
|
172
|
+
* };
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export function aggregatedRate(meta: ChartMeta): AggregatedRateField {
|
|
176
|
+
// Create fresh schema instance so each field gets its own chart metadata
|
|
177
|
+
const stateSchema = z.object({
|
|
178
|
+
_type: z.literal("rate"),
|
|
179
|
+
_success: z.number(),
|
|
180
|
+
_total: z.number(),
|
|
181
|
+
rate: z.number(),
|
|
182
|
+
});
|
|
183
|
+
// Register chart metadata for this field
|
|
184
|
+
stateSchema.register(healthResultRegistry, meta);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
type: "rate",
|
|
188
|
+
stateSchema,
|
|
189
|
+
meta,
|
|
190
|
+
mergeStates: mergeRateStates,
|
|
191
|
+
getDisplayValue: (state) => state.rate,
|
|
192
|
+
getInitialState: () => ({
|
|
193
|
+
_type: "rate" as const,
|
|
194
|
+
_success: 0,
|
|
195
|
+
_total: 0,
|
|
196
|
+
rate: 0,
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create an aggregated counter field.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const fields = {
|
|
207
|
+
* errorCount: aggregatedCounter({
|
|
208
|
+
* "x-chart-type": "counter",
|
|
209
|
+
* "x-chart-label": "Errors",
|
|
210
|
+
* }),
|
|
211
|
+
* };
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function aggregatedCounter(meta: ChartMeta): AggregatedCounterField {
|
|
215
|
+
// Create fresh schema instance so each field gets its own chart metadata
|
|
216
|
+
const stateSchema = z.object({
|
|
217
|
+
_type: z.literal("counter"),
|
|
218
|
+
count: z.number(),
|
|
219
|
+
});
|
|
220
|
+
// Register chart metadata for this field
|
|
221
|
+
stateSchema.register(healthResultRegistry, meta);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
type: "counter",
|
|
225
|
+
stateSchema,
|
|
226
|
+
meta,
|
|
227
|
+
mergeStates: mergeCounterStates,
|
|
228
|
+
getDisplayValue: (state) => state.count,
|
|
229
|
+
getInitialState: () => ({ _type: "counter" as const, count: 0 }),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Create an aggregated min/max field.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const fields = {
|
|
239
|
+
* latencyRange: aggregatedMinMax({
|
|
240
|
+
* "x-chart-type": "line",
|
|
241
|
+
* "x-chart-label": "Latency Range",
|
|
242
|
+
* "x-chart-unit": "ms",
|
|
243
|
+
* }),
|
|
244
|
+
* };
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export function aggregatedMinMax(meta: ChartMeta): AggregatedMinMaxField {
|
|
248
|
+
// Create fresh schema instance so each field gets its own chart metadata
|
|
249
|
+
const stateSchema = z.object({
|
|
250
|
+
_type: z.literal("minmax"),
|
|
251
|
+
min: z.number(),
|
|
252
|
+
max: z.number(),
|
|
253
|
+
});
|
|
254
|
+
// Register chart metadata for this field
|
|
255
|
+
stateSchema.register(healthResultRegistry, meta);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
type: "minmax",
|
|
259
|
+
stateSchema,
|
|
260
|
+
meta,
|
|
261
|
+
mergeStates: mergeMinMaxStates,
|
|
262
|
+
getDisplayValue: (state) => state.max, // Default to max
|
|
263
|
+
getMinValue: (state) => state.min,
|
|
264
|
+
getMaxValue: (state) => state.max,
|
|
265
|
+
getInitialState: () => ({ _type: "minmax" as const, min: 0, max: 0 }),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// =============================================================================
|
|
270
|
+
// AGGREGATED RESULT SCHEMA BUILDER
|
|
271
|
+
// =============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Definition for aggregated result schema.
|
|
275
|
+
* Maps display field names to their aggregated field configurations.
|
|
276
|
+
*/
|
|
277
|
+
export type AggregatedResultShape = Record<string, AggregatedField>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Registry to store field definitions for lookup during merge operations.
|
|
281
|
+
*/
|
|
282
|
+
const fieldRegistry = new WeakMap<ZodTypeAny, AggregatedResultShape>();
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build a Zod schema for aggregated results.
|
|
286
|
+
* Each field key maps directly to its state type.
|
|
287
|
+
*
|
|
288
|
+
* @param fields - Map of field names to aggregated field definitions
|
|
289
|
+
* @returns Object containing schema, fields, and merge function
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* const { schema, mergeAggregatedResults } = buildAggregatedResultSchema({
|
|
294
|
+
* avgResponseTimeMs: aggregatedAverage({ ... }),
|
|
295
|
+
* successRate: aggregatedRate({ ... }),
|
|
296
|
+
* });
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export function buildAggregatedResultSchema<T extends AggregatedResultShape>(
|
|
300
|
+
fields: T,
|
|
301
|
+
): {
|
|
302
|
+
schema: z.ZodType<InferAggregatedResult<T>>;
|
|
303
|
+
fields: T;
|
|
304
|
+
mergeAggregatedResults: (
|
|
305
|
+
a: InferAggregatedResult<T> | undefined,
|
|
306
|
+
b: InferAggregatedResult<T>,
|
|
307
|
+
) => InferAggregatedResult<T>;
|
|
308
|
+
} {
|
|
309
|
+
// Build schema shape - each field key maps to its state schema
|
|
310
|
+
// Schema instances are already registered with chart metadata by the factory functions
|
|
311
|
+
const shape: Record<string, ZodTypeAny> = {};
|
|
312
|
+
|
|
313
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
314
|
+
shape[key] = field.stateSchema;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const schema = z.object(shape);
|
|
318
|
+
|
|
319
|
+
// Store field definitions for later lookup
|
|
320
|
+
fieldRegistry.set(schema, fields);
|
|
321
|
+
|
|
322
|
+
// Create merge function
|
|
323
|
+
const mergeAggregatedResults = (
|
|
324
|
+
a: Record<string, unknown> | undefined,
|
|
325
|
+
b: Record<string, unknown>,
|
|
326
|
+
): Record<string, unknown> => {
|
|
327
|
+
const result: Record<string, unknown> = {};
|
|
328
|
+
|
|
329
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
330
|
+
// Get states from both results using the field key
|
|
331
|
+
const stateA = a?.[key] as Record<string, unknown> | undefined;
|
|
332
|
+
const stateB = b[key] as Record<string, unknown> | undefined;
|
|
333
|
+
|
|
334
|
+
let mergedState: Record<string, unknown>;
|
|
335
|
+
|
|
336
|
+
if (stateA && stateB) {
|
|
337
|
+
// Both exist, merge based on type
|
|
338
|
+
switch (field.type) {
|
|
339
|
+
case "average": {
|
|
340
|
+
mergedState = mergeAverageStates(
|
|
341
|
+
stateA as AverageState,
|
|
342
|
+
stateB as AverageState,
|
|
343
|
+
);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case "rate": {
|
|
347
|
+
mergedState = mergeRateStates(
|
|
348
|
+
stateA as RateState,
|
|
349
|
+
stateB as RateState,
|
|
350
|
+
);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "counter": {
|
|
354
|
+
mergedState = mergeCounterStates(
|
|
355
|
+
stateA as CounterState,
|
|
356
|
+
stateB as CounterState,
|
|
357
|
+
);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "minmax": {
|
|
361
|
+
mergedState = mergeMinMaxStates(
|
|
362
|
+
stateA as MinMaxState,
|
|
363
|
+
stateB as MinMaxState,
|
|
364
|
+
);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} else if (stateA) {
|
|
369
|
+
// Only A exists
|
|
370
|
+
mergedState = stateA;
|
|
371
|
+
} else if (stateB) {
|
|
372
|
+
// Only B exists
|
|
373
|
+
mergedState = stateB as Record<string, unknown>;
|
|
374
|
+
} else {
|
|
375
|
+
// Both missing, use initial state
|
|
376
|
+
mergedState = field.getInitialState() as Record<string, unknown>;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Store state directly at field key
|
|
380
|
+
result[key] = mergedState;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return result;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
schema: schema as unknown as z.ZodType<InferAggregatedResult<T>>,
|
|
388
|
+
fields,
|
|
389
|
+
mergeAggregatedResults: mergeAggregatedResults as (
|
|
390
|
+
a: InferAggregatedResult<T> | undefined,
|
|
391
|
+
b: InferAggregatedResult<T>,
|
|
392
|
+
) => InferAggregatedResult<T>,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get field definitions from a schema.
|
|
398
|
+
*/
|
|
399
|
+
export function getAggregatedFields(
|
|
400
|
+
schema: ZodTypeAny,
|
|
401
|
+
): AggregatedResultShape | undefined {
|
|
402
|
+
return fieldRegistry.get(schema);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// =============================================================================
|
|
406
|
+
// VERSIONED AGGREGATED
|
|
407
|
+
// =============================================================================
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Options for creating a VersionedAggregated instance.
|
|
411
|
+
*/
|
|
412
|
+
export interface VersionedAggregatedOptions<T extends AggregatedResultShape> {
|
|
413
|
+
/** Current schema version */
|
|
414
|
+
version: number;
|
|
415
|
+
/** Aggregated result field definitions */
|
|
416
|
+
fields: T;
|
|
417
|
+
/** Optional migrations for backward compatibility */
|
|
418
|
+
migrations?: Migration<unknown, unknown>[];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Versioned schema for aggregated results that bundles the merge function.
|
|
423
|
+
* Use this instead of Versioned for collector aggregatedResult fields.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* const aggregatedResult = new VersionedAggregated({
|
|
428
|
+
* version: 1,
|
|
429
|
+
* fields: {
|
|
430
|
+
* avgResponseTimeMs: aggregatedAverage("_responseTime", {...}),
|
|
431
|
+
* successRate: aggregatedRate("_success", {...}),
|
|
432
|
+
* },
|
|
433
|
+
* });
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
export class VersionedAggregated<
|
|
437
|
+
T extends AggregatedResultShape,
|
|
438
|
+
> extends Versioned<Record<string, unknown>> {
|
|
439
|
+
readonly fields: T;
|
|
440
|
+
private readonly _mergeAggregatedStates: (
|
|
441
|
+
a: Record<string, unknown> | undefined,
|
|
442
|
+
b: Record<string, unknown>,
|
|
443
|
+
) => Record<string, unknown>;
|
|
444
|
+
|
|
445
|
+
constructor(options: VersionedAggregatedOptions<T>) {
|
|
446
|
+
const { schema, mergeAggregatedResults } = buildAggregatedResultSchema(
|
|
447
|
+
options.fields,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
super({
|
|
451
|
+
version: options.version,
|
|
452
|
+
schema: schema as z.ZodType<Record<string, unknown>>,
|
|
453
|
+
migrations: options.migrations,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
this.fields = options.fields;
|
|
457
|
+
this._mergeAggregatedStates = mergeAggregatedResults as (
|
|
458
|
+
a: Record<string, unknown> | undefined,
|
|
459
|
+
b: Record<string, unknown>,
|
|
460
|
+
) => Record<string, unknown>;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Merge two pre-aggregated states.
|
|
465
|
+
* Used when combining buckets (e.g., during re-aggregation for chart display).
|
|
466
|
+
*/
|
|
467
|
+
mergeAggregatedStates(
|
|
468
|
+
a: Record<string, unknown> | undefined,
|
|
469
|
+
b: Record<string, unknown>,
|
|
470
|
+
): Record<string, unknown> {
|
|
471
|
+
return this._mergeAggregatedStates(a, b);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -2,6 +2,10 @@ import type { PluginMetadata } from "@checkstack/common";
|
|
|
2
2
|
import type { TransportClient } from "./transport-client";
|
|
3
3
|
import type { Versioned } from "./config-versioning";
|
|
4
4
|
import type { HealthCheckRunForAggregation } from "./health-check";
|
|
5
|
+
import type {
|
|
6
|
+
VersionedAggregated,
|
|
7
|
+
AggregatedResultShape,
|
|
8
|
+
} from "./aggregated-result";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Result from a collector execution.
|
|
@@ -29,7 +33,7 @@ export interface CollectorStrategy<
|
|
|
29
33
|
TClient extends TransportClient<unknown, unknown>,
|
|
30
34
|
TConfig = unknown,
|
|
31
35
|
TResult = Record<string, unknown>,
|
|
32
|
-
TAggregated = Record<string, unknown
|
|
36
|
+
TAggregated = Record<string, unknown>,
|
|
33
37
|
> {
|
|
34
38
|
/** Unique identifier for this collector */
|
|
35
39
|
id: string;
|
|
@@ -58,8 +62,8 @@ export interface CollectorStrategy<
|
|
|
58
62
|
/** Per-execution result schema (with x-chart-* metadata) */
|
|
59
63
|
result: Versioned<TResult>;
|
|
60
64
|
|
|
61
|
-
/** Aggregated result schema for bucket storage */
|
|
62
|
-
aggregatedResult:
|
|
65
|
+
/** Aggregated result schema for bucket storage with merge function */
|
|
66
|
+
aggregatedResult: VersionedAggregated<AggregatedResultShape>;
|
|
63
67
|
|
|
64
68
|
/**
|
|
65
69
|
* Execute the collector using the provided transport client.
|
|
@@ -76,8 +80,15 @@ export interface CollectorStrategy<
|
|
|
76
80
|
}): Promise<CollectorResult<TResult>>;
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
|
-
*
|
|
80
|
-
* Called
|
|
83
|
+
* Incrementally merge new run data into an existing aggregate.
|
|
84
|
+
* Called after each health check run for real-time aggregation.
|
|
85
|
+
*
|
|
86
|
+
* @param existing - Existing aggregated result (undefined for first run in bucket)
|
|
87
|
+
* @param newRun - Data from the new run to merge
|
|
88
|
+
* @returns Updated aggregated result
|
|
81
89
|
*/
|
|
82
|
-
|
|
90
|
+
mergeResult(
|
|
91
|
+
existing: TAggregated | undefined,
|
|
92
|
+
newRun: HealthCheckRunForAggregation<TResult>,
|
|
93
|
+
): TAggregated;
|
|
83
94
|
}
|
package/src/health-check.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Versioned } from "./config-versioning";
|
|
2
2
|
import type { TransportClient } from "./transport-client";
|
|
3
|
+
import type {
|
|
4
|
+
VersionedAggregated,
|
|
5
|
+
AggregatedResultShape,
|
|
6
|
+
} from "./aggregated-result";
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Health check result with typed metadata.
|
|
@@ -13,10 +17,10 @@ export interface HealthCheckResult<TMetadata = Record<string, unknown>> {
|
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
|
-
* Raw run data for aggregation (passed to
|
|
20
|
+
* Raw run data for aggregation (passed to mergeResult function).
|
|
17
21
|
*/
|
|
18
22
|
export interface HealthCheckRunForAggregation<
|
|
19
|
-
TResultMetadata = Record<string, unknown
|
|
23
|
+
TResultMetadata = Record<string, unknown>,
|
|
20
24
|
> {
|
|
21
25
|
status: "healthy" | "unhealthy" | "degraded";
|
|
22
26
|
latencyMs?: number;
|
|
@@ -27,7 +31,7 @@ export interface HealthCheckRunForAggregation<
|
|
|
27
31
|
* Connected transport client with cleanup capability.
|
|
28
32
|
*/
|
|
29
33
|
export interface ConnectedClient<
|
|
30
|
-
TClient extends TransportClient<unknown, unknown
|
|
34
|
+
TClient extends TransportClient<unknown, unknown>,
|
|
31
35
|
> {
|
|
32
36
|
/** The connected transport client */
|
|
33
37
|
client: TClient;
|
|
@@ -45,7 +49,7 @@ export interface ConnectedClient<
|
|
|
45
49
|
* @template TConfig - Configuration type for this strategy
|
|
46
50
|
* @template TClient - Transport client type (e.g., SshTransportClient)
|
|
47
51
|
* @template TResult - Per-run result type (for aggregation)
|
|
48
|
-
* @template
|
|
52
|
+
* @template TAggregatedFields - Aggregated field definitions for VersionedAggregated
|
|
49
53
|
*/
|
|
50
54
|
export interface HealthCheckStrategy<
|
|
51
55
|
TConfig = unknown,
|
|
@@ -54,7 +58,7 @@ export interface HealthCheckStrategy<
|
|
|
54
58
|
unknown
|
|
55
59
|
>,
|
|
56
60
|
TResult = Record<string, unknown>,
|
|
57
|
-
|
|
61
|
+
TAggregatedFields extends AggregatedResultShape = AggregatedResultShape,
|
|
58
62
|
> {
|
|
59
63
|
id: string;
|
|
60
64
|
displayName: string;
|
|
@@ -66,8 +70,8 @@ export interface HealthCheckStrategy<
|
|
|
66
70
|
/** Optional result schema with versioning and migrations */
|
|
67
71
|
result?: Versioned<TResult>;
|
|
68
72
|
|
|
69
|
-
/** Aggregated result schema for long-term bucket storage */
|
|
70
|
-
aggregatedResult:
|
|
73
|
+
/** Aggregated result schema for long-term bucket storage with automatic merging */
|
|
74
|
+
aggregatedResult: VersionedAggregated<TAggregatedFields>;
|
|
71
75
|
|
|
72
76
|
/**
|
|
73
77
|
* Create a connected transport client from the configuration.
|
|
@@ -80,14 +84,19 @@ export interface HealthCheckStrategy<
|
|
|
80
84
|
createClient(config: TConfig): Promise<ConnectedClient<TClient>>;
|
|
81
85
|
|
|
82
86
|
/**
|
|
83
|
-
*
|
|
84
|
-
* Called
|
|
87
|
+
* Incrementally merge new run data into an existing aggregate.
|
|
88
|
+
* Called after each health check run for real-time aggregation.
|
|
85
89
|
* Core metrics (counts, latency) are auto-calculated by platform.
|
|
86
90
|
* This function only handles strategy-specific result aggregation.
|
|
91
|
+
*
|
|
92
|
+
* @param existing - Existing aggregated result (undefined for first run in bucket)
|
|
93
|
+
* @param newRun - Data from the new run to merge
|
|
94
|
+
* @returns Updated aggregated result
|
|
87
95
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
mergeResult(
|
|
97
|
+
existing: Record<string, unknown> | undefined,
|
|
98
|
+
newRun: HealthCheckRunForAggregation<TResult>,
|
|
99
|
+
): Record<string, unknown>;
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/**
|
|
@@ -98,7 +107,7 @@ export interface RegisteredStrategy {
|
|
|
98
107
|
unknown,
|
|
99
108
|
TransportClient<unknown, unknown>,
|
|
100
109
|
unknown,
|
|
101
|
-
|
|
110
|
+
AggregatedResultShape
|
|
102
111
|
>;
|
|
103
112
|
ownerPluginId: string;
|
|
104
113
|
qualifiedId: string;
|
|
@@ -110,24 +119,24 @@ export interface HealthCheckRegistry {
|
|
|
110
119
|
unknown,
|
|
111
120
|
TransportClient<unknown, unknown>,
|
|
112
121
|
unknown,
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
AggregatedResultShape
|
|
123
|
+
>,
|
|
115
124
|
): void;
|
|
116
125
|
getStrategy(
|
|
117
|
-
id: string
|
|
126
|
+
id: string,
|
|
118
127
|
):
|
|
119
128
|
| HealthCheckStrategy<
|
|
120
129
|
unknown,
|
|
121
130
|
TransportClient<unknown, unknown>,
|
|
122
131
|
unknown,
|
|
123
|
-
|
|
132
|
+
AggregatedResultShape
|
|
124
133
|
>
|
|
125
134
|
| undefined;
|
|
126
135
|
getStrategies(): HealthCheckStrategy<
|
|
127
136
|
unknown,
|
|
128
137
|
TransportClient<unknown, unknown>,
|
|
129
138
|
unknown,
|
|
130
|
-
|
|
139
|
+
AggregatedResultShape
|
|
131
140
|
>[];
|
|
132
141
|
/**
|
|
133
142
|
* Get all registered strategies with their metadata (qualified ID, owner plugin).
|