@beignet/core 0.0.21 → 0.0.23
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 +12 -0
- package/README.md +26 -0
- package/dist/ports/audit.d.ts +1 -1
- package/dist/ports/audit.js +1 -1
- package/dist/server/audit-context.d.ts +4 -1
- package/dist/server/audit-context.d.ts.map +1 -1
- package/dist/server/audit-context.js +4 -1
- package/dist/server/audit-context.js.map +1 -1
- package/dist/server/health.d.ts +61 -4
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +84 -0
- package/dist/server/health.js.map +1 -1
- package/package.json +1 -1
- package/src/ports/audit.ts +1 -1
- package/src/server/audit-context.ts +4 -1
- package/src/server/health.ts +181 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @beignet/core
|
|
2
2
|
|
|
3
|
+
## 0.0.23
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 02f09fb: Add Drizzle audit log setup statements and `AuditLogPort` factories for SQLite, Postgres, and MySQL.
|
|
8
|
+
|
|
9
|
+
## 0.0.22
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Add app-owned readiness helpers, first-party provider health checks, generated `/api/ready` routes, and doctor readiness hints.
|
|
14
|
+
|
|
3
15
|
## 0.0.21
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1269,6 +1269,32 @@ The public adapter contract is `HttpAdapter<NativeRequest, NativeResponse>`.
|
|
|
1269
1269
|
Use it when building a runtime package beyond the first-party `@beignet/web`
|
|
1270
1270
|
and `@beignet/next` adapters.
|
|
1271
1271
|
|
|
1272
|
+
### Health and readiness
|
|
1273
|
+
|
|
1274
|
+
Use `createHealthHandler(...)` and `runHealthChecks(...)` from
|
|
1275
|
+
`@beignet/core/server` for app-owned liveness and readiness endpoints.
|
|
1276
|
+
Readiness checks should be cheap, bounded, and non-mutating:
|
|
1277
|
+
|
|
1278
|
+
```ts
|
|
1279
|
+
import { createHealthHandler } from "@beignet/core/server";
|
|
1280
|
+
import { server } from "@/server";
|
|
1281
|
+
|
|
1282
|
+
const readiness = createHealthHandler(
|
|
1283
|
+
server.ports,
|
|
1284
|
+
{
|
|
1285
|
+
checks: {
|
|
1286
|
+
database: (ports) => ports.db.checkHealth(),
|
|
1287
|
+
},
|
|
1288
|
+
timeoutMs: 2000,
|
|
1289
|
+
},
|
|
1290
|
+
"production",
|
|
1291
|
+
);
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
Provider checks such as `ctx.ports.db.checkHealth()` should be called from
|
|
1295
|
+
routes, workers, or deployment probes. Do not run migrations, drains, workers,
|
|
1296
|
+
or polling loops from health checks.
|
|
1297
|
+
|
|
1272
1298
|
### Request instrumentation and tracing
|
|
1273
1299
|
|
|
1274
1300
|
`createServer(...)` owns request instrumentation. For every request it
|
package/dist/ports/audit.d.ts
CHANGED
|
@@ -345,7 +345,7 @@ export interface InstrumentedAuditLogOptions {
|
|
|
345
345
|
* @example
|
|
346
346
|
* ```ts
|
|
347
347
|
* const audit = createInstrumentedAuditLog({
|
|
348
|
-
* audit:
|
|
348
|
+
* audit: createDrizzleSqliteAuditLogPort(db),
|
|
349
349
|
* instrumentation: ports,
|
|
350
350
|
* });
|
|
351
351
|
* ```
|
package/dist/ports/audit.js
CHANGED
|
@@ -17,7 +17,10 @@ import type { AuditLogPort } from "../ports/index.js";
|
|
|
17
17
|
* @example
|
|
18
18
|
* ```ts
|
|
19
19
|
* const audit = createAmbientAuditLog(
|
|
20
|
-
* createInstrumentedAuditLog({
|
|
20
|
+
* createInstrumentedAuditLog({
|
|
21
|
+
* audit: createDrizzleSqliteAuditLogPort(db),
|
|
22
|
+
* instrumentation: ports,
|
|
23
|
+
* }),
|
|
21
24
|
* );
|
|
22
25
|
* await audit.record({ action: "posts.publish", resource: { type: "post", id } });
|
|
23
26
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-context.d.ts","sourceRoot":"","sources":["../../src/server/audit-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD
|
|
1
|
+
{"version":3,"file":"audit-context.d.ts","sourceRoot":"","sources":["../../src/server/audit-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAevE"}
|
|
@@ -17,7 +17,10 @@ import { getActiveRequestContext } from "./request-context.js";
|
|
|
17
17
|
* @example
|
|
18
18
|
* ```ts
|
|
19
19
|
* const audit = createAmbientAuditLog(
|
|
20
|
-
* createInstrumentedAuditLog({
|
|
20
|
+
* createInstrumentedAuditLog({
|
|
21
|
+
* audit: createDrizzleSqliteAuditLogPort(db),
|
|
22
|
+
* instrumentation: ports,
|
|
23
|
+
* }),
|
|
21
24
|
* );
|
|
22
25
|
* await audit.record({ action: "posts.publish", resource: { type: "post", id } });
|
|
23
26
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-context.js","sourceRoot":"","sources":["../../src/server/audit-context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D
|
|
1
|
+
{"version":3,"file":"audit-context.js","sourceRoot":"","sources":["../../src/server/audit-context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAmB;IACvD,OAAO;QACL,MAAM,CAAC,KAAK;YACV,MAAM,OAAO,GAAG,uBAAuB,EAAE,CAAC;YAC1C,IAAI,CAAC,OAAO;gBAAE,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAEzC,OAAO,KAAK,CAAC,MAAM,CAAC;gBAClB,GAAG,KAAK;gBACR,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK;gBACnC,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM;gBACtC,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS;gBAC/C,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO;aAC1C,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/server/health.d.ts
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AnyPorts } from "../ports/index.js";
|
|
6
6
|
import type { HttpRequestLike, HttpResponseLike } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Per-dependency health detail returned by readiness checks.
|
|
9
|
+
*/
|
|
10
|
+
export interface HealthCheckDetail {
|
|
11
|
+
/**
|
|
12
|
+
* Whether this dependency is healthy.
|
|
13
|
+
*/
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Optional human-readable status. Avoid secrets and raw provider credentials.
|
|
17
|
+
*/
|
|
18
|
+
message?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Milliseconds spent running this dependency check.
|
|
21
|
+
*/
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optional safe metadata for operators.
|
|
25
|
+
*/
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
7
28
|
/**
|
|
8
29
|
* Health check result returned by health handlers.
|
|
9
30
|
*/
|
|
@@ -15,10 +36,33 @@ export interface HealthCheckResult {
|
|
|
15
36
|
/**
|
|
16
37
|
* Optional per-dependency health details.
|
|
17
38
|
*/
|
|
18
|
-
details?: Record<string,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
details?: Record<string, HealthCheckDetail>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A named dependency check run by {@link runHealthChecks}.
|
|
43
|
+
*/
|
|
44
|
+
export type HealthCheck<Ports> = (ports: Ports) => Promise<HealthCheckDetail | boolean | undefined> | HealthCheckDetail | boolean | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Named dependency checks for an app-owned readiness endpoint.
|
|
47
|
+
*/
|
|
48
|
+
export type HealthChecks<Ports> = Record<string, HealthCheck<Ports>>;
|
|
49
|
+
/**
|
|
50
|
+
* Options for running named dependency checks.
|
|
51
|
+
*/
|
|
52
|
+
export interface RunHealthChecksOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Maximum time to wait for each dependency check.
|
|
55
|
+
*
|
|
56
|
+
* Defaults to 2000ms.
|
|
57
|
+
*/
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Include thrown error messages in dependency details.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to true. Set false in production responses when provider errors
|
|
63
|
+
* may include sensitive details.
|
|
64
|
+
*/
|
|
65
|
+
includeErrorDetails?: boolean;
|
|
22
66
|
}
|
|
23
67
|
/**
|
|
24
68
|
* Health check configuration.
|
|
@@ -34,11 +78,24 @@ export interface HealthConfig<Ports> {
|
|
|
34
78
|
suggestedPath?: string;
|
|
35
79
|
/** Custom health check function */
|
|
36
80
|
check?: (ports: Ports) => Promise<HealthCheckResult>;
|
|
81
|
+
/** Named dependency checks for an app-owned readiness endpoint */
|
|
82
|
+
checks?: HealthChecks<Ports>;
|
|
83
|
+
/** Per-check timeout for named dependency checks */
|
|
84
|
+
timeoutMs?: number;
|
|
37
85
|
}
|
|
38
86
|
/**
|
|
39
87
|
* Application environment.
|
|
40
88
|
*/
|
|
41
89
|
export type AppEnvironment = "development" | "production" | "test";
|
|
90
|
+
/**
|
|
91
|
+
* Run named dependency health checks in parallel and aggregate the result.
|
|
92
|
+
*
|
|
93
|
+
* This is intended for app-owned readiness endpoints. Checks should be cheap,
|
|
94
|
+
* bounded, non-mutating probes such as `SELECT 1`, Redis `PING`, or provider
|
|
95
|
+
* health endpoints. Do not start workers, drains, migrations, or polling loops
|
|
96
|
+
* from readiness checks.
|
|
97
|
+
*/
|
|
98
|
+
export declare function runHealthChecks<Ports extends AnyPorts>(ports: Ports, checks: HealthChecks<Ports>, options?: RunHealthChecksOptions): Promise<HealthCheckResult>;
|
|
42
99
|
/**
|
|
43
100
|
* Create a framework-neutral health check handler.
|
|
44
101
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IACZ;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IACZ;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IACZ;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,KAAK,IAAI,CAC/B,KAAK,EAAE,KAAK,KAEV,OAAO,CAAC,iBAAiB,GAAG,OAAO,GAAG,SAAS,CAAC,GAChD,iBAAiB,GACjB,OAAO,GACP,SAAS,CAAC;AAEd;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,KAAK;IACjC,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACrD,kEAAkE;IAClE,MAAM,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IAC7B,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,YAAY,GAAG,MAAM,CAAC;AAyDnE;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,KAAK,SAAS,QAAQ,EAC1D,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CA6C5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,QAAQ,EACxD,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,SAAS,EAC7C,GAAG,EAAE,cAAc,GAClB,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAyCrD"}
|
package/dist/server/health.js
CHANGED
|
@@ -2,6 +2,84 @@
|
|
|
2
2
|
* Health check handler
|
|
3
3
|
* Health check handler for Beignet server adapters.
|
|
4
4
|
*/
|
|
5
|
+
const DEFAULT_HEALTH_TIMEOUT_MS = 2000;
|
|
6
|
+
function errorMessage(error) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
function assertTimeout(timeoutMs) {
|
|
10
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
11
|
+
throw new Error("Health check timeoutMs must be a positive integer.");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function normalizeHealthDetail(result) {
|
|
15
|
+
if (typeof result === "boolean")
|
|
16
|
+
return { ok: result };
|
|
17
|
+
return result ?? { ok: true };
|
|
18
|
+
}
|
|
19
|
+
function redactHealthDetail(detail, includeErrorDetails) {
|
|
20
|
+
if (includeErrorDetails || detail.ok || detail.message === undefined) {
|
|
21
|
+
return detail;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
...detail,
|
|
25
|
+
message: "Health check failed",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function withTimeout(promise, timeoutMs, name) {
|
|
29
|
+
let timeout;
|
|
30
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
31
|
+
timeout = setTimeout(() => {
|
|
32
|
+
reject(new Error(`Health check "${name}" did not complete within ${timeoutMs}ms.`));
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
});
|
|
35
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
36
|
+
if (timeout)
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run named dependency health checks in parallel and aggregate the result.
|
|
42
|
+
*
|
|
43
|
+
* This is intended for app-owned readiness endpoints. Checks should be cheap,
|
|
44
|
+
* bounded, non-mutating probes such as `SELECT 1`, Redis `PING`, or provider
|
|
45
|
+
* health endpoints. Do not start workers, drains, migrations, or polling loops
|
|
46
|
+
* from readiness checks.
|
|
47
|
+
*/
|
|
48
|
+
export async function runHealthChecks(ports, checks, options = {}) {
|
|
49
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
|
|
50
|
+
assertTimeout(timeoutMs);
|
|
51
|
+
const includeErrorDetails = options.includeErrorDetails ?? true;
|
|
52
|
+
const entries = await Promise.all(Object.entries(checks).map(async ([name, check]) => {
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
try {
|
|
55
|
+
const detail = redactHealthDetail(normalizeHealthDetail(await withTimeout(Promise.resolve(check(ports)), timeoutMs, name)), includeErrorDetails);
|
|
56
|
+
return [
|
|
57
|
+
name,
|
|
58
|
+
{
|
|
59
|
+
...detail,
|
|
60
|
+
durationMs: Date.now() - startedAt,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return [
|
|
66
|
+
name,
|
|
67
|
+
{
|
|
68
|
+
ok: false,
|
|
69
|
+
message: includeErrorDetails
|
|
70
|
+
? errorMessage(error)
|
|
71
|
+
: "Health check failed",
|
|
72
|
+
durationMs: Date.now() - startedAt,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
const details = Object.fromEntries(entries);
|
|
78
|
+
return {
|
|
79
|
+
ok: Object.values(details).every((detail) => detail.ok),
|
|
80
|
+
details,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
5
83
|
/**
|
|
6
84
|
* Create a framework-neutral health check handler.
|
|
7
85
|
*
|
|
@@ -35,6 +113,12 @@ export function createHealthHandler(ports, healthConfig, env) {
|
|
|
35
113
|
};
|
|
36
114
|
}
|
|
37
115
|
}
|
|
116
|
+
else if (healthConfig?.checks) {
|
|
117
|
+
result = await runHealthChecks(ports, healthConfig.checks, {
|
|
118
|
+
timeoutMs: healthConfig.timeoutMs,
|
|
119
|
+
includeErrorDetails: env !== "production",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
38
122
|
else {
|
|
39
123
|
result = { ok: true };
|
|
40
124
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/ports/audit.ts
CHANGED
|
@@ -472,7 +472,7 @@ function auditSummary(entry: AuditLogEntry): string {
|
|
|
472
472
|
* @example
|
|
473
473
|
* ```ts
|
|
474
474
|
* const audit = createInstrumentedAuditLog({
|
|
475
|
-
* audit:
|
|
475
|
+
* audit: createDrizzleSqliteAuditLogPort(db),
|
|
476
476
|
* instrumentation: ports,
|
|
477
477
|
* });
|
|
478
478
|
* ```
|
|
@@ -19,7 +19,10 @@ import { getActiveRequestContext } from "./request-context.js";
|
|
|
19
19
|
* @example
|
|
20
20
|
* ```ts
|
|
21
21
|
* const audit = createAmbientAuditLog(
|
|
22
|
-
* createInstrumentedAuditLog({
|
|
22
|
+
* createInstrumentedAuditLog({
|
|
23
|
+
* audit: createDrizzleSqliteAuditLogPort(db),
|
|
24
|
+
* instrumentation: ports,
|
|
25
|
+
* }),
|
|
23
26
|
* );
|
|
24
27
|
* await audit.record({ action: "posts.publish", resource: { type: "post", id } });
|
|
25
28
|
* ```
|
package/src/server/health.ts
CHANGED
|
@@ -6,6 +6,28 @@
|
|
|
6
6
|
import type { AnyPorts } from "../ports/index.js";
|
|
7
7
|
import type { HttpRequestLike, HttpResponseLike } from "./types.js";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Per-dependency health detail returned by readiness checks.
|
|
11
|
+
*/
|
|
12
|
+
export interface HealthCheckDetail {
|
|
13
|
+
/**
|
|
14
|
+
* Whether this dependency is healthy.
|
|
15
|
+
*/
|
|
16
|
+
ok: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Optional human-readable status. Avoid secrets and raw provider credentials.
|
|
19
|
+
*/
|
|
20
|
+
message?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Milliseconds spent running this dependency check.
|
|
23
|
+
*/
|
|
24
|
+
durationMs?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Optional safe metadata for operators.
|
|
27
|
+
*/
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
/**
|
|
10
32
|
* Health check result returned by health handlers.
|
|
11
33
|
*/
|
|
@@ -17,7 +39,42 @@ export interface HealthCheckResult {
|
|
|
17
39
|
/**
|
|
18
40
|
* Optional per-dependency health details.
|
|
19
41
|
*/
|
|
20
|
-
details?: Record<string,
|
|
42
|
+
details?: Record<string, HealthCheckDetail>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A named dependency check run by {@link runHealthChecks}.
|
|
47
|
+
*/
|
|
48
|
+
export type HealthCheck<Ports> = (
|
|
49
|
+
ports: Ports,
|
|
50
|
+
) =>
|
|
51
|
+
| Promise<HealthCheckDetail | boolean | undefined>
|
|
52
|
+
| HealthCheckDetail
|
|
53
|
+
| boolean
|
|
54
|
+
| undefined;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Named dependency checks for an app-owned readiness endpoint.
|
|
58
|
+
*/
|
|
59
|
+
export type HealthChecks<Ports> = Record<string, HealthCheck<Ports>>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Options for running named dependency checks.
|
|
63
|
+
*/
|
|
64
|
+
export interface RunHealthChecksOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Maximum time to wait for each dependency check.
|
|
67
|
+
*
|
|
68
|
+
* Defaults to 2000ms.
|
|
69
|
+
*/
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Include thrown error messages in dependency details.
|
|
73
|
+
*
|
|
74
|
+
* Defaults to true. Set false in production responses when provider errors
|
|
75
|
+
* may include sensitive details.
|
|
76
|
+
*/
|
|
77
|
+
includeErrorDetails?: boolean;
|
|
21
78
|
}
|
|
22
79
|
|
|
23
80
|
/**
|
|
@@ -34,6 +91,10 @@ export interface HealthConfig<Ports> {
|
|
|
34
91
|
suggestedPath?: string;
|
|
35
92
|
/** Custom health check function */
|
|
36
93
|
check?: (ports: Ports) => Promise<HealthCheckResult>;
|
|
94
|
+
/** Named dependency checks for an app-owned readiness endpoint */
|
|
95
|
+
checks?: HealthChecks<Ports>;
|
|
96
|
+
/** Per-check timeout for named dependency checks */
|
|
97
|
+
timeoutMs?: number;
|
|
37
98
|
}
|
|
38
99
|
|
|
39
100
|
/**
|
|
@@ -41,6 +102,120 @@ export interface HealthConfig<Ports> {
|
|
|
41
102
|
*/
|
|
42
103
|
export type AppEnvironment = "development" | "production" | "test";
|
|
43
104
|
|
|
105
|
+
const DEFAULT_HEALTH_TIMEOUT_MS = 2000;
|
|
106
|
+
|
|
107
|
+
function errorMessage(error: unknown): string {
|
|
108
|
+
return error instanceof Error ? error.message : String(error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertTimeout(timeoutMs: number): void {
|
|
112
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
113
|
+
throw new Error("Health check timeoutMs must be a positive integer.");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeHealthDetail(
|
|
118
|
+
result: HealthCheckDetail | boolean | undefined,
|
|
119
|
+
): HealthCheckDetail {
|
|
120
|
+
if (typeof result === "boolean") return { ok: result };
|
|
121
|
+
return result ?? { ok: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function redactHealthDetail(
|
|
125
|
+
detail: HealthCheckDetail,
|
|
126
|
+
includeErrorDetails: boolean,
|
|
127
|
+
): HealthCheckDetail {
|
|
128
|
+
if (includeErrorDetails || detail.ok || detail.message === undefined) {
|
|
129
|
+
return detail;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...detail,
|
|
134
|
+
message: "Health check failed",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function withTimeout<T>(
|
|
139
|
+
promise: Promise<T>,
|
|
140
|
+
timeoutMs: number,
|
|
141
|
+
name: string,
|
|
142
|
+
): Promise<T> {
|
|
143
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
144
|
+
|
|
145
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
146
|
+
timeout = setTimeout(() => {
|
|
147
|
+
reject(
|
|
148
|
+
new Error(
|
|
149
|
+
`Health check "${name}" did not complete within ${timeoutMs}ms.`,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
156
|
+
if (timeout) clearTimeout(timeout);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Run named dependency health checks in parallel and aggregate the result.
|
|
162
|
+
*
|
|
163
|
+
* This is intended for app-owned readiness endpoints. Checks should be cheap,
|
|
164
|
+
* bounded, non-mutating probes such as `SELECT 1`, Redis `PING`, or provider
|
|
165
|
+
* health endpoints. Do not start workers, drains, migrations, or polling loops
|
|
166
|
+
* from readiness checks.
|
|
167
|
+
*/
|
|
168
|
+
export async function runHealthChecks<Ports extends AnyPorts>(
|
|
169
|
+
ports: Ports,
|
|
170
|
+
checks: HealthChecks<Ports>,
|
|
171
|
+
options: RunHealthChecksOptions = {},
|
|
172
|
+
): Promise<HealthCheckResult> {
|
|
173
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
|
|
174
|
+
assertTimeout(timeoutMs);
|
|
175
|
+
|
|
176
|
+
const includeErrorDetails = options.includeErrorDetails ?? true;
|
|
177
|
+
const entries = await Promise.all(
|
|
178
|
+
Object.entries(checks).map(async ([name, check]) => {
|
|
179
|
+
const startedAt = Date.now();
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const detail = redactHealthDetail(
|
|
183
|
+
normalizeHealthDetail(
|
|
184
|
+
await withTimeout(Promise.resolve(check(ports)), timeoutMs, name),
|
|
185
|
+
),
|
|
186
|
+
includeErrorDetails,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return [
|
|
190
|
+
name,
|
|
191
|
+
{
|
|
192
|
+
...detail,
|
|
193
|
+
durationMs: Date.now() - startedAt,
|
|
194
|
+
},
|
|
195
|
+
] as const;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return [
|
|
198
|
+
name,
|
|
199
|
+
{
|
|
200
|
+
ok: false,
|
|
201
|
+
message: includeErrorDetails
|
|
202
|
+
? errorMessage(error)
|
|
203
|
+
: "Health check failed",
|
|
204
|
+
durationMs: Date.now() - startedAt,
|
|
205
|
+
},
|
|
206
|
+
] as const;
|
|
207
|
+
}
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const details = Object.fromEntries(entries);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
ok: Object.values(details).every((detail) => detail.ok),
|
|
215
|
+
details,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
44
219
|
/**
|
|
45
220
|
* Create a framework-neutral health check handler.
|
|
46
221
|
*
|
|
@@ -76,6 +251,11 @@ export function createHealthHandler<Ports extends AnyPorts>(
|
|
|
76
251
|
},
|
|
77
252
|
};
|
|
78
253
|
}
|
|
254
|
+
} else if (healthConfig?.checks) {
|
|
255
|
+
result = await runHealthChecks(ports, healthConfig.checks, {
|
|
256
|
+
timeoutMs: healthConfig.timeoutMs,
|
|
257
|
+
includeErrorDetails: env !== "production",
|
|
258
|
+
});
|
|
79
259
|
} else {
|
|
80
260
|
result = { ok: true };
|
|
81
261
|
}
|