@checkstack/healthcheck-backend 0.5.0 → 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 +80 -0
- package/drizzle/0008_broad_black_tom.sql +1 -0
- package/drizzle/meta/0008_snapshot.json +420 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +2 -1
- package/src/aggregation-utils.test.ts +65 -0
- package/src/aggregation-utils.ts +111 -29
- package/src/aggregation.test.ts +382 -0
- package/src/index.ts +5 -0
- package/src/queue-executor.test.ts +133 -0
- package/src/queue-executor.ts +40 -1
- package/src/router.ts +12 -0
- package/src/schema.ts +2 -0
- package/src/service-ordering.test.ts +316 -0
- package/src/service-pause.test.ts +50 -0
- package/src/service.ts +67 -13
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
|
+
import { HealthCheckService } from "./service";
|
|
3
|
+
import { createMockDb } from "@checkstack/test-utils-backend";
|
|
4
|
+
|
|
5
|
+
describe("HealthCheckService - pause/resume", () => {
|
|
6
|
+
let mockDb: ReturnType<typeof createMockDb>;
|
|
7
|
+
let service: HealthCheckService;
|
|
8
|
+
let mockUpdate: ReturnType<typeof mock>;
|
|
9
|
+
let mockSet: ReturnType<typeof mock>;
|
|
10
|
+
let mockWhere: ReturnType<typeof mock>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockDb = createMockDb();
|
|
14
|
+
mockWhere = mock(() => Promise.resolve());
|
|
15
|
+
mockSet = mock(() => ({ where: mockWhere }));
|
|
16
|
+
mockUpdate = mock(() => ({ set: mockSet }));
|
|
17
|
+
(mockDb.update as any) = mockUpdate;
|
|
18
|
+
service = new HealthCheckService(mockDb as any);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("pauseConfiguration", () => {
|
|
22
|
+
it("should update paused to true and set updatedAt", async () => {
|
|
23
|
+
await service.pauseConfiguration("config-123");
|
|
24
|
+
|
|
25
|
+
expect(mockUpdate).toHaveBeenCalled();
|
|
26
|
+
expect(mockSet).toHaveBeenCalledWith(
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
paused: true,
|
|
29
|
+
updatedAt: expect.any(Date),
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
expect(mockWhere).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("resumeConfiguration", () => {
|
|
37
|
+
it("should update paused to false and set updatedAt", async () => {
|
|
38
|
+
await service.resumeConfiguration("config-456");
|
|
39
|
+
|
|
40
|
+
expect(mockUpdate).toHaveBeenCalled();
|
|
41
|
+
expect(mockSet).toHaveBeenCalledWith(
|
|
42
|
+
expect.objectContaining({
|
|
43
|
+
paused: false,
|
|
44
|
+
updatedAt: expect.any(Date),
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
expect(mockWhere).toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/service.ts
CHANGED
|
@@ -105,6 +105,20 @@ export class HealthCheckService {
|
|
|
105
105
|
.where(eq(healthCheckConfigurations.id, id));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
async pauseConfiguration(id: string): Promise<void> {
|
|
109
|
+
await this.db
|
|
110
|
+
.update(healthCheckConfigurations)
|
|
111
|
+
.set({ paused: true, updatedAt: new Date() })
|
|
112
|
+
.where(eq(healthCheckConfigurations.id, id));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async resumeConfiguration(id: string): Promise<void> {
|
|
116
|
+
await this.db
|
|
117
|
+
.update(healthCheckConfigurations)
|
|
118
|
+
.set({ paused: false, updatedAt: new Date() })
|
|
119
|
+
.where(eq(healthCheckConfigurations.id, id));
|
|
120
|
+
}
|
|
121
|
+
|
|
108
122
|
async getConfigurations(): Promise<HealthCheckConfiguration[]> {
|
|
109
123
|
const configs = await this.db.select().from(healthCheckConfigurations);
|
|
110
124
|
return configs.map((c) => this.mapConfig(c));
|
|
@@ -400,7 +414,7 @@ export class HealthCheckService {
|
|
|
400
414
|
const sparklineLimit = 25;
|
|
401
415
|
|
|
402
416
|
for (const assoc of associations) {
|
|
403
|
-
// Get last 25 runs for sparkline
|
|
417
|
+
// Get last 25 runs for sparkline (newest first, then reverse for chronological display)
|
|
404
418
|
const runs = await this.db
|
|
405
419
|
.select({
|
|
406
420
|
id: healthCheckRuns.id,
|
|
@@ -417,15 +431,18 @@ export class HealthCheckService {
|
|
|
417
431
|
.orderBy(desc(healthCheckRuns.timestamp))
|
|
418
432
|
.limit(sparklineLimit);
|
|
419
433
|
|
|
434
|
+
// Reverse to chronological order (oldest first) for sparkline display
|
|
435
|
+
const chronologicalRuns = runs.toReversed();
|
|
436
|
+
|
|
420
437
|
// Migrate and extract thresholds
|
|
421
438
|
let thresholds: StateThresholds | undefined;
|
|
422
439
|
if (assoc.stateThresholds) {
|
|
423
440
|
thresholds = await stateThresholds.parse(assoc.stateThresholds);
|
|
424
441
|
}
|
|
425
442
|
|
|
426
|
-
// Evaluate current status
|
|
443
|
+
// Evaluate current status (runs are in DESC order - newest first - as evaluateHealthStatus expects)
|
|
427
444
|
const status = evaluateHealthStatus({
|
|
428
|
-
runs
|
|
445
|
+
runs,
|
|
429
446
|
thresholds,
|
|
430
447
|
});
|
|
431
448
|
|
|
@@ -437,7 +454,7 @@ export class HealthCheckService {
|
|
|
437
454
|
enabled: assoc.enabled,
|
|
438
455
|
status,
|
|
439
456
|
stateThresholds: thresholds,
|
|
440
|
-
recentRuns:
|
|
457
|
+
recentRuns: chronologicalRuns.map((r) => ({
|
|
441
458
|
id: r.id,
|
|
442
459
|
status: r.status,
|
|
443
460
|
timestamp: r.timestamp,
|
|
@@ -450,6 +467,7 @@ export class HealthCheckService {
|
|
|
450
467
|
|
|
451
468
|
/**
|
|
452
469
|
* Get paginated health check run history (public - no result data).
|
|
470
|
+
* @param sortOrder - 'asc' for chronological (oldest first), 'desc' for reverse (newest first)
|
|
453
471
|
*/
|
|
454
472
|
async getHistory(props: {
|
|
455
473
|
systemId?: string;
|
|
@@ -458,6 +476,7 @@ export class HealthCheckService {
|
|
|
458
476
|
endDate?: Date;
|
|
459
477
|
limit?: number;
|
|
460
478
|
offset?: number;
|
|
479
|
+
sortOrder: "asc" | "desc";
|
|
461
480
|
}) {
|
|
462
481
|
const {
|
|
463
482
|
systemId,
|
|
@@ -466,6 +485,7 @@ export class HealthCheckService {
|
|
|
466
485
|
endDate,
|
|
467
486
|
limit = 10,
|
|
468
487
|
offset = 0,
|
|
488
|
+
sortOrder,
|
|
469
489
|
} = props;
|
|
470
490
|
|
|
471
491
|
const conditions = [];
|
|
@@ -481,16 +501,17 @@ export class HealthCheckService {
|
|
|
481
501
|
// Get total count using drizzle $count
|
|
482
502
|
const total = await this.db.$count(healthCheckRuns, whereClause);
|
|
483
503
|
|
|
484
|
-
// Get paginated runs
|
|
504
|
+
// Get paginated runs with requested sort order
|
|
485
505
|
let query = this.db.select().from(healthCheckRuns);
|
|
486
506
|
if (whereClause) {
|
|
487
507
|
// @ts-expect-error drizzle-orm type mismatch
|
|
488
508
|
query = query.where(whereClause);
|
|
489
509
|
}
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
510
|
+
const orderColumn =
|
|
511
|
+
sortOrder === "desc"
|
|
512
|
+
? desc(healthCheckRuns.timestamp)
|
|
513
|
+
: healthCheckRuns.timestamp;
|
|
514
|
+
const runs = await query.orderBy(orderColumn).limit(limit).offset(offset);
|
|
494
515
|
|
|
495
516
|
// Return without result field for public access (latencyMs is public data)
|
|
496
517
|
return {
|
|
@@ -509,6 +530,7 @@ export class HealthCheckService {
|
|
|
509
530
|
/**
|
|
510
531
|
* Get detailed health check run history with full result data.
|
|
511
532
|
* Restricted to users with manage access.
|
|
533
|
+
* @param sortOrder - 'asc' for chronological (oldest first), 'desc' for reverse (newest first)
|
|
512
534
|
*/
|
|
513
535
|
async getDetailedHistory(props: {
|
|
514
536
|
systemId?: string;
|
|
@@ -517,6 +539,7 @@ export class HealthCheckService {
|
|
|
517
539
|
endDate?: Date;
|
|
518
540
|
limit?: number;
|
|
519
541
|
offset?: number;
|
|
542
|
+
sortOrder: "asc" | "desc";
|
|
520
543
|
}) {
|
|
521
544
|
const {
|
|
522
545
|
systemId,
|
|
@@ -525,6 +548,7 @@ export class HealthCheckService {
|
|
|
525
548
|
endDate,
|
|
526
549
|
limit = 10,
|
|
527
550
|
offset = 0,
|
|
551
|
+
sortOrder,
|
|
528
552
|
} = props;
|
|
529
553
|
|
|
530
554
|
const conditions = [];
|
|
@@ -542,10 +566,11 @@ export class HealthCheckService {
|
|
|
542
566
|
// @ts-expect-error drizzle-orm type mismatch
|
|
543
567
|
query = query.where(whereClause);
|
|
544
568
|
}
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
569
|
+
const orderColumn =
|
|
570
|
+
sortOrder === "desc"
|
|
571
|
+
? desc(healthCheckRuns.timestamp)
|
|
572
|
+
: healthCheckRuns.timestamp;
|
|
573
|
+
const runs = await query.orderBy(orderColumn).limit(limit).offset(offset);
|
|
549
574
|
|
|
550
575
|
// Return with full result data for manage access
|
|
551
576
|
return {
|
|
@@ -562,6 +587,32 @@ export class HealthCheckService {
|
|
|
562
587
|
};
|
|
563
588
|
}
|
|
564
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Get a single health check run by its ID.
|
|
592
|
+
*/
|
|
593
|
+
async getRunById(props: { runId: string }) {
|
|
594
|
+
const run = await this.db
|
|
595
|
+
.select()
|
|
596
|
+
.from(healthCheckRuns)
|
|
597
|
+
.where(eq(healthCheckRuns.id, props.runId))
|
|
598
|
+
.limit(1);
|
|
599
|
+
|
|
600
|
+
if (run.length === 0) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const r = run[0];
|
|
605
|
+
return {
|
|
606
|
+
id: r.id,
|
|
607
|
+
configurationId: r.configurationId,
|
|
608
|
+
systemId: r.systemId,
|
|
609
|
+
status: r.status,
|
|
610
|
+
result: r.result ?? {},
|
|
611
|
+
timestamp: r.timestamp,
|
|
612
|
+
latencyMs: r.latencyMs ?? undefined,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
565
616
|
/**
|
|
566
617
|
* Get aggregated health check history with dynamically-sized buckets.
|
|
567
618
|
* Queries all three tiers (raw, hourly, daily) and merges with priority.
|
|
@@ -704,6 +755,7 @@ export class HealthCheckService {
|
|
|
704
755
|
sourceBuckets: mergedBuckets,
|
|
705
756
|
targetIntervalMs: bucketIntervalMs,
|
|
706
757
|
rangeStart: startDate,
|
|
758
|
+
rangeEnd: endDate,
|
|
707
759
|
});
|
|
708
760
|
|
|
709
761
|
// Convert to output format
|
|
@@ -717,6 +769,7 @@ export class HealthCheckService {
|
|
|
717
769
|
|
|
718
770
|
const baseBucket = {
|
|
719
771
|
bucketStart: bucket.bucketStart,
|
|
772
|
+
bucketEnd: new Date(bucket.bucketEndMs),
|
|
720
773
|
bucketIntervalSeconds,
|
|
721
774
|
runCount: bucket.runCount,
|
|
722
775
|
healthyCount: bucket.healthyCount,
|
|
@@ -884,6 +937,7 @@ export class HealthCheckService {
|
|
|
884
937
|
config: row.config,
|
|
885
938
|
collectors: row.collectors ?? undefined,
|
|
886
939
|
intervalSeconds: row.intervalSeconds,
|
|
940
|
+
paused: row.paused,
|
|
887
941
|
createdAt: row.createdAt,
|
|
888
942
|
updatedAt: row.updatedAt,
|
|
889
943
|
};
|