@btravstack/core 0.4.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/README.md +1 -1
- package/dist/index.cjs +69 -16
- package/dist/index.d.cts +80 -1
- package/dist/index.d.mts +81 -2
- package/dist/index.mjs +66 -17
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ pnpm add @btravstack/core @btravstack/config @btravstack/di unthrown
|
|
|
16
16
|
|
|
17
17
|
`@btravstack/config`, `@btravstack/di` and `unthrown` are **peer
|
|
18
18
|
dependencies** — install all four. The kernel itself depends on `node:`
|
|
19
|
-
builtins only. Node `>=
|
|
19
|
+
builtins only. Node `>=22`.
|
|
20
20
|
|
|
21
21
|
## A worked example
|
|
22
22
|
|
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(),
|
|
@@ -128,17 +178,6 @@ const whenAborted = (signal) => signal.aborted ? (0, unthrown.OkAsync)() : (0, u
|
|
|
128
178
|
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
129
179
|
}));
|
|
130
180
|
//#endregion
|
|
131
|
-
//#region src/deferred.ts
|
|
132
|
-
const createDeferred = () => {
|
|
133
|
-
let resolve;
|
|
134
|
-
return {
|
|
135
|
-
promise: new Promise((r) => {
|
|
136
|
-
resolve = r;
|
|
137
|
-
}),
|
|
138
|
-
resolve
|
|
139
|
-
};
|
|
140
|
-
};
|
|
141
|
-
//#endregion
|
|
142
181
|
//#region src/drain.ts
|
|
143
182
|
const drainApp = (args) => {
|
|
144
183
|
args.onUnready();
|
|
@@ -196,6 +235,12 @@ const startProbeServer = (args) => (0, unthrown.fromSafePromise)(new Promise((re
|
|
|
196
235
|
respond(response, args.ready(), "ready");
|
|
197
236
|
return;
|
|
198
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
|
+
}
|
|
199
244
|
response.writeHead(404).end();
|
|
200
245
|
});
|
|
201
246
|
const onBindError = (cause) => {
|
|
@@ -334,7 +379,7 @@ const start = (module, options = {}) => {
|
|
|
334
379
|
if (phase === "exited") emit({ type: "exited" });
|
|
335
380
|
});
|
|
336
381
|
const registry = createUnitRegistry();
|
|
337
|
-
const shutdown =
|
|
382
|
+
const shutdown = Promise.withResolvers();
|
|
338
383
|
const teardownErrors = [];
|
|
339
384
|
const startedAt = clock.now();
|
|
340
385
|
const preDrainDelayMs = options.preDrainDelayMs ?? 5e3;
|
|
@@ -370,8 +415,10 @@ const start = (module, options = {}) => {
|
|
|
370
415
|
requestShutdown("uncaught");
|
|
371
416
|
});
|
|
372
417
|
let disposeProbes = () => {};
|
|
373
|
-
|
|
374
|
-
const
|
|
418
|
+
let healthChecks = [];
|
|
419
|
+
const health = () => runHealthChecks(healthChecks);
|
|
420
|
+
const probeBound = Promise.withResolvers();
|
|
421
|
+
const runtimePublished = Promise.withResolvers();
|
|
375
422
|
emit({ type: "building" });
|
|
376
423
|
const probesOptions = options.probes === void 0 ? _btravstack_config.Config.port("PROBE_PORT", { default: 9e3 }).parse(env["PROBE_PORT"]).map((port) => ({ port })).mapErrCases((matcher) => matcher.with(unthrown.P.tag("ConfigFieldInvalid"), ({ reason }) => new RuntimeStartFailed({
|
|
377
424
|
runtime: "probes",
|
|
@@ -387,7 +434,8 @@ const start = (module, options = {}) => {
|
|
|
387
434
|
const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? (0, unthrown.OkAsync)() : startProbeServer({
|
|
388
435
|
port: probes.port,
|
|
389
436
|
live,
|
|
390
|
-
ready
|
|
437
|
+
ready,
|
|
438
|
+
health
|
|
391
439
|
}).tap((server) => {
|
|
392
440
|
probeBoundPort = server.port;
|
|
393
441
|
probeBound.resolve(server.port);
|
|
@@ -455,6 +503,7 @@ const start = (module, options = {}) => {
|
|
|
455
503
|
tracker.advanceTo("starting");
|
|
456
504
|
const runtime = ctx.get(RuntimePort);
|
|
457
505
|
runtimeName = runtime.name;
|
|
506
|
+
healthChecks = ctx.get(HealthChecks);
|
|
458
507
|
const runtimeCtx = ctx;
|
|
459
508
|
const unit = options.unit;
|
|
460
509
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
@@ -565,6 +614,9 @@ const runMain = async (module, options = {}, exit = (code) => {
|
|
|
565
614
|
await awaitExit(start(module, options), exit);
|
|
566
615
|
};
|
|
567
616
|
//#endregion
|
|
617
|
+
exports.HealthCheckFailed = HealthCheckFailed;
|
|
618
|
+
exports.HealthChecks = HealthChecks;
|
|
619
|
+
exports.Instrumentations = Instrumentations;
|
|
568
620
|
exports.LEVELS = LEVELS;
|
|
569
621
|
exports.Logger = Logger;
|
|
570
622
|
exports.Meter = Meter;
|
|
@@ -574,6 +626,7 @@ exports.SPAN_STATUS = SPAN_STATUS;
|
|
|
574
626
|
exports.Tracer = Tracer;
|
|
575
627
|
exports.currentUnit = currentUnit;
|
|
576
628
|
exports.releasedBy = releasedBy;
|
|
629
|
+
exports.runHealthChecks = runHealthChecks;
|
|
577
630
|
exports.runMain = runMain;
|
|
578
631
|
exports.start = start;
|
|
579
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(),
|
|
@@ -127,17 +177,6 @@ const whenAborted = (signal) => signal.aborted ? OkAsync() : fromSafePromise(new
|
|
|
127
177
|
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
128
178
|
}));
|
|
129
179
|
//#endregion
|
|
130
|
-
//#region src/deferred.ts
|
|
131
|
-
const createDeferred = () => {
|
|
132
|
-
let resolve;
|
|
133
|
-
return {
|
|
134
|
-
promise: new Promise((r) => {
|
|
135
|
-
resolve = r;
|
|
136
|
-
}),
|
|
137
|
-
resolve
|
|
138
|
-
};
|
|
139
|
-
};
|
|
140
|
-
//#endregion
|
|
141
180
|
//#region src/drain.ts
|
|
142
181
|
const drainApp = (args) => {
|
|
143
182
|
args.onUnready();
|
|
@@ -195,6 +234,12 @@ const startProbeServer = (args) => fromSafePromise(new Promise((resolve) => {
|
|
|
195
234
|
respond(response, args.ready(), "ready");
|
|
196
235
|
return;
|
|
197
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
|
+
}
|
|
198
243
|
response.writeHead(404).end();
|
|
199
244
|
});
|
|
200
245
|
const onBindError = (cause) => {
|
|
@@ -333,7 +378,7 @@ const start = (module, options = {}) => {
|
|
|
333
378
|
if (phase === "exited") emit({ type: "exited" });
|
|
334
379
|
});
|
|
335
380
|
const registry = createUnitRegistry();
|
|
336
|
-
const shutdown =
|
|
381
|
+
const shutdown = Promise.withResolvers();
|
|
337
382
|
const teardownErrors = [];
|
|
338
383
|
const startedAt = clock.now();
|
|
339
384
|
const preDrainDelayMs = options.preDrainDelayMs ?? 5e3;
|
|
@@ -369,8 +414,10 @@ const start = (module, options = {}) => {
|
|
|
369
414
|
requestShutdown("uncaught");
|
|
370
415
|
});
|
|
371
416
|
let disposeProbes = () => {};
|
|
372
|
-
|
|
373
|
-
const
|
|
417
|
+
let healthChecks = [];
|
|
418
|
+
const health = () => runHealthChecks(healthChecks);
|
|
419
|
+
const probeBound = Promise.withResolvers();
|
|
420
|
+
const runtimePublished = Promise.withResolvers();
|
|
374
421
|
emit({ type: "building" });
|
|
375
422
|
const probesOptions = options.probes === void 0 ? Config.port("PROBE_PORT", { default: 9e3 }).parse(env["PROBE_PORT"]).map((port) => ({ port })).mapErrCases((matcher) => matcher.with(P.tag("ConfigFieldInvalid"), ({ reason }) => new RuntimeStartFailed({
|
|
376
423
|
runtime: "probes",
|
|
@@ -386,7 +433,8 @@ const start = (module, options = {}) => {
|
|
|
386
433
|
const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? OkAsync() : startProbeServer({
|
|
387
434
|
port: probes.port,
|
|
388
435
|
live,
|
|
389
|
-
ready
|
|
436
|
+
ready,
|
|
437
|
+
health
|
|
390
438
|
}).tap((server) => {
|
|
391
439
|
probeBoundPort = server.port;
|
|
392
440
|
probeBound.resolve(server.port);
|
|
@@ -454,6 +502,7 @@ const start = (module, options = {}) => {
|
|
|
454
502
|
tracker.advanceTo("starting");
|
|
455
503
|
const runtime = ctx.get(RuntimePort);
|
|
456
504
|
runtimeName = runtime.name;
|
|
505
|
+
healthChecks = ctx.get(HealthChecks);
|
|
457
506
|
const runtimeCtx = ctx;
|
|
458
507
|
const unit = options.unit;
|
|
459
508
|
const run = (meta, work) => registry.run(meta, (signal) => {
|
|
@@ -564,4 +613,4 @@ const runMain = async (module, options = {}, exit = (code) => {
|
|
|
564
613
|
await awaitExit(start(module, options), exit);
|
|
565
614
|
};
|
|
566
615
|
//#endregion
|
|
567
|
-
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,16 +53,16 @@
|
|
|
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
|
-
"node": ">=
|
|
65
|
+
"node": ">=22"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsdown src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.build.json",
|