@checkstack/healthcheck-jenkins-backend 0.2.12 → 0.3.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/collectors/build-history.test.ts +5 -4
- package/src/collectors/build-history.ts +32 -36
- package/src/collectors/job-status.test.ts +9 -7
- package/src/collectors/job-status.ts +36 -46
- package/src/collectors/node-health.test.ts +6 -5
- package/src/collectors/node-health.ts +41 -42
- package/src/collectors/queue-info.test.ts +6 -5
- package/src/collectors/queue-info.ts +36 -38
- package/src/collectors/server-info.test.ts +6 -5
- package/src/collectors/server-info.ts +29 -34
- package/src/strategy.test.ts +24 -17
- package/src/strategy.ts +43 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @checkstack/healthcheck-jenkins-backend
|
|
2
2
|
|
|
3
|
+
## 0.3.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.2.13
|
|
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.2.12
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-jenkins-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
"test": "bun test"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@checkstack/backend-api": "0.5.
|
|
14
|
-
"@checkstack/common": "0.6.
|
|
15
|
-
"@checkstack/healthcheck-common": "0.
|
|
13
|
+
"@checkstack/backend-api": "0.5.2",
|
|
14
|
+
"@checkstack/common": "0.6.1",
|
|
15
|
+
"@checkstack/healthcheck-common": "0.8.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/bun": "^1.0.0",
|
|
19
19
|
"typescript": "^5.0.0",
|
|
20
|
-
"@checkstack/tsconfig": "0.0.
|
|
21
|
-
"@checkstack/scripts": "0.1.
|
|
20
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
21
|
+
"@checkstack/scripts": "0.1.1"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -65,7 +65,7 @@ describe("BuildHistoryCollector", () => {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it("should aggregate correctly", () => {
|
|
68
|
-
const runs: Parameters<typeof collector.
|
|
68
|
+
const runs: Parameters<typeof collector.mergeResult>[1][] = [
|
|
69
69
|
{
|
|
70
70
|
status: "healthy" as const,
|
|
71
71
|
latencyMs: 100,
|
|
@@ -98,9 +98,10 @@ describe("BuildHistoryCollector", () => {
|
|
|
98
98
|
},
|
|
99
99
|
];
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
102
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
102
103
|
|
|
103
|
-
expect(aggregated.avgSuccessRate).toBe(70);
|
|
104
|
-
expect(aggregated.avgBuildDuration).toBe(70000);
|
|
104
|
+
expect(aggregated.avgSuccessRate.avg).toBe(70);
|
|
105
|
+
expect(aggregated.avgBuildDuration.avg).toBe(70000);
|
|
105
106
|
});
|
|
106
107
|
});
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
type HealthCheckRunForAggregation,
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
|
+
mergeAverage,
|
|
8
|
+
VersionedAggregated,
|
|
9
|
+
aggregatedAverage,
|
|
10
|
+
type InferAggregatedResult,
|
|
7
11
|
} from "@checkstack/backend-api";
|
|
8
12
|
import { healthResultNumber } from "@checkstack/healthcheck-common";
|
|
9
13
|
import { pluginMetadata } from "../plugin-metadata";
|
|
@@ -86,21 +90,23 @@ const buildHistoryResultSchema = z.object({
|
|
|
86
90
|
|
|
87
91
|
export type BuildHistoryResult = z.infer<typeof buildHistoryResultSchema>;
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
// Aggregated result fields definition
|
|
94
|
+
const buildHistoryAggregatedFields = {
|
|
95
|
+
avgSuccessRate: aggregatedAverage({
|
|
91
96
|
"x-chart-type": "gauge",
|
|
92
97
|
"x-chart-label": "Avg Success Rate",
|
|
93
98
|
"x-chart-unit": "%",
|
|
94
99
|
}),
|
|
95
|
-
avgBuildDuration:
|
|
100
|
+
avgBuildDuration: aggregatedAverage({
|
|
96
101
|
"x-chart-type": "line",
|
|
97
102
|
"x-chart-label": "Avg Build Duration",
|
|
98
103
|
"x-chart-unit": "ms",
|
|
99
104
|
}),
|
|
100
|
-
}
|
|
105
|
+
};
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
// Type inferred from field definitions
|
|
108
|
+
export type BuildHistoryAggregatedResult = InferAggregatedResult<
|
|
109
|
+
typeof buildHistoryAggregatedFields
|
|
104
110
|
>;
|
|
105
111
|
|
|
106
112
|
// ============================================================================
|
|
@@ -111,15 +117,12 @@ export type BuildHistoryAggregatedResult = z.infer<
|
|
|
111
117
|
* Collector for Jenkins build history.
|
|
112
118
|
* Analyzes recent builds for trends and patterns.
|
|
113
119
|
*/
|
|
114
|
-
export class BuildHistoryCollector
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
BuildHistoryAggregatedResult
|
|
121
|
-
>
|
|
122
|
-
{
|
|
120
|
+
export class BuildHistoryCollector implements CollectorStrategy<
|
|
121
|
+
JenkinsTransportClient,
|
|
122
|
+
BuildHistoryConfig,
|
|
123
|
+
BuildHistoryResult,
|
|
124
|
+
BuildHistoryAggregatedResult
|
|
125
|
+
> {
|
|
123
126
|
id = "build-history";
|
|
124
127
|
displayName = "Build History";
|
|
125
128
|
description = "Analyze recent build trends for a Jenkins job";
|
|
@@ -129,9 +132,9 @@ export class BuildHistoryCollector
|
|
|
129
132
|
|
|
130
133
|
config = new Versioned({ version: 1, schema: buildHistoryConfigSchema });
|
|
131
134
|
result = new Versioned({ version: 1, schema: buildHistoryResultSchema });
|
|
132
|
-
aggregatedResult = new
|
|
135
|
+
aggregatedResult = new VersionedAggregated({
|
|
133
136
|
version: 1,
|
|
134
|
-
|
|
137
|
+
fields: buildHistoryAggregatedFields,
|
|
135
138
|
});
|
|
136
139
|
|
|
137
140
|
async execute({
|
|
@@ -253,28 +256,21 @@ export class BuildHistoryCollector
|
|
|
253
256
|
};
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
mergeResult(
|
|
260
|
+
existing: BuildHistoryAggregatedResult | undefined,
|
|
261
|
+
run: HealthCheckRunForAggregation<BuildHistoryResult>,
|
|
258
262
|
): BuildHistoryAggregatedResult {
|
|
259
|
-
const
|
|
260
|
-
.map((r) => r.metadata?.successRate)
|
|
261
|
-
.filter((v): v is number => typeof v === "number");
|
|
262
|
-
|
|
263
|
-
const durations = runs
|
|
264
|
-
.map((r) => r.metadata?.avgDurationMs)
|
|
265
|
-
.filter((v): v is number => typeof v === "number");
|
|
263
|
+
const metadata = run.metadata;
|
|
266
264
|
|
|
267
265
|
return {
|
|
268
|
-
avgSuccessRate:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
|
|
277
|
-
: 0,
|
|
266
|
+
avgSuccessRate: mergeAverage(
|
|
267
|
+
existing?.avgSuccessRate,
|
|
268
|
+
metadata?.successRate,
|
|
269
|
+
),
|
|
270
|
+
avgBuildDuration: mergeAverage(
|
|
271
|
+
existing?.avgBuildDuration,
|
|
272
|
+
metadata?.avgDurationMs,
|
|
273
|
+
),
|
|
278
274
|
};
|
|
279
275
|
}
|
|
280
276
|
}
|
|
@@ -9,7 +9,7 @@ describe("JobStatusCollector", () => {
|
|
|
9
9
|
const collector = new JobStatusCollector();
|
|
10
10
|
|
|
11
11
|
const createMockClient = (
|
|
12
|
-
response: JenkinsResponse
|
|
12
|
+
response: JenkinsResponse,
|
|
13
13
|
): JenkinsTransportClient => ({
|
|
14
14
|
exec: async () => response,
|
|
15
15
|
});
|
|
@@ -93,12 +93,12 @@ describe("JobStatusCollector", () => {
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
expect(capturedPath).toBe(
|
|
96
|
-
"/job/folder/job/subfolder/job/nested-job/api/json"
|
|
96
|
+
"/job/folder/job/subfolder/job/nested-job/api/json",
|
|
97
97
|
);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
it("should aggregate success rate correctly", () => {
|
|
101
|
-
const runs: Parameters<typeof collector.
|
|
101
|
+
const runs: Parameters<typeof collector.mergeResult>[1][] = [
|
|
102
102
|
{
|
|
103
103
|
status: "healthy" as const,
|
|
104
104
|
latencyMs: 100,
|
|
@@ -137,10 +137,12 @@ describe("JobStatusCollector", () => {
|
|
|
137
137
|
},
|
|
138
138
|
];
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
141
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
142
|
+
aggregated = collector.mergeResult(aggregated, runs[2]);
|
|
141
143
|
|
|
142
|
-
expect(aggregated.successRate).toBe(67); // 2/3
|
|
143
|
-
expect(aggregated.avgBuildDurationMs).toBe(60000); // (60000+80000+40000)/3
|
|
144
|
-
expect(aggregated.buildableRate).toBe(100);
|
|
144
|
+
expect(aggregated.successRate.rate).toBe(67); // 2/3
|
|
145
|
+
expect(aggregated.avgBuildDurationMs.avg).toBe(60000); // (60000+80000+40000)/3
|
|
146
|
+
expect(aggregated.buildableRate.rate).toBe(100);
|
|
145
147
|
});
|
|
146
148
|
});
|
|
@@ -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,
|
|
@@ -77,26 +83,28 @@ const jobStatusResultSchema = z.object({
|
|
|
77
83
|
|
|
78
84
|
export type JobStatusResult = z.infer<typeof jobStatusResultSchema>;
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
// Aggregated result fields definition
|
|
87
|
+
const jobStatusAggregatedFields = {
|
|
88
|
+
avgBuildDurationMs: aggregatedAverage({
|
|
82
89
|
"x-chart-type": "line",
|
|
83
90
|
"x-chart-label": "Avg Build Duration",
|
|
84
91
|
"x-chart-unit": "ms",
|
|
85
92
|
}),
|
|
86
|
-
successRate:
|
|
93
|
+
successRate: aggregatedRate({
|
|
87
94
|
"x-chart-type": "gauge",
|
|
88
95
|
"x-chart-label": "Success Rate",
|
|
89
96
|
"x-chart-unit": "%",
|
|
90
97
|
}),
|
|
91
|
-
buildableRate:
|
|
98
|
+
buildableRate: aggregatedRate({
|
|
92
99
|
"x-chart-type": "gauge",
|
|
93
100
|
"x-chart-label": "Enabled Rate",
|
|
94
101
|
"x-chart-unit": "%",
|
|
95
102
|
}),
|
|
96
|
-
}
|
|
103
|
+
};
|
|
97
104
|
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
// Type inferred from field definitions
|
|
106
|
+
export type JobStatusAggregatedResult = InferAggregatedResult<
|
|
107
|
+
typeof jobStatusAggregatedFields
|
|
100
108
|
>;
|
|
101
109
|
|
|
102
110
|
// ============================================================================
|
|
@@ -107,15 +115,12 @@ export type JobStatusAggregatedResult = z.infer<
|
|
|
107
115
|
* Collector for Jenkins job status.
|
|
108
116
|
* Monitors individual job health and last build information.
|
|
109
117
|
*/
|
|
110
|
-
export class JobStatusCollector
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
JobStatusAggregatedResult
|
|
117
|
-
>
|
|
118
|
-
{
|
|
118
|
+
export class JobStatusCollector implements CollectorStrategy<
|
|
119
|
+
JenkinsTransportClient,
|
|
120
|
+
JobStatusConfig,
|
|
121
|
+
JobStatusResult,
|
|
122
|
+
JobStatusAggregatedResult
|
|
123
|
+
> {
|
|
119
124
|
id = "job-status";
|
|
120
125
|
displayName = "Job Status";
|
|
121
126
|
description = "Monitor Jenkins job status and last build information";
|
|
@@ -125,9 +130,9 @@ export class JobStatusCollector
|
|
|
125
130
|
|
|
126
131
|
config = new Versioned({ version: 1, schema: jobStatusConfigSchema });
|
|
127
132
|
result = new Versioned({ version: 1, schema: jobStatusResultSchema });
|
|
128
|
-
aggregatedResult = new
|
|
133
|
+
aggregatedResult = new VersionedAggregated({
|
|
129
134
|
version: 1,
|
|
130
|
-
|
|
135
|
+
fields: jobStatusAggregatedFields,
|
|
131
136
|
});
|
|
132
137
|
|
|
133
138
|
async execute({
|
|
@@ -205,37 +210,22 @@ export class JobStatusCollector
|
|
|
205
210
|
};
|
|
206
211
|
}
|
|
207
212
|
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
mergeResult(
|
|
214
|
+
existing: JobStatusAggregatedResult | undefined,
|
|
215
|
+
run: HealthCheckRunForAggregation<JobStatusResult>,
|
|
210
216
|
): JobStatusAggregatedResult {
|
|
211
|
-
const
|
|
212
|
-
.map((r) => r.metadata?.lastBuildDurationMs)
|
|
213
|
-
.filter((v): v is number => typeof v === "number");
|
|
214
|
-
|
|
215
|
-
const results = runs
|
|
216
|
-
.map((r) => r.metadata?.lastBuildResult)
|
|
217
|
-
.filter((v): v is string => typeof v === "string");
|
|
218
|
-
|
|
219
|
-
const buildables = runs
|
|
220
|
-
.map((r) => r.metadata?.buildable)
|
|
221
|
-
.filter((v): v is boolean => typeof v === "boolean");
|
|
222
|
-
|
|
223
|
-
const successCount = results.filter((r) => r === "SUCCESS").length;
|
|
224
|
-
const buildableCount = buildables.filter(Boolean).length;
|
|
217
|
+
const metadata = run.metadata;
|
|
225
218
|
|
|
226
219
|
return {
|
|
227
|
-
avgBuildDurationMs:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
successRate:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
buildableRate:
|
|
236
|
-
buildables.length > 0
|
|
237
|
-
? Math.round((buildableCount / buildables.length) * 100)
|
|
238
|
-
: 0,
|
|
220
|
+
avgBuildDurationMs: mergeAverage(
|
|
221
|
+
existing?.avgBuildDurationMs,
|
|
222
|
+
metadata?.lastBuildDurationMs,
|
|
223
|
+
),
|
|
224
|
+
successRate: mergeRate(
|
|
225
|
+
existing?.successRate,
|
|
226
|
+
metadata?.lastBuildResult === "SUCCESS",
|
|
227
|
+
),
|
|
228
|
+
buildableRate: mergeRate(existing?.buildableRate, metadata?.buildable),
|
|
239
229
|
};
|
|
240
230
|
}
|
|
241
231
|
}
|
|
@@ -111,7 +111,7 @@ describe("NodeHealthCollector", () => {
|
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
it("should aggregate correctly", () => {
|
|
114
|
-
const runs: Parameters<typeof collector.
|
|
114
|
+
const runs: Parameters<typeof collector.mergeResult>[1][] = [
|
|
115
115
|
{
|
|
116
116
|
status: "healthy" as const,
|
|
117
117
|
latencyMs: 100,
|
|
@@ -140,10 +140,11 @@ describe("NodeHealthCollector", () => {
|
|
|
140
140
|
},
|
|
141
141
|
];
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
144
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
144
145
|
|
|
145
|
-
expect(aggregated.avgOnlineNodes).toBe(4);
|
|
146
|
-
expect(aggregated.avgUtilization).toBe(60);
|
|
147
|
-
expect(aggregated.minOnlineNodes).toBe(3);
|
|
146
|
+
expect(aggregated.avgOnlineNodes.avg).toBe(4);
|
|
147
|
+
expect(aggregated.avgUtilization.avg).toBe(60);
|
|
148
|
+
expect(aggregated.minOnlineNodes.min).toBe(3);
|
|
148
149
|
});
|
|
149
150
|
});
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type HealthCheckRunForAggregation,
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
|
+
mergeAverage,
|
|
8
|
+
mergeMinMax,
|
|
9
|
+
VersionedAggregated,
|
|
10
|
+
aggregatedAverage,
|
|
11
|
+
aggregatedMinMax,
|
|
12
|
+
type InferAggregatedResult,
|
|
7
13
|
} from "@checkstack/backend-api";
|
|
8
14
|
import {
|
|
9
15
|
healthResultBoolean,
|
|
@@ -77,24 +83,26 @@ const nodeHealthResultSchema = z.object({
|
|
|
77
83
|
|
|
78
84
|
export type NodeHealthResult = z.infer<typeof nodeHealthResultSchema>;
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
// Aggregated result fields definition
|
|
87
|
+
const nodeHealthAggregatedFields = {
|
|
88
|
+
avgOnlineNodes: aggregatedAverage({
|
|
82
89
|
"x-chart-type": "line",
|
|
83
90
|
"x-chart-label": "Avg Online Nodes",
|
|
84
91
|
}),
|
|
85
|
-
avgUtilization:
|
|
92
|
+
avgUtilization: aggregatedAverage({
|
|
86
93
|
"x-chart-type": "gauge",
|
|
87
94
|
"x-chart-label": "Avg Utilization",
|
|
88
95
|
"x-chart-unit": "%",
|
|
89
96
|
}),
|
|
90
|
-
minOnlineNodes:
|
|
97
|
+
minOnlineNodes: aggregatedMinMax({
|
|
91
98
|
"x-chart-type": "line",
|
|
92
99
|
"x-chart-label": "Min Online Nodes",
|
|
93
100
|
}),
|
|
94
|
-
}
|
|
101
|
+
};
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
// Type inferred from field definitions
|
|
104
|
+
export type NodeHealthAggregatedResult = InferAggregatedResult<
|
|
105
|
+
typeof nodeHealthAggregatedFields
|
|
98
106
|
>;
|
|
99
107
|
|
|
100
108
|
// ============================================================================
|
|
@@ -105,15 +113,12 @@ export type NodeHealthAggregatedResult = z.infer<
|
|
|
105
113
|
* Collector for Jenkins node/agent health.
|
|
106
114
|
* Monitors node availability and executor utilization.
|
|
107
115
|
*/
|
|
108
|
-
export class NodeHealthCollector
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
NodeHealthAggregatedResult
|
|
115
|
-
>
|
|
116
|
-
{
|
|
116
|
+
export class NodeHealthCollector implements CollectorStrategy<
|
|
117
|
+
JenkinsTransportClient,
|
|
118
|
+
NodeHealthConfig,
|
|
119
|
+
NodeHealthResult,
|
|
120
|
+
NodeHealthAggregatedResult
|
|
121
|
+
> {
|
|
117
122
|
id = "node-health";
|
|
118
123
|
displayName = "Node Health";
|
|
119
124
|
description = "Monitor Jenkins agent/node availability and executor usage";
|
|
@@ -123,9 +128,9 @@ export class NodeHealthCollector
|
|
|
123
128
|
|
|
124
129
|
config = new Versioned({ version: 1, schema: nodeHealthConfigSchema });
|
|
125
130
|
result = new Versioned({ version: 1, schema: nodeHealthResultSchema });
|
|
126
|
-
aggregatedResult = new
|
|
131
|
+
aggregatedResult = new VersionedAggregated({
|
|
127
132
|
version: 1,
|
|
128
|
-
|
|
133
|
+
fields: nodeHealthAggregatedFields,
|
|
129
134
|
});
|
|
130
135
|
|
|
131
136
|
async execute({
|
|
@@ -147,7 +152,7 @@ export class NodeHealthCollector
|
|
|
147
152
|
|
|
148
153
|
private async executeForSingleNode(
|
|
149
154
|
nodeName: string,
|
|
150
|
-
client: JenkinsTransportClient
|
|
155
|
+
client: JenkinsTransportClient,
|
|
151
156
|
): Promise<CollectorResult<NodeHealthResult>> {
|
|
152
157
|
const encodedName = encodeURIComponent(nodeName);
|
|
153
158
|
const response = await client.exec({
|
|
@@ -209,7 +214,7 @@ export class NodeHealthCollector
|
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
private async executeForAllNodes(
|
|
212
|
-
client: JenkinsTransportClient
|
|
217
|
+
client: JenkinsTransportClient,
|
|
213
218
|
): Promise<CollectorResult<NodeHealthResult>> {
|
|
214
219
|
const response = await client.exec({
|
|
215
220
|
path: "/computer/api/json",
|
|
@@ -275,31 +280,25 @@ export class NodeHealthCollector
|
|
|
275
280
|
};
|
|
276
281
|
}
|
|
277
282
|
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
mergeResult(
|
|
284
|
+
existing: NodeHealthAggregatedResult | undefined,
|
|
285
|
+
run: HealthCheckRunForAggregation<NodeHealthResult>,
|
|
280
286
|
): NodeHealthAggregatedResult {
|
|
281
|
-
const
|
|
282
|
-
.map((r) => r.metadata?.onlineNodes)
|
|
283
|
-
.filter((v): v is number => typeof v === "number");
|
|
284
|
-
|
|
285
|
-
const utilizations = runs
|
|
286
|
-
.map((r) => r.metadata?.executorUtilization)
|
|
287
|
-
.filter((v): v is number => typeof v === "number");
|
|
287
|
+
const metadata = run.metadata;
|
|
288
288
|
|
|
289
289
|
return {
|
|
290
|
-
avgOnlineNodes:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
minOnlineNodes: onlineNodes.length > 0 ? Math.min(...onlineNodes) : 0,
|
|
290
|
+
avgOnlineNodes: mergeAverage(
|
|
291
|
+
existing?.avgOnlineNodes,
|
|
292
|
+
metadata?.onlineNodes,
|
|
293
|
+
),
|
|
294
|
+
avgUtilization: mergeAverage(
|
|
295
|
+
existing?.avgUtilization,
|
|
296
|
+
metadata?.executorUtilization,
|
|
297
|
+
),
|
|
298
|
+
minOnlineNodes: mergeMinMax(
|
|
299
|
+
existing?.minOnlineNodes,
|
|
300
|
+
metadata?.onlineNodes,
|
|
301
|
+
),
|
|
303
302
|
};
|
|
304
303
|
}
|
|
305
304
|
}
|
|
@@ -77,7 +77,7 @@ describe("QueueInfoCollector", () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it("should aggregate correctly", () => {
|
|
80
|
-
const runs: Parameters<typeof collector.
|
|
80
|
+
const runs: Parameters<typeof collector.mergeResult>[1][] = [
|
|
81
81
|
{
|
|
82
82
|
status: "healthy" as const,
|
|
83
83
|
latencyMs: 100,
|
|
@@ -104,10 +104,11 @@ describe("QueueInfoCollector", () => {
|
|
|
104
104
|
},
|
|
105
105
|
];
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
108
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
108
109
|
|
|
109
|
-
expect(aggregated.avgQueueLength).toBe(4);
|
|
110
|
-
expect(aggregated.maxQueueLength).toBe(5);
|
|
111
|
-
expect(aggregated.avgWaitTime).toBe(15000);
|
|
110
|
+
expect(aggregated.avgQueueLength.avg).toBe(4);
|
|
111
|
+
expect(aggregated.maxQueueLength.max).toBe(5);
|
|
112
|
+
expect(aggregated.avgWaitTime.avg).toBe(15000);
|
|
112
113
|
});
|
|
113
114
|
});
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type HealthCheckRunForAggregation,
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
|
+
mergeAverage,
|
|
8
|
+
mergeMinMax,
|
|
9
|
+
VersionedAggregated,
|
|
10
|
+
aggregatedAverage,
|
|
11
|
+
aggregatedMinMax,
|
|
12
|
+
type InferAggregatedResult,
|
|
7
13
|
} from "@checkstack/backend-api";
|
|
8
14
|
import { healthResultNumber } from "@checkstack/healthcheck-common";
|
|
9
15
|
import { pluginMetadata } from "../plugin-metadata";
|
|
@@ -52,24 +58,26 @@ const queueInfoResultSchema = z.object({
|
|
|
52
58
|
|
|
53
59
|
export type QueueInfoResult = z.infer<typeof queueInfoResultSchema>;
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
// Aggregated result fields definition
|
|
62
|
+
const queueInfoAggregatedFields = {
|
|
63
|
+
avgQueueLength: aggregatedAverage({
|
|
57
64
|
"x-chart-type": "line",
|
|
58
65
|
"x-chart-label": "Avg Queue Length",
|
|
59
66
|
}),
|
|
60
|
-
maxQueueLength:
|
|
67
|
+
maxQueueLength: aggregatedMinMax({
|
|
61
68
|
"x-chart-type": "line",
|
|
62
69
|
"x-chart-label": "Max Queue Length",
|
|
63
70
|
}),
|
|
64
|
-
avgWaitTime:
|
|
71
|
+
avgWaitTime: aggregatedAverage({
|
|
65
72
|
"x-chart-type": "line",
|
|
66
73
|
"x-chart-label": "Avg Wait Time",
|
|
67
74
|
"x-chart-unit": "ms",
|
|
68
75
|
}),
|
|
69
|
-
}
|
|
76
|
+
};
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
// Type inferred from field definitions
|
|
79
|
+
export type QueueInfoAggregatedResult = InferAggregatedResult<
|
|
80
|
+
typeof queueInfoAggregatedFields
|
|
73
81
|
>;
|
|
74
82
|
|
|
75
83
|
// ============================================================================
|
|
@@ -80,15 +88,12 @@ export type QueueInfoAggregatedResult = z.infer<
|
|
|
80
88
|
* Collector for Jenkins build queue.
|
|
81
89
|
* Monitors queue length and wait times.
|
|
82
90
|
*/
|
|
83
|
-
export class QueueInfoCollector
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
QueueInfoAggregatedResult
|
|
90
|
-
>
|
|
91
|
-
{
|
|
91
|
+
export class QueueInfoCollector implements CollectorStrategy<
|
|
92
|
+
JenkinsTransportClient,
|
|
93
|
+
QueueInfoConfig,
|
|
94
|
+
QueueInfoResult,
|
|
95
|
+
QueueInfoAggregatedResult
|
|
96
|
+
> {
|
|
92
97
|
id = "queue-info";
|
|
93
98
|
displayName = "Queue Info";
|
|
94
99
|
description = "Monitor Jenkins build queue length and wait times";
|
|
@@ -97,9 +102,9 @@ export class QueueInfoCollector
|
|
|
97
102
|
|
|
98
103
|
config = new Versioned({ version: 1, schema: queueInfoConfigSchema });
|
|
99
104
|
result = new Versioned({ version: 1, schema: queueInfoResultSchema });
|
|
100
|
-
aggregatedResult = new
|
|
105
|
+
aggregatedResult = new VersionedAggregated({
|
|
101
106
|
version: 1,
|
|
102
|
-
|
|
107
|
+
fields: queueInfoAggregatedFields,
|
|
103
108
|
});
|
|
104
109
|
|
|
105
110
|
async execute({
|
|
@@ -187,29 +192,22 @@ export class QueueInfoCollector
|
|
|
187
192
|
};
|
|
188
193
|
}
|
|
189
194
|
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
mergeResult(
|
|
196
|
+
existing: QueueInfoAggregatedResult | undefined,
|
|
197
|
+
run: HealthCheckRunForAggregation<QueueInfoResult>,
|
|
192
198
|
): QueueInfoAggregatedResult {
|
|
193
|
-
const
|
|
194
|
-
.map((r) => r.metadata?.queueLength)
|
|
195
|
-
.filter((v): v is number => typeof v === "number");
|
|
196
|
-
|
|
197
|
-
const waitTimes = runs
|
|
198
|
-
.map((r) => r.metadata?.avgWaitingMs)
|
|
199
|
-
.filter((v): v is number => typeof v === "number");
|
|
199
|
+
const metadata = run.metadata;
|
|
200
200
|
|
|
201
201
|
return {
|
|
202
|
-
avgQueueLength:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
? Math.round(waitTimes.reduce((a, b) => a + b, 0) / waitTimes.length)
|
|
212
|
-
: 0,
|
|
202
|
+
avgQueueLength: mergeAverage(
|
|
203
|
+
existing?.avgQueueLength,
|
|
204
|
+
metadata?.queueLength,
|
|
205
|
+
),
|
|
206
|
+
maxQueueLength: mergeMinMax(
|
|
207
|
+
existing?.maxQueueLength,
|
|
208
|
+
metadata?.queueLength,
|
|
209
|
+
),
|
|
210
|
+
avgWaitTime: mergeAverage(existing?.avgWaitTime, metadata?.avgWaitingMs),
|
|
213
211
|
};
|
|
214
212
|
}
|
|
215
213
|
}
|
|
@@ -9,7 +9,7 @@ describe("ServerInfoCollector", () => {
|
|
|
9
9
|
const collector = new ServerInfoCollector();
|
|
10
10
|
|
|
11
11
|
const createMockClient = (
|
|
12
|
-
response: JenkinsResponse
|
|
12
|
+
response: JenkinsResponse,
|
|
13
13
|
): JenkinsTransportClient => ({
|
|
14
14
|
exec: async () => response,
|
|
15
15
|
});
|
|
@@ -57,7 +57,7 @@ describe("ServerInfoCollector", () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it("should aggregate results correctly", () => {
|
|
60
|
-
const runs: Parameters<typeof collector.
|
|
60
|
+
const runs: Parameters<typeof collector.mergeResult>[1][] = [
|
|
61
61
|
{
|
|
62
62
|
status: "healthy" as const,
|
|
63
63
|
latencyMs: 100,
|
|
@@ -82,9 +82,10 @@ describe("ServerInfoCollector", () => {
|
|
|
82
82
|
},
|
|
83
83
|
];
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
86
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
86
87
|
|
|
87
|
-
expect(aggregated.avgExecutors).toBe(5);
|
|
88
|
-
expect(aggregated.avgTotalJobs).toBe(11);
|
|
88
|
+
expect(aggregated.avgExecutors.avg).toBe(5);
|
|
89
|
+
expect(aggregated.avgTotalJobs.avg).toBe(11);
|
|
89
90
|
});
|
|
90
91
|
});
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
type HealthCheckRunForAggregation,
|
|
5
5
|
type CollectorResult,
|
|
6
6
|
type CollectorStrategy,
|
|
7
|
+
mergeAverage,
|
|
8
|
+
VersionedAggregated,
|
|
9
|
+
aggregatedAverage,
|
|
10
|
+
type InferAggregatedResult,
|
|
7
11
|
} from "@checkstack/backend-api";
|
|
8
12
|
import {
|
|
9
13
|
healthResultNumber,
|
|
@@ -54,19 +58,21 @@ const serverInfoResultSchema = z.object({
|
|
|
54
58
|
|
|
55
59
|
export type ServerInfoResult = z.infer<typeof serverInfoResultSchema>;
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
// Aggregated result fields definition
|
|
62
|
+
const serverInfoAggregatedFields = {
|
|
63
|
+
avgExecutors: aggregatedAverage({
|
|
59
64
|
"x-chart-type": "line",
|
|
60
65
|
"x-chart-label": "Avg Executors",
|
|
61
66
|
}),
|
|
62
|
-
avgTotalJobs:
|
|
67
|
+
avgTotalJobs: aggregatedAverage({
|
|
63
68
|
"x-chart-type": "line",
|
|
64
69
|
"x-chart-label": "Avg Jobs",
|
|
65
70
|
}),
|
|
66
|
-
}
|
|
71
|
+
};
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
// Type inferred from field definitions
|
|
74
|
+
export type ServerInfoAggregatedResult = InferAggregatedResult<
|
|
75
|
+
typeof serverInfoAggregatedFields
|
|
70
76
|
>;
|
|
71
77
|
|
|
72
78
|
// ============================================================================
|
|
@@ -77,15 +83,12 @@ export type ServerInfoAggregatedResult = z.infer<
|
|
|
77
83
|
* Built-in collector for Jenkins server information.
|
|
78
84
|
* Fetches basic server health metrics via /api/json.
|
|
79
85
|
*/
|
|
80
|
-
export class ServerInfoCollector
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
ServerInfoAggregatedResult
|
|
87
|
-
>
|
|
88
|
-
{
|
|
86
|
+
export class ServerInfoCollector implements CollectorStrategy<
|
|
87
|
+
JenkinsTransportClient,
|
|
88
|
+
ServerInfoConfig,
|
|
89
|
+
ServerInfoResult,
|
|
90
|
+
ServerInfoAggregatedResult
|
|
91
|
+
> {
|
|
89
92
|
id = "server-info";
|
|
90
93
|
displayName = "Server Info";
|
|
91
94
|
description = "Collects Jenkins server information and health metrics";
|
|
@@ -94,9 +97,9 @@ export class ServerInfoCollector
|
|
|
94
97
|
|
|
95
98
|
config = new Versioned({ version: 1, schema: serverInfoConfigSchema });
|
|
96
99
|
result = new Versioned({ version: 1, schema: serverInfoResultSchema });
|
|
97
|
-
aggregatedResult = new
|
|
100
|
+
aggregatedResult = new VersionedAggregated({
|
|
98
101
|
version: 1,
|
|
99
|
-
|
|
102
|
+
fields: serverInfoAggregatedFields,
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
async execute({
|
|
@@ -144,26 +147,18 @@ export class ServerInfoCollector
|
|
|
144
147
|
};
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
mergeResult(
|
|
151
|
+
existing: ServerInfoAggregatedResult | undefined,
|
|
152
|
+
run: HealthCheckRunForAggregation<ServerInfoResult>,
|
|
149
153
|
): ServerInfoAggregatedResult {
|
|
150
|
-
const
|
|
151
|
-
.map((r) => r.metadata?.numExecutors)
|
|
152
|
-
.filter((v): v is number => typeof v === "number");
|
|
153
|
-
|
|
154
|
-
const jobs = runs
|
|
155
|
-
.map((r) => r.metadata?.totalJobs)
|
|
156
|
-
.filter((v): v is number => typeof v === "number");
|
|
154
|
+
const metadata = run.metadata;
|
|
157
155
|
|
|
158
156
|
return {
|
|
159
|
-
avgExecutors:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
avgTotalJobs:
|
|
164
|
-
jobs.length > 0
|
|
165
|
-
? Math.round(jobs.reduce((a, b) => a + b, 0) / jobs.length)
|
|
166
|
-
: 0,
|
|
157
|
+
avgExecutors: mergeAverage(
|
|
158
|
+
existing?.avgExecutors,
|
|
159
|
+
metadata?.numExecutors,
|
|
160
|
+
),
|
|
161
|
+
avgTotalJobs: mergeAverage(existing?.avgTotalJobs, metadata?.totalJobs),
|
|
167
162
|
};
|
|
168
163
|
}
|
|
169
164
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ describe("JenkinsHealthCheckStrategy", () => {
|
|
|
41
41
|
status: 200,
|
|
42
42
|
statusText: "OK",
|
|
43
43
|
headers: { "X-Jenkins": "2.426.1" },
|
|
44
|
-
})
|
|
44
|
+
}),
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
const connectedClient = await strategy.createClient({
|
|
@@ -66,7 +66,7 @@ describe("JenkinsHealthCheckStrategy", () => {
|
|
|
66
66
|
it("should include query parameters in request", async () => {
|
|
67
67
|
let capturedUrl = "";
|
|
68
68
|
spyOn(globalThis, "fetch").mockImplementation((async (
|
|
69
|
-
url: RequestInfo | URL
|
|
69
|
+
url: RequestInfo | URL,
|
|
70
70
|
) => {
|
|
71
71
|
capturedUrl = url.toString();
|
|
72
72
|
return new Response(JSON.stringify({}), { status: 200 });
|
|
@@ -94,7 +94,7 @@ describe("JenkinsHealthCheckStrategy", () => {
|
|
|
94
94
|
let capturedHeaders: Record<string, string> | undefined;
|
|
95
95
|
spyOn(globalThis, "fetch").mockImplementation((async (
|
|
96
96
|
_url: RequestInfo | URL,
|
|
97
|
-
options?: RequestInit
|
|
97
|
+
options?: RequestInit,
|
|
98
98
|
) => {
|
|
99
99
|
capturedHeaders = options?.headers as Record<string, string>;
|
|
100
100
|
return new Response(JSON.stringify({}), { status: 200 });
|
|
@@ -112,7 +112,7 @@ describe("JenkinsHealthCheckStrategy", () => {
|
|
|
112
112
|
expect(capturedHeaders?.["Authorization"]).toContain("Basic ");
|
|
113
113
|
const decoded = Buffer.from(
|
|
114
114
|
capturedHeaders?.["Authorization"]?.replace("Basic ", "") || "",
|
|
115
|
-
"base64"
|
|
115
|
+
"base64",
|
|
116
116
|
).toString();
|
|
117
117
|
expect(decoded).toBe("admin:api-token-123");
|
|
118
118
|
|
|
@@ -121,7 +121,7 @@ describe("JenkinsHealthCheckStrategy", () => {
|
|
|
121
121
|
|
|
122
122
|
it("should return error for HTTP error response", async () => {
|
|
123
123
|
spyOn(globalThis, "fetch").mockResolvedValue(
|
|
124
|
-
new Response(null, { status: 401, statusText: "Unauthorized" })
|
|
124
|
+
new Response(null, { status: 401, statusText: "Unauthorized" }),
|
|
125
125
|
);
|
|
126
126
|
|
|
127
127
|
const connectedClient = await strategy.createClient({
|
|
@@ -160,9 +160,9 @@ describe("JenkinsHealthCheckStrategy", () => {
|
|
|
160
160
|
});
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
describe("
|
|
163
|
+
describe("mergeResult", () => {
|
|
164
164
|
it("should calculate success rate from runs", () => {
|
|
165
|
-
const runs: Parameters<typeof strategy.
|
|
165
|
+
const runs: Parameters<typeof strategy.mergeResult>[1][] = [
|
|
166
166
|
{
|
|
167
167
|
status: "healthy" as const,
|
|
168
168
|
latencyMs: 100,
|
|
@@ -180,19 +180,26 @@ describe("JenkinsHealthCheckStrategy", () => {
|
|
|
180
180
|
},
|
|
181
181
|
];
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
184
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
185
|
+
aggregated = strategy.mergeResult(aggregated, runs[2]);
|
|
184
186
|
|
|
185
|
-
expect(aggregated.successRate).toBe(67); // 2/3
|
|
186
|
-
expect(aggregated.avgResponseTimeMs).toBe(175); // (150+200)/2
|
|
187
|
-
expect(aggregated.errorCount).toBe(1);
|
|
187
|
+
expect(aggregated.successRate.rate).toBe(67); // 2/3
|
|
188
|
+
expect(aggregated.avgResponseTimeMs.avg).toBe(175); // (150+200)/2
|
|
189
|
+
expect(aggregated.errorCount.count).toBe(1);
|
|
188
190
|
});
|
|
189
191
|
|
|
190
|
-
it("should handle
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
it("should handle single run", () => {
|
|
193
|
+
const run = {
|
|
194
|
+
status: "healthy" as const,
|
|
195
|
+
latencyMs: 100,
|
|
196
|
+
metadata: { connected: true, responseTimeMs: 150 },
|
|
197
|
+
};
|
|
198
|
+
const aggregated = strategy.mergeResult(undefined, run);
|
|
199
|
+
|
|
200
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
201
|
+
expect(aggregated.avgResponseTimeMs.avg).toBe(150);
|
|
202
|
+
expect(aggregated.errorCount.count).toBe(0);
|
|
196
203
|
});
|
|
197
204
|
});
|
|
198
205
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -2,10 +2,18 @@ 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
|
configString,
|
|
7
14
|
configNumber,
|
|
8
15
|
type ConnectedClient,
|
|
16
|
+
type InferAggregatedResult,
|
|
9
17
|
} from "@checkstack/backend-api";
|
|
10
18
|
import {
|
|
11
19
|
healthResultNumber,
|
|
@@ -33,10 +41,10 @@ export const jenkinsConfigSchema = z.object({
|
|
|
33
41
|
.url()
|
|
34
42
|
.describe("Jenkins server URL (e.g., https://jenkins.example.com)"),
|
|
35
43
|
username: configString({}).describe(
|
|
36
|
-
"Jenkins username for API authentication"
|
|
44
|
+
"Jenkins username for API authentication",
|
|
37
45
|
),
|
|
38
46
|
apiToken: configString({ "x-secret": true }).describe(
|
|
39
|
-
"Jenkins API token (generate from User > Configure > API Token)"
|
|
47
|
+
"Jenkins API token (generate from User > Configure > API Token)",
|
|
40
48
|
),
|
|
41
49
|
timeout: configNumber({})
|
|
42
50
|
.int()
|
|
@@ -66,39 +74,38 @@ const jenkinsResultSchema = healthResultSchema({
|
|
|
66
74
|
|
|
67
75
|
type JenkinsResult = z.infer<typeof jenkinsResultSchema>;
|
|
68
76
|
|
|
69
|
-
/** Aggregated
|
|
70
|
-
const
|
|
71
|
-
successRate:
|
|
77
|
+
/** Aggregated field definitions for bucket merging */
|
|
78
|
+
const jenkinsAggregatedFields = {
|
|
79
|
+
successRate: aggregatedRate({
|
|
72
80
|
"x-chart-type": "gauge",
|
|
73
81
|
"x-chart-label": "Success Rate",
|
|
74
82
|
"x-chart-unit": "%",
|
|
75
83
|
}),
|
|
76
|
-
avgResponseTimeMs:
|
|
84
|
+
avgResponseTimeMs: aggregatedAverage({
|
|
77
85
|
"x-chart-type": "line",
|
|
78
86
|
"x-chart-label": "Avg Response Time",
|
|
79
87
|
"x-chart-unit": "ms",
|
|
80
88
|
}),
|
|
81
|
-
errorCount:
|
|
89
|
+
errorCount: aggregatedCounter({
|
|
82
90
|
"x-chart-type": "counter",
|
|
83
91
|
"x-chart-label": "Errors",
|
|
84
92
|
}),
|
|
85
|
-
}
|
|
93
|
+
};
|
|
86
94
|
|
|
87
|
-
type JenkinsAggregatedResult =
|
|
95
|
+
type JenkinsAggregatedResult = InferAggregatedResult<
|
|
96
|
+
typeof jenkinsAggregatedFields
|
|
97
|
+
>;
|
|
88
98
|
|
|
89
99
|
// ============================================================================
|
|
90
100
|
// STRATEGY
|
|
91
101
|
// ============================================================================
|
|
92
102
|
|
|
93
|
-
export class JenkinsHealthCheckStrategy
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
JenkinsAggregatedResult
|
|
100
|
-
>
|
|
101
|
-
{
|
|
103
|
+
export class JenkinsHealthCheckStrategy implements HealthCheckStrategy<
|
|
104
|
+
JenkinsConfig,
|
|
105
|
+
JenkinsTransportClient,
|
|
106
|
+
JenkinsResult,
|
|
107
|
+
typeof jenkinsAggregatedFields
|
|
108
|
+
> {
|
|
102
109
|
id = "jenkins";
|
|
103
110
|
displayName = "Jenkins Health Check";
|
|
104
111
|
description = "Monitor Jenkins CI/CD server health and job status";
|
|
@@ -113,23 +120,23 @@ export class JenkinsHealthCheckStrategy
|
|
|
113
120
|
schema: jenkinsResultSchema,
|
|
114
121
|
});
|
|
115
122
|
|
|
116
|
-
aggregatedResult
|
|
123
|
+
aggregatedResult = new VersionedAggregated({
|
|
117
124
|
version: 1,
|
|
118
|
-
|
|
125
|
+
fields: jenkinsAggregatedFields,
|
|
119
126
|
});
|
|
120
127
|
|
|
121
128
|
/**
|
|
122
129
|
* Create a Jenkins transport client for API requests.
|
|
123
130
|
*/
|
|
124
131
|
async createClient(
|
|
125
|
-
config: JenkinsConfig
|
|
132
|
+
config: JenkinsConfig,
|
|
126
133
|
): Promise<ConnectedClient<JenkinsTransportClient>> {
|
|
127
134
|
const validatedConfig = this.config.validate(config);
|
|
128
135
|
const baseUrl = validatedConfig.baseUrl.replace(/\/$/, ""); // Remove trailing slash
|
|
129
136
|
|
|
130
137
|
// Create Basic Auth header
|
|
131
138
|
const authHeader = `Basic ${Buffer.from(
|
|
132
|
-
`${validatedConfig.username}:${validatedConfig.apiToken}
|
|
139
|
+
`${validatedConfig.username}:${validatedConfig.apiToken}`,
|
|
133
140
|
).toString("base64")}`;
|
|
134
141
|
|
|
135
142
|
const client: JenkinsTransportClient = {
|
|
@@ -144,7 +151,7 @@ export class JenkinsHealthCheckStrategy
|
|
|
144
151
|
const controller = new AbortController();
|
|
145
152
|
const timeoutId = setTimeout(
|
|
146
153
|
() => controller.abort(),
|
|
147
|
-
validatedConfig.timeout
|
|
154
|
+
validatedConfig.timeout,
|
|
148
155
|
);
|
|
149
156
|
|
|
150
157
|
try {
|
|
@@ -200,31 +207,23 @@ export class JenkinsHealthCheckStrategy
|
|
|
200
207
|
};
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
mergeResult(
|
|
211
|
+
existing: JenkinsAggregatedResult | undefined,
|
|
212
|
+
run: HealthCheckRunForAggregation<JenkinsResult>,
|
|
205
213
|
): JenkinsAggregatedResult {
|
|
206
|
-
const
|
|
214
|
+
const metadata = run.metadata;
|
|
207
215
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
const avgResponseTimeMs = mergeAverage(
|
|
217
|
+
existing?.avgResponseTimeMs,
|
|
218
|
+
metadata?.responseTimeMs,
|
|
219
|
+
);
|
|
211
220
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
.filter((t): t is number => typeof t === "number");
|
|
221
|
+
const isSuccess = metadata?.connected ?? false;
|
|
222
|
+
const successRate = mergeRate(existing?.successRate, isSuccess);
|
|
215
223
|
|
|
216
|
-
const
|
|
217
|
-
const errorCount =
|
|
224
|
+
const hasError = metadata?.error !== undefined;
|
|
225
|
+
const errorCount = mergeCounter(existing?.errorCount, hasError);
|
|
218
226
|
|
|
219
|
-
return {
|
|
220
|
-
successRate: Math.round((successCount / validRuns.length) * 100),
|
|
221
|
-
avgResponseTimeMs:
|
|
222
|
-
responseTimes.length > 0
|
|
223
|
-
? Math.round(
|
|
224
|
-
responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
|
225
|
-
)
|
|
226
|
-
: 0,
|
|
227
|
-
errorCount,
|
|
228
|
-
};
|
|
227
|
+
return { successRate, avgResponseTimeMs, errorCount };
|
|
229
228
|
}
|
|
230
229
|
}
|