@checkstack/healthcheck-backend 1.21.2 → 1.21.3
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 +70 -0
- package/package.json +29 -31
- package/src/index.ts +30 -14
- package/src/queue-executor.ts +9 -13
- package/src/realtime-aggregation.test.ts +10 -11
- package/src/realtime-aggregation.ts +12 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# @checkstack/healthcheck-backend
|
|
2
2
|
|
|
3
|
+
## 1.21.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6c8b36b: Promote the health-check run-queue contract and the observability window
|
|
8
|
+
math into `@checkstack/healthcheck-common`: `HEALTH_CHECK_QUEUE`,
|
|
9
|
+
`HealthCheckJobPayload`, `fastPathJobId` (per-plugin prefix) and
|
|
10
|
+
`computeWindowBounds`/`computeSecondsSinceLast` now have ONE definition
|
|
11
|
+
that the queue owner (healthcheck-backend) and every observability
|
|
12
|
+
strategy plugin import, replacing the per-plugin mirror copies that had
|
|
13
|
+
to be kept in lock-step by convention. Enqueued job ids and window
|
|
14
|
+
semantics are byte-identical; this is a drift-proofing refactor, not a
|
|
15
|
+
behavior change.
|
|
16
|
+
- 6c8b36b: Run the config-secrets backfill in afterPluginsReady instead of init.
|
|
17
|
+
Health-check strategies contributed by other plugins register during THEIR
|
|
18
|
+
init, and plugin init order follows the service-ref graph, so running the
|
|
19
|
+
backfill during healthcheck's own init could scan configurations before a
|
|
20
|
+
contributor (e.g. logstream's health strategy) had registered - skipping
|
|
21
|
+
that strategy's config with a "strategy not registered" warning at boot.
|
|
22
|
+
Only afterPluginsReady guarantees a complete registry. The backfill is
|
|
23
|
+
idempotent, so any configuration skipped by an earlier boot is picked up
|
|
24
|
+
on the next one.
|
|
25
|
+
- 6c8b36b: Promote the t-digest percentile helpers from healthcheck-backend into
|
|
26
|
+
backend-api (`createTDigest`, `serializeTDigest`, `deserializeTDigest`,
|
|
27
|
+
`mergeTDigestStates`, `percentileFromState`, ...), so any plugin can maintain
|
|
28
|
+
mergeable percentile sketches; tracestream's per-operation p95 buckets are the
|
|
29
|
+
first new consumer. healthcheck-backend now imports the shared module (the
|
|
30
|
+
local copy is removed, no behavior change).
|
|
31
|
+
- Updated dependencies [6c8b36b]
|
|
32
|
+
- Updated dependencies [6c8b36b]
|
|
33
|
+
- Updated dependencies [6c8b36b]
|
|
34
|
+
- Updated dependencies [6c8b36b]
|
|
35
|
+
- Updated dependencies [6c8b36b]
|
|
36
|
+
- Updated dependencies [6c8b36b]
|
|
37
|
+
- Updated dependencies [6c8b36b]
|
|
38
|
+
- Updated dependencies [6c8b36b]
|
|
39
|
+
- Updated dependencies [6c8b36b]
|
|
40
|
+
- Updated dependencies [6c8b36b]
|
|
41
|
+
- Updated dependencies [6c8b36b]
|
|
42
|
+
- Updated dependencies [6c8b36b]
|
|
43
|
+
- Updated dependencies [6c8b36b]
|
|
44
|
+
- Updated dependencies [6c8b36b]
|
|
45
|
+
- Updated dependencies [6c8b36b]
|
|
46
|
+
- @checkstack/ai-backend@0.11.3
|
|
47
|
+
- @checkstack/backend-api@0.34.0
|
|
48
|
+
- @checkstack/healthcheck-common@1.18.0
|
|
49
|
+
- @checkstack/catalog-common@2.8.0
|
|
50
|
+
- @checkstack/queue-api@0.4.0
|
|
51
|
+
- @checkstack/common@0.23.0
|
|
52
|
+
- @checkstack/catalog-backend@1.10.0
|
|
53
|
+
- @checkstack/status-page-backend@0.6.5
|
|
54
|
+
- @checkstack/automation-backend@0.11.7
|
|
55
|
+
- @checkstack/incident-backend@1.13.5
|
|
56
|
+
- @checkstack/script-packages-backend@0.4.5
|
|
57
|
+
- @checkstack/sdk@0.133.1
|
|
58
|
+
- @checkstack/command-backend@0.2.26
|
|
59
|
+
- @checkstack/gitops-backend@0.5.26
|
|
60
|
+
- @checkstack/satellite-backend@0.9.3
|
|
61
|
+
- @checkstack/secrets-backend@0.3.8
|
|
62
|
+
- @checkstack/incident-common@1.10.4
|
|
63
|
+
- @checkstack/maintenance-common@1.10.4
|
|
64
|
+
- @checkstack/status-page-common@0.6.4
|
|
65
|
+
- @checkstack/ai-common@0.6.7
|
|
66
|
+
- @checkstack/cache-api@0.3.20
|
|
67
|
+
- @checkstack/gitops-common@0.7.4
|
|
68
|
+
- @checkstack/notification-common@1.7.2
|
|
69
|
+
- @checkstack/secrets-common@0.3.3
|
|
70
|
+
- @checkstack/signal-common@0.3.1
|
|
71
|
+
- @checkstack/cache-utils@0.3.1
|
|
72
|
+
|
|
3
73
|
## 1.21.2
|
|
4
74
|
|
|
5
75
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-backend",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.3",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -14,50 +14,48 @@
|
|
|
14
14
|
"lint:code": "eslint . --max-warnings 0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/ai-backend": "0.11.
|
|
18
|
-
"@checkstack/ai-common": "0.6.
|
|
19
|
-
"@checkstack/automation-backend": "0.11.
|
|
20
|
-
"@checkstack/backend-api": "0.
|
|
21
|
-
"@checkstack/cache-api": "0.3.
|
|
22
|
-
"@checkstack/cache-utils": "0.3.
|
|
23
|
-
"@checkstack/catalog-backend": "1.
|
|
24
|
-
"@checkstack/catalog-common": "2.
|
|
25
|
-
"@checkstack/command-backend": "0.2.
|
|
26
|
-
"@checkstack/common": "0.
|
|
27
|
-
"@checkstack/gitops-backend": "0.5.
|
|
28
|
-
"@checkstack/gitops-common": "0.7.
|
|
29
|
-
"@checkstack/healthcheck-common": "1.
|
|
30
|
-
"@checkstack/incident-backend": "1.13.
|
|
31
|
-
"@checkstack/incident-common": "1.10.
|
|
32
|
-
"@checkstack/maintenance-common": "1.10.
|
|
33
|
-
"@checkstack/notification-common": "1.7.
|
|
34
|
-
"@checkstack/queue-api": "0.
|
|
35
|
-
"@checkstack/satellite-backend": "0.9.
|
|
36
|
-
"@checkstack/script-packages-backend": "0.4.
|
|
37
|
-
"@checkstack/sdk": "0.
|
|
38
|
-
"@checkstack/secrets-backend": "0.3.
|
|
39
|
-
"@checkstack/secrets-common": "0.3.
|
|
40
|
-
"@checkstack/signal-common": "0.3.
|
|
41
|
-
"@checkstack/status-page-backend": "0.6.
|
|
42
|
-
"@checkstack/status-page-common": "0.6.
|
|
17
|
+
"@checkstack/ai-backend": "0.11.3",
|
|
18
|
+
"@checkstack/ai-common": "0.6.7",
|
|
19
|
+
"@checkstack/automation-backend": "0.11.7",
|
|
20
|
+
"@checkstack/backend-api": "0.34.0",
|
|
21
|
+
"@checkstack/cache-api": "0.3.20",
|
|
22
|
+
"@checkstack/cache-utils": "0.3.1",
|
|
23
|
+
"@checkstack/catalog-backend": "1.10.0",
|
|
24
|
+
"@checkstack/catalog-common": "2.8.0",
|
|
25
|
+
"@checkstack/command-backend": "0.2.26",
|
|
26
|
+
"@checkstack/common": "0.23.0",
|
|
27
|
+
"@checkstack/gitops-backend": "0.5.26",
|
|
28
|
+
"@checkstack/gitops-common": "0.7.4",
|
|
29
|
+
"@checkstack/healthcheck-common": "1.18.0",
|
|
30
|
+
"@checkstack/incident-backend": "1.13.5",
|
|
31
|
+
"@checkstack/incident-common": "1.10.4",
|
|
32
|
+
"@checkstack/maintenance-common": "1.10.4",
|
|
33
|
+
"@checkstack/notification-common": "1.7.2",
|
|
34
|
+
"@checkstack/queue-api": "0.4.0",
|
|
35
|
+
"@checkstack/satellite-backend": "0.9.3",
|
|
36
|
+
"@checkstack/script-packages-backend": "0.4.5",
|
|
37
|
+
"@checkstack/sdk": "0.133.1",
|
|
38
|
+
"@checkstack/secrets-backend": "0.3.8",
|
|
39
|
+
"@checkstack/secrets-common": "0.3.3",
|
|
40
|
+
"@checkstack/signal-common": "0.3.1",
|
|
41
|
+
"@checkstack/status-page-backend": "0.6.5",
|
|
42
|
+
"@checkstack/status-page-common": "0.6.4",
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@orpc/contract": "^1.14.4",
|
|
45
45
|
"@orpc/server": "^1.14.4",
|
|
46
46
|
"drizzle-orm": "^0.45.0",
|
|
47
47
|
"hono": "^4.12.25",
|
|
48
48
|
"jsonpath-plus": "^10.4.0",
|
|
49
|
-
"tdigest": "^0.1.2",
|
|
50
49
|
"yaml": "^2.6.1",
|
|
51
50
|
"zod": "^4.2.1"
|
|
52
51
|
},
|
|
53
52
|
"devDependencies": {
|
|
54
53
|
"@checkstack/drizzle-helper": "0.0.6",
|
|
55
|
-
"@checkstack/scripts": "0.7.
|
|
56
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
54
|
+
"@checkstack/scripts": "0.7.6",
|
|
55
|
+
"@checkstack/test-utils-backend": "0.1.60",
|
|
57
56
|
"@checkstack/tsconfig": "0.0.7",
|
|
58
57
|
"@types/bun": "^1.0.0",
|
|
59
58
|
"@types/pg": "^8.20.0",
|
|
60
|
-
"@types/tdigest": "^0.1.5",
|
|
61
59
|
"bullmq": "^5.66.4",
|
|
62
60
|
"date-fns": "^4.4.0",
|
|
63
61
|
"drizzle-kit": "^0.31.10",
|
package/src/index.ts
CHANGED
|
@@ -287,20 +287,12 @@ export default createBackendPlugin({
|
|
|
287
287
|
};
|
|
288
288
|
gitopsConfigSecrets = configSecrets;
|
|
289
289
|
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
collectorRegistry,
|
|
297
|
-
internalSecrets,
|
|
298
|
-
advisoryLock,
|
|
299
|
-
logger,
|
|
300
|
-
});
|
|
301
|
-
} catch (error) {
|
|
302
|
-
logger.warn("Config-secrets backfill failed; continuing boot", error);
|
|
303
|
-
}
|
|
290
|
+
// NOTE: the config-secrets backfill runs in afterPluginsReady, NOT
|
|
291
|
+
// here. Strategies contributed by OTHER plugins (e.g. logstream's
|
|
292
|
+
// LogStreamHealthStrategy) register during THEIR init(), and init
|
|
293
|
+
// order follows the service-ref graph - it is NOT guaranteed that
|
|
294
|
+
// every contributor initialized before this plugin. Backfilling here
|
|
295
|
+
// would skip configs of not-yet-registered strategies with a warn.
|
|
304
296
|
|
|
305
297
|
// Resolve/search health-check configurations by name for the Teams admin
|
|
306
298
|
// UI (team grants are stored as opaque `<type>:<configId>` rows, where
|
|
@@ -644,6 +636,30 @@ export default createBackendPlugin({
|
|
|
644
636
|
// Store emitHook for the queue worker (Closure-based Hook Getter pattern)
|
|
645
637
|
storedEmitHook = emitHook;
|
|
646
638
|
|
|
639
|
+
// Move any pre-channel inline secrets out of stored rows (idempotent,
|
|
640
|
+
// advisory-locked, fail-open: a backfill failure must not block boot).
|
|
641
|
+
// Runs HERE - after every plugin's init() - because strategies are
|
|
642
|
+
// contributed by other plugins during their init (e.g. logstream's
|
|
643
|
+
// health strategy), and only afterPluginsReady guarantees the registry
|
|
644
|
+
// is complete; running in init raced contributor init order.
|
|
645
|
+
if (gitopsConfigSecrets && resolvedAdvisoryLock) {
|
|
646
|
+
try {
|
|
647
|
+
await backfillConfigSecrets({
|
|
648
|
+
db: database,
|
|
649
|
+
registry: healthCheckRegistry,
|
|
650
|
+
collectorRegistry,
|
|
651
|
+
internalSecrets: gitopsConfigSecrets.internalSecrets,
|
|
652
|
+
advisoryLock: resolvedAdvisoryLock,
|
|
653
|
+
logger,
|
|
654
|
+
});
|
|
655
|
+
} catch (error) {
|
|
656
|
+
logger.warn(
|
|
657
|
+
"Config-secrets backfill failed; continuing boot",
|
|
658
|
+
error,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
647
663
|
// No cross-pod status-cache broadcast: the status cache runs on the
|
|
648
664
|
// platform CacheManager, so a distributed backend (Redis) makes every
|
|
649
665
|
// eviction visible to all pods through the shared store. The prior
|
package/src/queue-executor.ts
CHANGED
|
@@ -32,6 +32,15 @@ import {
|
|
|
32
32
|
ENVIRONMENT_RESOLUTION_FAILED,
|
|
33
33
|
type HealthCheckStatus,
|
|
34
34
|
stripEphemeralFields,
|
|
35
|
+
HEALTH_CHECK_QUEUE,
|
|
36
|
+
type HealthCheckJobPayload,
|
|
37
|
+
} from "@checkstack/healthcheck-common";
|
|
38
|
+
// The run-queue contract (queue name + payload) now lives in the common leaf so
|
|
39
|
+
// every fast-path enqueuer shares one shape; re-exported from the owner here
|
|
40
|
+
// because this file's many internal importers reference these names.
|
|
41
|
+
export {
|
|
42
|
+
HEALTH_CHECK_QUEUE,
|
|
43
|
+
type HealthCheckJobPayload,
|
|
35
44
|
} from "@checkstack/healthcheck-common";
|
|
36
45
|
import {
|
|
37
46
|
CatalogApi,
|
|
@@ -244,12 +253,6 @@ async function emitCheckCompletedHook({
|
|
|
244
253
|
* The scheduling reconciler owns which (config, system, env) jobs exist; the
|
|
245
254
|
* `run_now` action enqueues one job per effective environment.
|
|
246
255
|
*/
|
|
247
|
-
export interface HealthCheckJobPayload {
|
|
248
|
-
configId: string;
|
|
249
|
-
systemId: string;
|
|
250
|
-
environmentId: string | null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
256
|
/** Prefix every health-check recurring jobId shares (used for orphan scans). */
|
|
254
257
|
export const HEALTH_CHECK_JOB_PREFIX = "healthcheck:";
|
|
255
258
|
|
|
@@ -268,13 +271,6 @@ export function encodeHealthCheckJobId(props: {
|
|
|
268
271
|
return environmentId === null ? base : `${base}:${environmentId}`;
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
/**
|
|
272
|
-
* Queue name for health check execution. Exported so consumers like
|
|
273
|
-
* the `healthcheck.run_now` automation action can enqueue a one-off
|
|
274
|
-
* job without re-importing the recurring-job factory.
|
|
275
|
-
*/
|
|
276
|
-
export const HEALTH_CHECK_QUEUE = "health-checks";
|
|
277
|
-
|
|
278
274
|
/**
|
|
279
275
|
* Worker group for health check execution (work-queue mode)
|
|
280
276
|
*/
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
|
+
import { getHourBucketStart, incrementHourlyAggregate } from "./realtime-aggregation";
|
|
2
3
|
import {
|
|
3
|
-
getHourBucketStart,
|
|
4
4
|
serializeTDigest,
|
|
5
5
|
deserializeTDigest,
|
|
6
|
-
|
|
7
|
-
} from "
|
|
6
|
+
createTDigest,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
8
|
import { computeAssertionKey } from "@checkstack/healthcheck-common";
|
|
9
|
-
import { TDigest } from "tdigest";
|
|
10
9
|
|
|
11
10
|
describe("getHourBucketStart", () => {
|
|
12
11
|
it("floors timestamp to the hour", () => {
|
|
@@ -42,7 +41,7 @@ describe("getHourBucketStart", () => {
|
|
|
42
41
|
|
|
43
42
|
describe("t-digest serialization", () => {
|
|
44
43
|
it("serializes and deserializes empty t-digest", () => {
|
|
45
|
-
const original =
|
|
44
|
+
const original = createTDigest();
|
|
46
45
|
const serialized = serializeTDigest(original);
|
|
47
46
|
const restored = deserializeTDigest(serialized);
|
|
48
47
|
|
|
@@ -52,7 +51,7 @@ describe("t-digest serialization", () => {
|
|
|
52
51
|
});
|
|
53
52
|
|
|
54
53
|
it("serializes and deserializes t-digest with values", () => {
|
|
55
|
-
const original =
|
|
54
|
+
const original = createTDigest();
|
|
56
55
|
original.push(100);
|
|
57
56
|
original.push(200);
|
|
58
57
|
original.push(300);
|
|
@@ -67,7 +66,7 @@ describe("t-digest serialization", () => {
|
|
|
67
66
|
});
|
|
68
67
|
|
|
69
68
|
it("preserves p95 accuracy after serialization", () => {
|
|
70
|
-
const original =
|
|
69
|
+
const original = createTDigest();
|
|
71
70
|
// Add 100 values from 1 to 100
|
|
72
71
|
for (let i = 1; i <= 100; i++) {
|
|
73
72
|
original.push(i);
|
|
@@ -85,7 +84,7 @@ describe("t-digest serialization", () => {
|
|
|
85
84
|
|
|
86
85
|
it("handles incremental updates correctly", () => {
|
|
87
86
|
// First batch
|
|
88
|
-
const digest1 =
|
|
87
|
+
const digest1 = createTDigest();
|
|
89
88
|
digest1.push(100);
|
|
90
89
|
digest1.push(110);
|
|
91
90
|
|
|
@@ -254,7 +253,7 @@ describe("incrementHourlyAggregate", () => {
|
|
|
254
253
|
it("updates min/max when existing aggregate has values", async () => {
|
|
255
254
|
// Set up existing aggregate
|
|
256
255
|
existingAggregate = {
|
|
257
|
-
tdigestState: serializeTDigest(
|
|
256
|
+
tdigestState: serializeTDigest(createTDigest()),
|
|
258
257
|
minLatencyMs: 100,
|
|
259
258
|
maxLatencyMs: 200,
|
|
260
259
|
};
|
|
@@ -309,7 +308,7 @@ describe("incrementHourlyAggregate", () => {
|
|
|
309
308
|
|
|
310
309
|
it("updates max when new latency is higher", async () => {
|
|
311
310
|
existingAggregate = {
|
|
312
|
-
tdigestState: serializeTDigest(
|
|
311
|
+
tdigestState: serializeTDigest(createTDigest()),
|
|
313
312
|
minLatencyMs: 100,
|
|
314
313
|
maxLatencyMs: 200,
|
|
315
314
|
};
|
|
@@ -361,7 +360,7 @@ describe("incrementHourlyAggregate", () => {
|
|
|
361
360
|
|
|
362
361
|
it("accumulates t-digest state across multiple runs", async () => {
|
|
363
362
|
// First run
|
|
364
|
-
const digest1 =
|
|
363
|
+
const digest1 = createTDigest();
|
|
365
364
|
digest1.push(100);
|
|
366
365
|
digest1.push(200);
|
|
367
366
|
digest1.push(300);
|
|
@@ -2,6 +2,10 @@ import type {
|
|
|
2
2
|
ScopedQueryRunner,
|
|
3
3
|
CollectorRegistry,
|
|
4
4
|
} from "@checkstack/backend-api";
|
|
5
|
+
import {
|
|
6
|
+
pushValuesIntoState,
|
|
7
|
+
percentileFromState,
|
|
8
|
+
} from "@checkstack/backend-api";
|
|
5
9
|
import {
|
|
6
10
|
ASSERTIONS_AGG_KEY,
|
|
7
11
|
AssertionOutcomeSchema,
|
|
@@ -9,7 +13,6 @@ import {
|
|
|
9
13
|
readAssertionStats,
|
|
10
14
|
type AssertionOutcome,
|
|
11
15
|
} from "@checkstack/healthcheck-common";
|
|
12
|
-
import { TDigest } from "tdigest";
|
|
13
16
|
import * as schema from "./schema";
|
|
14
17
|
import { healthCheckAggregates } from "./schema";
|
|
15
18
|
import { eq, and, sql } from "drizzle-orm";
|
|
@@ -30,46 +33,6 @@ export function getHourBucketStart(timestamp: Date): Date {
|
|
|
30
33
|
return bucketStart;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
/**
|
|
34
|
-
* Serialize a t-digest to an array of numbers for storage.
|
|
35
|
-
* Format: [centroid1_mean, centroid1_n, centroid2_mean, centroid2_n, ...]
|
|
36
|
-
*/
|
|
37
|
-
export function serializeTDigest(digest: TDigest): number[] {
|
|
38
|
-
const centroids = digest.toArray();
|
|
39
|
-
const result: number[] = [];
|
|
40
|
-
for (const c of centroids) {
|
|
41
|
-
result.push(c.mean, c.n);
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Deserialize a t-digest from storage format.
|
|
48
|
-
*/
|
|
49
|
-
export function deserializeTDigest(state: number[]): TDigest {
|
|
50
|
-
const digest = new TDigest();
|
|
51
|
-
|
|
52
|
-
if (state.length === 0) {
|
|
53
|
-
return digest;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Reconstruct centroids from pairs of [mean, n]
|
|
57
|
-
const centroids: Array<{ mean: number; n: number }> = [];
|
|
58
|
-
for (let i = 0; i < state.length; i += 2) {
|
|
59
|
-
const mean = state[i];
|
|
60
|
-
const n = state[i + 1];
|
|
61
|
-
if (mean !== undefined && n !== undefined && n > 0) {
|
|
62
|
-
centroids.push({ mean, n });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (centroids.length > 0) {
|
|
67
|
-
digest.push_centroid(centroids);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return digest;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
36
|
interface IncrementHourlyAggregateParams {
|
|
74
37
|
db: Db;
|
|
75
38
|
systemId: string;
|
|
@@ -177,15 +140,14 @@ export async function incrementHourlyAggregate(
|
|
|
177
140
|
max = latencyMs;
|
|
178
141
|
}
|
|
179
142
|
|
|
180
|
-
// Update t-digest for p95 calculation
|
|
181
|
-
const
|
|
182
|
-
existing?.tdigestState
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const p95 = Math.round(digest.percentile(0.95));
|
|
143
|
+
// Update t-digest for p95 calculation (shared backend-api helper).
|
|
144
|
+
const tdigestState = pushValuesIntoState({
|
|
145
|
+
state: existing?.tdigestState ?? null,
|
|
146
|
+
values: [latencyMs],
|
|
147
|
+
});
|
|
148
|
+
const p95 = Math.round(
|
|
149
|
+
percentileFromState({ state: tdigestState, q: 0.95 }) ?? latencyMs,
|
|
150
|
+
);
|
|
189
151
|
|
|
190
152
|
return { latencySumIncr, min, max, tdigestState, p95 };
|
|
191
153
|
})();
|