@btravstack/core 0.5.0 → 0.6.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/dist/index.cjs +66 -2
- package/dist/index.d.cts +80 -1
- package/dist/index.d.mts +81 -2
- package/dist/index.mjs +63 -3
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,59 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let unthrown = require("unthrown");
|
|
3
2
|
let _btravstack_di = require("@btravstack/di");
|
|
3
|
+
let unthrown = require("unthrown");
|
|
4
4
|
let _btravstack_config = require("@btravstack/config");
|
|
5
5
|
let node_http = require("node:http");
|
|
6
6
|
let node_async_hooks = require("node:async_hooks");
|
|
7
|
+
//#region src/instrumentation.ts
|
|
8
|
+
/**
|
|
9
|
+
* The set port a package contributes its instrumentation to.
|
|
10
|
+
*
|
|
11
|
+
* Nothing collects it unless an OTel SDK is composed, which is the point: a
|
|
12
|
+
* starter DECLARES what it can instrument, and composing
|
|
13
|
+
* `@btravstack/observability/otel` is what turns the declarations on. A graph
|
|
14
|
+
* without it registers nothing and pays nothing — the Spring Boot starter
|
|
15
|
+
* shape, in one port.
|
|
16
|
+
*/
|
|
17
|
+
var Instrumentations = class extends _btravstack_di.Port.many("Instrumentations") {};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/health.ts
|
|
20
|
+
/**
|
|
21
|
+
* A component could not answer for itself. Modeled rather than thrown, because
|
|
22
|
+
* a health check that throws is a bug in the check; one that fails is the news
|
|
23
|
+
* `/healthz` exists to carry.
|
|
24
|
+
*/
|
|
25
|
+
var HealthCheckFailed = class extends (0, unthrown.TaggedError)("HealthCheckFailed") {};
|
|
26
|
+
/**
|
|
27
|
+
* The set port every starter contributes to and the kernel reads whole.
|
|
28
|
+
*
|
|
29
|
+
* A set port rather than a registry the kernel hands out: a starter DECLARES
|
|
30
|
+
* its check the same way it declares anything else, and a starter an
|
|
31
|
+
* application never composed contributes nothing — no registration call to
|
|
32
|
+
* forget, and no order to get wrong.
|
|
33
|
+
*/
|
|
34
|
+
var HealthChecks = class extends _btravstack_di.Port.many("HealthChecks") {};
|
|
35
|
+
/**
|
|
36
|
+
* Runs every check and folds the results into one report — the whole app is
|
|
37
|
+
* unhealthy if any single component is.
|
|
38
|
+
*
|
|
39
|
+
* Each check's failure is recovered into a component line BEFORE `allAsync`
|
|
40
|
+
* sees it, so a failing dependency cannot short-circuit the others: a report
|
|
41
|
+
* naming one component is worth less than one naming all of them. What
|
|
42
|
+
* survives to `allAsync`'s error channel is a `Defect` alone — a check that
|
|
43
|
+
* threw rather than answered, which is a bug in the check.
|
|
44
|
+
*/
|
|
45
|
+
const runHealthChecks = (checks) => (0, unthrown.allAsync)(checks.map((health) => health.check().map(() => ({
|
|
46
|
+
name: health.name,
|
|
47
|
+
status: "healthy"
|
|
48
|
+
})).recoverErrCases((matcher) => matcher.with(unthrown.P.tag("HealthCheckFailed"), (error) => ({
|
|
49
|
+
name: health.name,
|
|
50
|
+
status: "unhealthy",
|
|
51
|
+
reason: error.reason
|
|
52
|
+
}))))).map((components) => ({
|
|
53
|
+
status: components.every((c) => c.status === "healthy") ? "healthy" : "unhealthy",
|
|
54
|
+
components
|
|
55
|
+
}));
|
|
56
|
+
//#endregion
|
|
7
57
|
//#region src/clock.ts
|
|
8
58
|
const systemClock = {
|
|
9
59
|
now: () => Date.now(),
|
|
@@ -185,6 +235,12 @@ const startProbeServer = (args) => (0, unthrown.fromSafePromise)(new Promise((re
|
|
|
185
235
|
respond(response, args.ready(), "ready");
|
|
186
236
|
return;
|
|
187
237
|
}
|
|
238
|
+
if (request.url === "/healthz") {
|
|
239
|
+
args.health().map((report) => {
|
|
240
|
+
response.writeHead(report.status === "healthy" ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify(report));
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
188
244
|
response.writeHead(404).end();
|
|
189
245
|
});
|
|
190
246
|
const onBindError = (cause) => {
|
|
@@ -359,6 +415,8 @@ const start = (module, options = {}) => {
|
|
|
359
415
|
requestShutdown("uncaught");
|
|
360
416
|
});
|
|
361
417
|
let disposeProbes = () => {};
|
|
418
|
+
let healthChecks = [];
|
|
419
|
+
const health = () => runHealthChecks(healthChecks);
|
|
362
420
|
const probeBound = Promise.withResolvers();
|
|
363
421
|
const runtimePublished = Promise.withResolvers();
|
|
364
422
|
emit({ type: "building" });
|
|
@@ -376,7 +434,8 @@ const start = (module, options = {}) => {
|
|
|
376
434
|
const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? (0, unthrown.OkAsync)() : startProbeServer({
|
|
377
435
|
port: probes.port,
|
|
378
436
|
live,
|
|
379
|
-
ready
|
|
437
|
+
ready,
|
|
438
|
+
health
|
|
380
439
|
}).tap((server) => {
|
|
381
440
|
probeBoundPort = server.port;
|
|
382
441
|
probeBound.resolve(server.port);
|
|
@@ -444,6 +503,7 @@ const start = (module, options = {}) => {
|
|
|
444
503
|
tracker.advanceTo("starting");
|
|
445
504
|
const runtime = ctx.get(RuntimePort);
|
|
446
505
|
runtimeName = runtime.name;
|
|
506
|
+
healthChecks = ctx.get(HealthChecks);
|
|
447
507
|
const runtimeCtx = ctx;
|
|
448
508
|
const unit = options.unit;
|
|
449
509
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
@@ -554,6 +614,9 @@ const runMain = async (module, options = {}, exit = (code) => {
|
|
|
554
614
|
await awaitExit(start(module, options), exit);
|
|
555
615
|
};
|
|
556
616
|
//#endregion
|
|
617
|
+
exports.HealthCheckFailed = HealthCheckFailed;
|
|
618
|
+
exports.HealthChecks = HealthChecks;
|
|
619
|
+
exports.Instrumentations = Instrumentations;
|
|
557
620
|
exports.LEVELS = LEVELS;
|
|
558
621
|
exports.Logger = Logger;
|
|
559
622
|
exports.Meter = Meter;
|
|
@@ -563,6 +626,7 @@ exports.SPAN_STATUS = SPAN_STATUS;
|
|
|
563
626
|
exports.Tracer = Tracer;
|
|
564
627
|
exports.currentUnit = currentUnit;
|
|
565
628
|
exports.releasedBy = releasedBy;
|
|
629
|
+
exports.runHealthChecks = runHealthChecks;
|
|
566
630
|
exports.runMain = runMain;
|
|
567
631
|
exports.start = start;
|
|
568
632
|
exports.stderrSink = stderrSink;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
import { AsyncResult, Result } from "unthrown";
|
|
2
2
|
import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
|
|
3
3
|
import { Env, Environment } from "@btravstack/config";
|
|
4
|
+
//#region src/instrumentation.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* One package's offer of an OpenTelemetry instrumentation.
|
|
7
|
+
*
|
|
8
|
+
* **Async, and answers `undefined` rather than failing, because the package
|
|
9
|
+
* supplying the instrumentation is an OPTIONAL peer.** A starter cannot know at
|
|
10
|
+
* composition time whether the consumer installed it, so it contributes the
|
|
11
|
+
* ATTEMPT and the collector drops what did not load — which is what lets
|
|
12
|
+
* `@btravstack/prisma` declare engine tracing without making every consumer
|
|
13
|
+
* install `@prisma/instrumentation`. A contributor that wants to say why its
|
|
14
|
+
* load answered nothing logs that itself; it is the one that knows.
|
|
15
|
+
*
|
|
16
|
+
* The instrumentation is `unknown` on purpose. Naming OpenTelemetry's
|
|
17
|
+
* `Instrumentation` would put the vendor in the package every other one peers
|
|
18
|
+
* on — the same reason `Tracer` and `Meter` are declared as narrowings rather
|
|
19
|
+
* than as OTel's own types. The collector
|
|
20
|
+
* (`@btravstack/observability/otel`) is where the vendor is already a
|
|
21
|
+
* dependency, so that is where the cast belongs.
|
|
22
|
+
*/
|
|
23
|
+
type InstrumentationLoader = () => Promise<unknown>;
|
|
24
|
+
declare const Instrumentations_base: import("@btravstack/di").ManyPortClass<"Instrumentations">;
|
|
25
|
+
/**
|
|
26
|
+
* The set port a package contributes its instrumentation to.
|
|
27
|
+
*
|
|
28
|
+
* Nothing collects it unless an OTel SDK is composed, which is the point: a
|
|
29
|
+
* starter DECLARES what it can instrument, and composing
|
|
30
|
+
* `@btravstack/observability/otel` is what turns the declarations on. A graph
|
|
31
|
+
* without it registers nothing and pays nothing — the Spring Boot starter
|
|
32
|
+
* shape, in one port.
|
|
33
|
+
*/
|
|
34
|
+
declare class Instrumentations extends Instrumentations_base<InstrumentationLoader> {}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/health.d.ts
|
|
37
|
+
declare const HealthCheckFailed_base: import("unthrown").TaggedErrorConstructor<"HealthCheckFailed">;
|
|
38
|
+
/**
|
|
39
|
+
* A component could not answer for itself. Modeled rather than thrown, because
|
|
40
|
+
* a health check that throws is a bug in the check; one that fails is the news
|
|
41
|
+
* `/healthz` exists to carry.
|
|
42
|
+
*/
|
|
43
|
+
declare class HealthCheckFailed extends HealthCheckFailed_base<{
|
|
44
|
+
readonly reason: string;
|
|
45
|
+
}> {}
|
|
46
|
+
/** One contribution: what to call the component, and how to ask it. */
|
|
47
|
+
type HealthCheck = {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly check: () => AsyncResult<void, HealthCheckFailed>;
|
|
50
|
+
};
|
|
51
|
+
declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
|
|
52
|
+
/**
|
|
53
|
+
* The set port every starter contributes to and the kernel reads whole.
|
|
54
|
+
*
|
|
55
|
+
* A set port rather than a registry the kernel hands out: a starter DECLARES
|
|
56
|
+
* its check the same way it declares anything else, and a starter an
|
|
57
|
+
* application never composed contributes nothing — no registration call to
|
|
58
|
+
* forget, and no order to get wrong.
|
|
59
|
+
*/
|
|
60
|
+
declare class HealthChecks extends HealthChecks_base<HealthCheck> {}
|
|
61
|
+
type ComponentHealth = {
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly status: "healthy" | "unhealthy";
|
|
64
|
+
/** Present only when the component is unhealthy. */
|
|
65
|
+
readonly reason?: string;
|
|
66
|
+
};
|
|
67
|
+
type HealthReport = {
|
|
68
|
+
readonly status: "healthy" | "unhealthy";
|
|
69
|
+
readonly components: readonly ComponentHealth[];
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Runs every check and folds the results into one report — the whole app is
|
|
73
|
+
* unhealthy if any single component is.
|
|
74
|
+
*
|
|
75
|
+
* Each check's failure is recovered into a component line BEFORE `allAsync`
|
|
76
|
+
* sees it, so a failing dependency cannot short-circuit the others: a report
|
|
77
|
+
* naming one component is worth less than one naming all of them. What
|
|
78
|
+
* survives to `allAsync`'s error channel is a `Defect` alone — a check that
|
|
79
|
+
* threw rather than answered, which is a bug in the check.
|
|
80
|
+
*/
|
|
81
|
+
declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
|
|
82
|
+
//#endregion
|
|
4
83
|
//#region src/clock.d.ts
|
|
5
84
|
type Clock = {
|
|
6
85
|
readonly now: () => number;
|
|
@@ -455,4 +534,4 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
|
|
|
455
534
|
*/
|
|
456
535
|
declare const runMain: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E, Scope | Env> & StartGate<X, UnitNeeds>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
|
|
457
536
|
//#endregion
|
|
458
|
-
export { type Attributes, type Clock, type Counter, type DrainReport, type EventSink, type ExitReport, type Histogram, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };
|
|
537
|
+
export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
|
-
import { AsyncResult, Result } from "unthrown";
|
|
2
1
|
import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
|
|
2
|
+
import { AsyncResult, Result } from "unthrown";
|
|
3
3
|
import { Env, Environment } from "@btravstack/config";
|
|
4
|
+
//#region src/instrumentation.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* One package's offer of an OpenTelemetry instrumentation.
|
|
7
|
+
*
|
|
8
|
+
* **Async, and answers `undefined` rather than failing, because the package
|
|
9
|
+
* supplying the instrumentation is an OPTIONAL peer.** A starter cannot know at
|
|
10
|
+
* composition time whether the consumer installed it, so it contributes the
|
|
11
|
+
* ATTEMPT and the collector drops what did not load — which is what lets
|
|
12
|
+
* `@btravstack/prisma` declare engine tracing without making every consumer
|
|
13
|
+
* install `@prisma/instrumentation`. A contributor that wants to say why its
|
|
14
|
+
* load answered nothing logs that itself; it is the one that knows.
|
|
15
|
+
*
|
|
16
|
+
* The instrumentation is `unknown` on purpose. Naming OpenTelemetry's
|
|
17
|
+
* `Instrumentation` would put the vendor in the package every other one peers
|
|
18
|
+
* on — the same reason `Tracer` and `Meter` are declared as narrowings rather
|
|
19
|
+
* than as OTel's own types. The collector
|
|
20
|
+
* (`@btravstack/observability/otel`) is where the vendor is already a
|
|
21
|
+
* dependency, so that is where the cast belongs.
|
|
22
|
+
*/
|
|
23
|
+
type InstrumentationLoader = () => Promise<unknown>;
|
|
24
|
+
declare const Instrumentations_base: import("@btravstack/di").ManyPortClass<"Instrumentations">;
|
|
25
|
+
/**
|
|
26
|
+
* The set port a package contributes its instrumentation to.
|
|
27
|
+
*
|
|
28
|
+
* Nothing collects it unless an OTel SDK is composed, which is the point: a
|
|
29
|
+
* starter DECLARES what it can instrument, and composing
|
|
30
|
+
* `@btravstack/observability/otel` is what turns the declarations on. A graph
|
|
31
|
+
* without it registers nothing and pays nothing — the Spring Boot starter
|
|
32
|
+
* shape, in one port.
|
|
33
|
+
*/
|
|
34
|
+
declare class Instrumentations extends Instrumentations_base<InstrumentationLoader> {}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/health.d.ts
|
|
37
|
+
declare const HealthCheckFailed_base: import("unthrown").TaggedErrorConstructor<"HealthCheckFailed">;
|
|
38
|
+
/**
|
|
39
|
+
* A component could not answer for itself. Modeled rather than thrown, because
|
|
40
|
+
* a health check that throws is a bug in the check; one that fails is the news
|
|
41
|
+
* `/healthz` exists to carry.
|
|
42
|
+
*/
|
|
43
|
+
declare class HealthCheckFailed extends HealthCheckFailed_base<{
|
|
44
|
+
readonly reason: string;
|
|
45
|
+
}> {}
|
|
46
|
+
/** One contribution: what to call the component, and how to ask it. */
|
|
47
|
+
type HealthCheck = {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly check: () => AsyncResult<void, HealthCheckFailed>;
|
|
50
|
+
};
|
|
51
|
+
declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
|
|
52
|
+
/**
|
|
53
|
+
* The set port every starter contributes to and the kernel reads whole.
|
|
54
|
+
*
|
|
55
|
+
* A set port rather than a registry the kernel hands out: a starter DECLARES
|
|
56
|
+
* its check the same way it declares anything else, and a starter an
|
|
57
|
+
* application never composed contributes nothing — no registration call to
|
|
58
|
+
* forget, and no order to get wrong.
|
|
59
|
+
*/
|
|
60
|
+
declare class HealthChecks extends HealthChecks_base<HealthCheck> {}
|
|
61
|
+
type ComponentHealth = {
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly status: "healthy" | "unhealthy";
|
|
64
|
+
/** Present only when the component is unhealthy. */
|
|
65
|
+
readonly reason?: string;
|
|
66
|
+
};
|
|
67
|
+
type HealthReport = {
|
|
68
|
+
readonly status: "healthy" | "unhealthy";
|
|
69
|
+
readonly components: readonly ComponentHealth[];
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Runs every check and folds the results into one report — the whole app is
|
|
73
|
+
* unhealthy if any single component is.
|
|
74
|
+
*
|
|
75
|
+
* Each check's failure is recovered into a component line BEFORE `allAsync`
|
|
76
|
+
* sees it, so a failing dependency cannot short-circuit the others: a report
|
|
77
|
+
* naming one component is worth less than one naming all of them. What
|
|
78
|
+
* survives to `allAsync`'s error channel is a `Defect` alone — a check that
|
|
79
|
+
* threw rather than answered, which is a bug in the check.
|
|
80
|
+
*/
|
|
81
|
+
declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
|
|
82
|
+
//#endregion
|
|
4
83
|
//#region src/clock.d.ts
|
|
5
84
|
type Clock = {
|
|
6
85
|
readonly now: () => number;
|
|
@@ -455,4 +534,4 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
|
|
|
455
534
|
*/
|
|
456
535
|
declare const runMain: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E, Scope | Env> & StartGate<X, UnitNeeds>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
|
|
457
536
|
//#endregion
|
|
458
|
-
export { type Attributes, type Clock, type Counter, type DrainReport, type EventSink, type ExitReport, type Histogram, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };
|
|
537
|
+
export { type Attributes, type Clock, type ComponentHealth, type Counter, type DrainReport, type EventSink, type ExitReport, type HealthCheck, HealthCheckFailed, HealthChecks, type HealthReport, type Histogram, type InstrumentationLoader, Instrumentations, type KernelEvent, LEVELS, type Level, Logger, type LoggerService, Meter, type MeterService, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,58 @@
|
|
|
1
|
-
import { Err, Ok, OkAsync, P, TaggedError, fromSafePromise } from "unthrown";
|
|
2
1
|
import { Module, Port, Provider } from "@btravstack/di";
|
|
2
|
+
import { Err, Ok, OkAsync, P, TaggedError, allAsync, fromSafePromise } from "unthrown";
|
|
3
3
|
import { Config, ConfigInvalid, Env } from "@btravstack/config";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
//#region src/instrumentation.ts
|
|
7
|
+
/**
|
|
8
|
+
* The set port a package contributes its instrumentation to.
|
|
9
|
+
*
|
|
10
|
+
* Nothing collects it unless an OTel SDK is composed, which is the point: a
|
|
11
|
+
* starter DECLARES what it can instrument, and composing
|
|
12
|
+
* `@btravstack/observability/otel` is what turns the declarations on. A graph
|
|
13
|
+
* without it registers nothing and pays nothing — the Spring Boot starter
|
|
14
|
+
* shape, in one port.
|
|
15
|
+
*/
|
|
16
|
+
var Instrumentations = class extends Port.many("Instrumentations") {};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/health.ts
|
|
19
|
+
/**
|
|
20
|
+
* A component could not answer for itself. Modeled rather than thrown, because
|
|
21
|
+
* a health check that throws is a bug in the check; one that fails is the news
|
|
22
|
+
* `/healthz` exists to carry.
|
|
23
|
+
*/
|
|
24
|
+
var HealthCheckFailed = class extends TaggedError("HealthCheckFailed") {};
|
|
25
|
+
/**
|
|
26
|
+
* The set port every starter contributes to and the kernel reads whole.
|
|
27
|
+
*
|
|
28
|
+
* A set port rather than a registry the kernel hands out: a starter DECLARES
|
|
29
|
+
* its check the same way it declares anything else, and a starter an
|
|
30
|
+
* application never composed contributes nothing — no registration call to
|
|
31
|
+
* forget, and no order to get wrong.
|
|
32
|
+
*/
|
|
33
|
+
var HealthChecks = class extends Port.many("HealthChecks") {};
|
|
34
|
+
/**
|
|
35
|
+
* Runs every check and folds the results into one report — the whole app is
|
|
36
|
+
* unhealthy if any single component is.
|
|
37
|
+
*
|
|
38
|
+
* Each check's failure is recovered into a component line BEFORE `allAsync`
|
|
39
|
+
* sees it, so a failing dependency cannot short-circuit the others: a report
|
|
40
|
+
* naming one component is worth less than one naming all of them. What
|
|
41
|
+
* survives to `allAsync`'s error channel is a `Defect` alone — a check that
|
|
42
|
+
* threw rather than answered, which is a bug in the check.
|
|
43
|
+
*/
|
|
44
|
+
const runHealthChecks = (checks) => allAsync(checks.map((health) => health.check().map(() => ({
|
|
45
|
+
name: health.name,
|
|
46
|
+
status: "healthy"
|
|
47
|
+
})).recoverErrCases((matcher) => matcher.with(P.tag("HealthCheckFailed"), (error) => ({
|
|
48
|
+
name: health.name,
|
|
49
|
+
status: "unhealthy",
|
|
50
|
+
reason: error.reason
|
|
51
|
+
}))))).map((components) => ({
|
|
52
|
+
status: components.every((c) => c.status === "healthy") ? "healthy" : "unhealthy",
|
|
53
|
+
components
|
|
54
|
+
}));
|
|
55
|
+
//#endregion
|
|
6
56
|
//#region src/clock.ts
|
|
7
57
|
const systemClock = {
|
|
8
58
|
now: () => Date.now(),
|
|
@@ -184,6 +234,12 @@ const startProbeServer = (args) => fromSafePromise(new Promise((resolve) => {
|
|
|
184
234
|
respond(response, args.ready(), "ready");
|
|
185
235
|
return;
|
|
186
236
|
}
|
|
237
|
+
if (request.url === "/healthz") {
|
|
238
|
+
args.health().map((report) => {
|
|
239
|
+
response.writeHead(report.status === "healthy" ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify(report));
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
187
243
|
response.writeHead(404).end();
|
|
188
244
|
});
|
|
189
245
|
const onBindError = (cause) => {
|
|
@@ -358,6 +414,8 @@ const start = (module, options = {}) => {
|
|
|
358
414
|
requestShutdown("uncaught");
|
|
359
415
|
});
|
|
360
416
|
let disposeProbes = () => {};
|
|
417
|
+
let healthChecks = [];
|
|
418
|
+
const health = () => runHealthChecks(healthChecks);
|
|
361
419
|
const probeBound = Promise.withResolvers();
|
|
362
420
|
const runtimePublished = Promise.withResolvers();
|
|
363
421
|
emit({ type: "building" });
|
|
@@ -375,7 +433,8 @@ const start = (module, options = {}) => {
|
|
|
375
433
|
const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? OkAsync() : startProbeServer({
|
|
376
434
|
port: probes.port,
|
|
377
435
|
live,
|
|
378
|
-
ready
|
|
436
|
+
ready,
|
|
437
|
+
health
|
|
379
438
|
}).tap((server) => {
|
|
380
439
|
probeBoundPort = server.port;
|
|
381
440
|
probeBound.resolve(server.port);
|
|
@@ -443,6 +502,7 @@ const start = (module, options = {}) => {
|
|
|
443
502
|
tracker.advanceTo("starting");
|
|
444
503
|
const runtime = ctx.get(RuntimePort);
|
|
445
504
|
runtimeName = runtime.name;
|
|
505
|
+
healthChecks = ctx.get(HealthChecks);
|
|
446
506
|
const runtimeCtx = ctx;
|
|
447
507
|
const unit = options.unit;
|
|
448
508
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
@@ -553,4 +613,4 @@ const runMain = async (module, options = {}, exit = (code) => {
|
|
|
553
613
|
await awaitExit(start(module, options), exit);
|
|
554
614
|
};
|
|
555
615
|
//#endregion
|
|
556
|
-
export { LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };
|
|
616
|
+
export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The application kernel: boot a di module into a running process, and stop it without losing work",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"application",
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
"typescript": "7.0.2",
|
|
54
54
|
"unthrown": "5.7.0",
|
|
55
55
|
"vitest": "4.1.11",
|
|
56
|
-
"@btravstack/config": "0.
|
|
57
|
-
"@btravstack/di": "0.
|
|
56
|
+
"@btravstack/config": "0.6.0",
|
|
57
|
+
"@btravstack/di": "0.6.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"unthrown": "^5.0.0",
|
|
61
|
-
"@btravstack/config": "^0.
|
|
62
|
-
"@btravstack/di": "^0.
|
|
61
|
+
"@btravstack/config": "^0.6.0",
|
|
62
|
+
"@btravstack/di": "^0.6.0"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22"
|