@checkstack/healthcheck-postgres-backend 0.1.12 → 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 +43 -0
- package/package.json +6 -6
- package/src/query-collector.test.ts +5 -3
- package/src/query-collector.ts +38 -27
- package/src/strategy.test.ts +24 -24
- package/src/strategy.ts +65 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @checkstack/healthcheck-postgres-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
|
+
|
|
33
|
+
## 0.1.13
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- 0b9fc58: Fix workspace:\* protocol resolution in published packages
|
|
38
|
+
|
|
39
|
+
Published packages now correctly have resolved dependency versions instead of `workspace:*` references. This is achieved by using `bun publish` which properly resolves workspace protocol references.
|
|
40
|
+
|
|
41
|
+
- Updated dependencies [0b9fc58]
|
|
42
|
+
- @checkstack/backend-api@0.5.2
|
|
43
|
+
- @checkstack/common@0.6.1
|
|
44
|
+
- @checkstack/healthcheck-common@0.8.1
|
|
45
|
+
|
|
3
46
|
## 0.1.12
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-postgres-backend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/backend-api": "
|
|
13
|
-
"@checkstack/common": "
|
|
14
|
-
"@checkstack/healthcheck-common": "
|
|
12
|
+
"@checkstack/backend-api": "0.5.2",
|
|
13
|
+
"@checkstack/common": "0.6.1",
|
|
14
|
+
"@checkstack/healthcheck-common": "0.8.1",
|
|
15
15
|
"pg": "^8.11.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/bun": "^1.0.0",
|
|
19
19
|
"@types/pg": "^8.11.0",
|
|
20
20
|
"typescript": "^5.0.0",
|
|
21
|
-
"@checkstack/tsconfig": "
|
|
22
|
-
"@checkstack/scripts": "
|
|
21
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
22
|
+
"@checkstack/scripts": "0.1.1"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -64,7 +64,7 @@ describe("QueryCollector", () => {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
describe("
|
|
67
|
+
describe("mergeResult", () => {
|
|
68
68
|
it("should calculate average execution time and success rate", () => {
|
|
69
69
|
const collector = new QueryCollector();
|
|
70
70
|
const runs = [
|
|
@@ -86,7 +86,8 @@ describe("QueryCollector", () => {
|
|
|
86
86
|
},
|
|
87
87
|
];
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
90
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
90
91
|
|
|
91
92
|
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
92
93
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -113,7 +114,8 @@ describe("QueryCollector", () => {
|
|
|
113
114
|
},
|
|
114
115
|
];
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
118
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
117
119
|
|
|
118
120
|
expect(aggregated.successRate).toBe(50);
|
|
119
121
|
});
|
package/src/query-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
|
+
averageStateSchema,
|
|
10
|
+
rateStateSchema,
|
|
11
|
+
type AverageState,
|
|
12
|
+
type RateState,
|
|
7
13
|
} from "@checkstack/backend-api";
|
|
8
14
|
import {
|
|
9
15
|
healthResultNumber,
|
|
@@ -45,7 +51,7 @@ const queryResultSchema = healthResultSchema({
|
|
|
45
51
|
|
|
46
52
|
export type QueryResult = z.infer<typeof queryResultSchema>;
|
|
47
53
|
|
|
48
|
-
const
|
|
54
|
+
const queryAggregatedDisplaySchema = healthResultSchema({
|
|
49
55
|
avgExecutionTimeMs: healthResultNumber({
|
|
50
56
|
"x-chart-type": "line",
|
|
51
57
|
"x-chart-label": "Avg Execution Time",
|
|
@@ -58,6 +64,15 @@ const queryAggregatedSchema = healthResultSchema({
|
|
|
58
64
|
}),
|
|
59
65
|
});
|
|
60
66
|
|
|
67
|
+
const queryAggregatedInternalSchema = z.object({
|
|
68
|
+
_executionTime: averageStateSchema.optional(),
|
|
69
|
+
_success: rateStateSchema.optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const queryAggregatedSchema = queryAggregatedDisplaySchema.merge(
|
|
73
|
+
queryAggregatedInternalSchema,
|
|
74
|
+
);
|
|
75
|
+
|
|
61
76
|
export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
|
|
62
77
|
|
|
63
78
|
// ============================================================================
|
|
@@ -68,15 +83,12 @@ export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
|
|
|
68
83
|
* Built-in PostgreSQL query collector.
|
|
69
84
|
* Executes SQL queries and checks results.
|
|
70
85
|
*/
|
|
71
|
-
export class QueryCollector
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
QueryAggregatedResult
|
|
78
|
-
>
|
|
79
|
-
{
|
|
86
|
+
export class QueryCollector implements CollectorStrategy<
|
|
87
|
+
PostgresTransportClient,
|
|
88
|
+
QueryConfig,
|
|
89
|
+
QueryResult,
|
|
90
|
+
QueryAggregatedResult
|
|
91
|
+
> {
|
|
80
92
|
id = "query";
|
|
81
93
|
displayName = "SQL Query";
|
|
82
94
|
description = "Execute a SQL query and check the result";
|
|
@@ -115,28 +127,27 @@ export class QueryCollector
|
|
|
115
127
|
};
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
mergeResult(
|
|
131
|
+
existing: QueryAggregatedResult | undefined,
|
|
132
|
+
run: HealthCheckRunForAggregation<QueryResult>,
|
|
120
133
|
): QueryAggregatedResult {
|
|
121
|
-
const
|
|
122
|
-
.map((r) => r.metadata?.executionTimeMs)
|
|
123
|
-
.filter((v): v is number => typeof v === "number");
|
|
134
|
+
const metadata = run.metadata;
|
|
124
135
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
const executionTimeState = mergeAverage(
|
|
137
|
+
existing?._executionTime as AverageState | undefined,
|
|
138
|
+
metadata?.executionTimeMs,
|
|
139
|
+
);
|
|
128
140
|
|
|
129
|
-
const
|
|
141
|
+
const successState = mergeRate(
|
|
142
|
+
existing?._success as RateState | undefined,
|
|
143
|
+
metadata?.success,
|
|
144
|
+
);
|
|
130
145
|
|
|
131
146
|
return {
|
|
132
|
-
avgExecutionTimeMs:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
successRate:
|
|
137
|
-
successes.length > 0
|
|
138
|
-
? Math.round((successCount / successes.length) * 100)
|
|
139
|
-
: 0,
|
|
147
|
+
avgExecutionTimeMs: executionTimeState.avg,
|
|
148
|
+
successRate: successState.rate,
|
|
149
|
+
_executionTime: executionTimeState,
|
|
150
|
+
_success: successState,
|
|
140
151
|
};
|
|
141
152
|
}
|
|
142
153
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
8
8
|
rowCount?: number;
|
|
9
9
|
queryError?: Error;
|
|
10
10
|
connectError?: Error;
|
|
11
|
-
} = {}
|
|
11
|
+
} = {},
|
|
12
12
|
): DbClient => ({
|
|
13
13
|
connect: mock(() =>
|
|
14
14
|
config.connectError
|
|
@@ -17,10 +17,10 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
17
17
|
query: mock(() =>
|
|
18
18
|
config.queryError
|
|
19
19
|
? Promise.reject(config.queryError)
|
|
20
|
-
: Promise.resolve({ rowCount: config.rowCount ?? 1 })
|
|
20
|
+
: Promise.resolve({ rowCount: config.rowCount ?? 1 }),
|
|
21
21
|
),
|
|
22
22
|
end: mock(() => Promise.resolve()),
|
|
23
|
-
})
|
|
23
|
+
}),
|
|
24
24
|
),
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -46,7 +46,7 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
46
46
|
|
|
47
47
|
it("should throw for connection error", async () => {
|
|
48
48
|
const strategy = new PostgresHealthCheckStrategy(
|
|
49
|
-
createMockClient({ connectError: new Error("Connection refused") })
|
|
49
|
+
createMockClient({ connectError: new Error("Connection refused") }),
|
|
50
50
|
);
|
|
51
51
|
|
|
52
52
|
await expect(
|
|
@@ -57,7 +57,7 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
57
57
|
user: "postgres",
|
|
58
58
|
password: "secret",
|
|
59
59
|
timeout: 5000,
|
|
60
|
-
})
|
|
60
|
+
}),
|
|
61
61
|
).rejects.toThrow("Connection refused");
|
|
62
62
|
});
|
|
63
63
|
});
|
|
@@ -85,7 +85,7 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
85
85
|
|
|
86
86
|
it("should return error for query error", async () => {
|
|
87
87
|
const strategy = new PostgresHealthCheckStrategy(
|
|
88
|
-
createMockClient({ queryError: new Error("Syntax error") })
|
|
88
|
+
createMockClient({ queryError: new Error("Syntax error") }),
|
|
89
89
|
);
|
|
90
90
|
const connectedClient = await strategy.createClient({
|
|
91
91
|
host: "localhost",
|
|
@@ -107,7 +107,7 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
107
107
|
|
|
108
108
|
it("should return custom row count", async () => {
|
|
109
109
|
const strategy = new PostgresHealthCheckStrategy(
|
|
110
|
-
createMockClient({ rowCount: 5 })
|
|
110
|
+
createMockClient({ rowCount: 5 }),
|
|
111
111
|
);
|
|
112
112
|
const connectedClient = await strategy.createClient({
|
|
113
113
|
host: "localhost",
|
|
@@ -128,8 +128,8 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
describe("
|
|
132
|
-
it("should calculate averages correctly", () => {
|
|
131
|
+
describe("mergeResult", () => {
|
|
132
|
+
it("should calculate averages correctly through incremental merging", () => {
|
|
133
133
|
const strategy = new PostgresHealthCheckStrategy();
|
|
134
134
|
const runs = [
|
|
135
135
|
{
|
|
@@ -158,7 +158,9 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
158
158
|
},
|
|
159
159
|
];
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
// Merge incrementally
|
|
162
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
163
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
162
164
|
|
|
163
165
|
expect(aggregated.avgConnectionTime).toBe(75);
|
|
164
166
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -167,22 +169,20 @@ describe("PostgresHealthCheckStrategy", () => {
|
|
|
167
169
|
|
|
168
170
|
it("should count errors", () => {
|
|
169
171
|
const strategy = new PostgresHealthCheckStrategy();
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
error: "Connection refused",
|
|
181
|
-
},
|
|
172
|
+
const run = {
|
|
173
|
+
id: "1",
|
|
174
|
+
status: "unhealthy" as const,
|
|
175
|
+
latencyMs: 100,
|
|
176
|
+
checkId: "c1",
|
|
177
|
+
timestamp: new Date(),
|
|
178
|
+
metadata: {
|
|
179
|
+
connected: false,
|
|
180
|
+
connectionTimeMs: 100,
|
|
181
|
+
error: "Connection refused",
|
|
182
182
|
},
|
|
183
|
-
|
|
183
|
+
};
|
|
184
184
|
|
|
185
|
-
const aggregated = strategy.
|
|
185
|
+
const aggregated = strategy.mergeResult(undefined, run);
|
|
186
186
|
|
|
187
187
|
expect(aggregated.errorCount).toBe(1);
|
|
188
188
|
expect(aggregated.successRate).toBe(0);
|
package/src/strategy.ts
CHANGED
|
@@ -8,6 +8,18 @@ import {
|
|
|
8
8
|
configNumber,
|
|
9
9
|
configBoolean,
|
|
10
10
|
type ConnectedClient,
|
|
11
|
+
mergeAverage,
|
|
12
|
+
mergeRate,
|
|
13
|
+
mergeCounter,
|
|
14
|
+
mergeMinMax,
|
|
15
|
+
averageStateSchema,
|
|
16
|
+
minMaxStateSchema,
|
|
17
|
+
rateStateSchema,
|
|
18
|
+
counterStateSchema,
|
|
19
|
+
type AverageState,
|
|
20
|
+
type RateState,
|
|
21
|
+
type CounterState,
|
|
22
|
+
type MinMaxState,
|
|
11
23
|
} from "@checkstack/backend-api";
|
|
12
24
|
import {
|
|
13
25
|
healthResultBoolean,
|
|
@@ -73,7 +85,8 @@ type PostgresResult = z.infer<typeof postgresResultSchema>;
|
|
|
73
85
|
/**
|
|
74
86
|
* Aggregated metadata for buckets.
|
|
75
87
|
*/
|
|
76
|
-
|
|
88
|
+
// UI-visible aggregated fields (for charts)
|
|
89
|
+
const postgresAggregatedDisplaySchema = healthResultSchema({
|
|
77
90
|
avgConnectionTime: healthResultNumber({
|
|
78
91
|
"x-chart-type": "line",
|
|
79
92
|
"x-chart-label": "Avg Connection Time",
|
|
@@ -95,6 +108,18 @@ const postgresAggregatedSchema = healthResultSchema({
|
|
|
95
108
|
}),
|
|
96
109
|
});
|
|
97
110
|
|
|
111
|
+
// Internal state for incremental aggregation
|
|
112
|
+
const postgresAggregatedInternalSchema = z.object({
|
|
113
|
+
_connectionTime: averageStateSchema.optional(),
|
|
114
|
+
_maxConnectionTime: minMaxStateSchema.optional(),
|
|
115
|
+
_success: rateStateSchema.optional(),
|
|
116
|
+
_errors: counterStateSchema.optional(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const postgresAggregatedSchema = postgresAggregatedDisplaySchema.merge(
|
|
120
|
+
postgresAggregatedInternalSchema,
|
|
121
|
+
);
|
|
122
|
+
|
|
98
123
|
type PostgresAggregatedResult = z.infer<typeof postgresAggregatedSchema>;
|
|
99
124
|
|
|
100
125
|
// ============================================================================
|
|
@@ -133,15 +158,12 @@ const defaultDbClient: DbClient = {
|
|
|
133
158
|
// STRATEGY
|
|
134
159
|
// ============================================================================
|
|
135
160
|
|
|
136
|
-
export class PostgresHealthCheckStrategy
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
PostgresAggregatedResult
|
|
143
|
-
>
|
|
144
|
-
{
|
|
161
|
+
export class PostgresHealthCheckStrategy implements HealthCheckStrategy<
|
|
162
|
+
PostgresConfig,
|
|
163
|
+
PostgresTransportClient,
|
|
164
|
+
PostgresResult,
|
|
165
|
+
PostgresAggregatedResult
|
|
166
|
+
> {
|
|
145
167
|
id = "postgres";
|
|
146
168
|
displayName = "PostgreSQL Health Check";
|
|
147
169
|
description = "PostgreSQL database connectivity and query health check";
|
|
@@ -183,53 +205,50 @@ export class PostgresHealthCheckStrategy
|
|
|
183
205
|
schema: postgresAggregatedSchema,
|
|
184
206
|
});
|
|
185
207
|
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
mergeResult(
|
|
209
|
+
existing: PostgresAggregatedResult | undefined,
|
|
210
|
+
run: HealthCheckRunForAggregation<PostgresResult>,
|
|
188
211
|
): PostgresAggregatedResult {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
if (validRuns.length === 0) {
|
|
192
|
-
return {
|
|
193
|
-
avgConnectionTime: 0,
|
|
194
|
-
maxConnectionTime: 0,
|
|
195
|
-
successRate: 0,
|
|
196
|
-
errorCount: 0,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const connectionTimes = validRuns
|
|
201
|
-
.map((r) => r.metadata?.connectionTimeMs)
|
|
202
|
-
.filter((t): t is number => typeof t === "number");
|
|
212
|
+
const metadata = run.metadata;
|
|
203
213
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
: 0;
|
|
214
|
+
// Merge connection time average
|
|
215
|
+
const connectionTimeState = mergeAverage(
|
|
216
|
+
existing?._connectionTime as AverageState | undefined,
|
|
217
|
+
metadata?.connectionTimeMs,
|
|
218
|
+
);
|
|
210
219
|
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
// Merge max connection time
|
|
221
|
+
const maxConnectionTimeState = mergeMinMax(
|
|
222
|
+
existing?._maxConnectionTime as MinMaxState | undefined,
|
|
223
|
+
metadata?.connectionTimeMs,
|
|
224
|
+
);
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
// Merge success rate
|
|
227
|
+
const successState = mergeRate(
|
|
228
|
+
existing?._success as RateState | undefined,
|
|
229
|
+
metadata?.connected,
|
|
230
|
+
);
|
|
218
231
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
232
|
+
// Merge error count
|
|
233
|
+
const errorState = mergeCounter(
|
|
234
|
+
existing?._errors as CounterState | undefined,
|
|
235
|
+
metadata?.error !== undefined,
|
|
236
|
+
);
|
|
222
237
|
|
|
223
238
|
return {
|
|
224
|
-
avgConnectionTime,
|
|
225
|
-
maxConnectionTime,
|
|
226
|
-
successRate,
|
|
227
|
-
errorCount,
|
|
239
|
+
avgConnectionTime: connectionTimeState.avg,
|
|
240
|
+
maxConnectionTime: maxConnectionTimeState.max,
|
|
241
|
+
successRate: successState.rate,
|
|
242
|
+
errorCount: errorState.count,
|
|
243
|
+
_connectionTime: connectionTimeState,
|
|
244
|
+
_maxConnectionTime: maxConnectionTimeState,
|
|
245
|
+
_success: successState,
|
|
246
|
+
_errors: errorState,
|
|
228
247
|
};
|
|
229
248
|
}
|
|
230
249
|
|
|
231
250
|
async createClient(
|
|
232
|
-
config: PostgresConfigInput
|
|
251
|
+
config: PostgresConfigInput,
|
|
233
252
|
): Promise<ConnectedClient<PostgresTransportClient>> {
|
|
234
253
|
const validatedConfig = this.config.validate(config);
|
|
235
254
|
|