@btravstack/core 0.9.0 → 0.11.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 +47 -1
- package/dist/index.d.cts +239 -126
- package/dist/index.d.mts +239 -126
- package/dist/index.mjs +45 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ is a module that exports no runtime port at all.
|
|
|
132
132
|
- **A per-unit scope no handler manages** — pass a module as
|
|
133
133
|
`StartOptions.unit` and the kernel forks it around every unit.
|
|
134
134
|
- **An ambient record, not an ambient container** — `currentUnit()` reads
|
|
135
|
-
`{ unitId, traceId, tenantId,
|
|
135
|
+
`{ unitId, traceId, tenantId, signal }`; services never travel
|
|
136
136
|
there. `signal` is the very `AbortSignal` the unit's work callback is handed,
|
|
137
137
|
so a runtime whose work is a library's `next()` — a Temporal activity, an
|
|
138
138
|
AMQP delivery — can still honour the drain deadline.
|
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,50 @@ const runHealthChecks = (checks) => (0, unthrown.allAsync)(checks.map((health) =
|
|
|
61
61
|
components
|
|
62
62
|
}));
|
|
63
63
|
//#endregion
|
|
64
|
+
//#region src/observation.ts
|
|
65
|
+
/**
|
|
66
|
+
* The set port a starter contributes its observability to, and reads whole.
|
|
67
|
+
*
|
|
68
|
+
* **Called at the START and answering a finisher**, rather than told about a
|
|
69
|
+
* finished operation. That is what lets an observer open a span before the work
|
|
70
|
+
* and end it after — a span reconstructed afterwards from a duration is not the
|
|
71
|
+
* parent of anything that ran inside it.
|
|
72
|
+
*
|
|
73
|
+
* A set port rather than a `Meter` in every starter's `needs`: an observer
|
|
74
|
+
* package an application never composed contributes nothing, and a starter
|
|
75
|
+
* costs a graph no ports to have observability available. That is the
|
|
76
|
+
* difference between "instrumented by default" and "instrumented if you asked
|
|
77
|
+
* for observability", and only the second is free.
|
|
78
|
+
*
|
|
79
|
+
* **A module that reads this port contributes a no-op member of its own**, the
|
|
80
|
+
* way `otel()` does for `Instrumentations`: a collector depending on a set port
|
|
81
|
+
* NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
|
|
82
|
+
* Several no-ops in one graph cost a call each and nothing else.
|
|
83
|
+
*/
|
|
84
|
+
var Observers = class extends _btravstack_di.Port.many("Observers") {};
|
|
85
|
+
/** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
|
|
86
|
+
const noObserver = () => () => {};
|
|
87
|
+
/**
|
|
88
|
+
* Run every observer around one operation, and hand back the one finisher that
|
|
89
|
+
* settles them all.
|
|
90
|
+
*
|
|
91
|
+
* Observers are started in order and settled in order, and the finisher is
|
|
92
|
+
* **guarded**: a second call is dropped. "Called exactly once" is therefore a
|
|
93
|
+
* property of this function rather than a rule each starter re-keeps — which
|
|
94
|
+
* matters because the settling call sites are error paths, where a `tap` and a
|
|
95
|
+
* `tapFailure` on the same chain, or a retry, is exactly the shape that fires
|
|
96
|
+
* twice and doubles a failure count.
|
|
97
|
+
*/
|
|
98
|
+
const observe = (observers, operation) => {
|
|
99
|
+
const settlers = observers.map((observer) => observer(operation));
|
|
100
|
+
let done = false;
|
|
101
|
+
return (settled) => {
|
|
102
|
+
if (done) return;
|
|
103
|
+
done = true;
|
|
104
|
+
for (const settle of settlers) settle(settled);
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
64
108
|
//#region src/clock.ts
|
|
65
109
|
const systemClock = {
|
|
66
110
|
now: () => Date.now(),
|
|
@@ -342,7 +386,6 @@ const createUnitRegistry = () => {
|
|
|
342
386
|
unitId: nextId(),
|
|
343
387
|
traceId: meta.traceId ?? meta.id,
|
|
344
388
|
tenantId: meta.tenantId,
|
|
345
|
-
deadline: meta.deadline,
|
|
346
389
|
signal: controller.signal
|
|
347
390
|
};
|
|
348
391
|
return (0, unthrown.fromSafePromise)(runWithUnit(record, async () => {
|
|
@@ -652,11 +695,14 @@ exports.Instrumentations = Instrumentations;
|
|
|
652
695
|
exports.LEVELS = LEVELS;
|
|
653
696
|
exports.Logger = Logger;
|
|
654
697
|
exports.Meter = Meter;
|
|
698
|
+
exports.Observers = Observers;
|
|
655
699
|
exports.RuntimePort = RuntimePort;
|
|
656
700
|
exports.RuntimeStartFailed = RuntimeStartFailed;
|
|
657
701
|
exports.SPAN_STATUS = SPAN_STATUS;
|
|
658
702
|
exports.Tracer = Tracer;
|
|
659
703
|
exports.currentUnit = currentUnit;
|
|
704
|
+
exports.noObserver = noObserver;
|
|
705
|
+
exports.observe = observe;
|
|
660
706
|
exports.releasedBy = releasedBy;
|
|
661
707
|
exports.runHealthChecks = runHealthChecks;
|
|
662
708
|
exports.runMain = runMain;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AsyncResult, Result } from "unthrown";
|
|
2
|
-
import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
|
|
2
|
+
import { AnyPort, Context, Module, PortClass, PortInstance, Scope, ServiceOf } from "@btravstack/di";
|
|
3
3
|
import { Env, Environment } from "@btravstack/config";
|
|
4
4
|
//#region src/instrumentation.d.ts
|
|
5
5
|
/**
|
|
@@ -83,6 +83,216 @@ type HealthReport = {
|
|
|
83
83
|
*/
|
|
84
84
|
declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
|
|
85
85
|
//#endregion
|
|
86
|
+
//#region src/observability.d.ts
|
|
87
|
+
/**
|
|
88
|
+
* The severity of one line, and the whole of the set: six levels, ordered, with
|
|
89
|
+
* no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
|
|
90
|
+
* startup and `isEnabled` be a comparison rather than a lookup.
|
|
91
|
+
*/
|
|
92
|
+
type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
93
|
+
/** The levels in order, least severe first — what `isEnabled` compares through. */
|
|
94
|
+
declare const LEVELS: readonly Level[];
|
|
95
|
+
/**
|
|
96
|
+
* What a line, a span or a measurement carries besides itself: a flat record of
|
|
97
|
+
* scalars.
|
|
98
|
+
*
|
|
99
|
+
* Flat and scalar on purpose. A nested object is where a field's name stops
|
|
100
|
+
* being stable, and an `unknown` value is where a logger starts stringifying
|
|
101
|
+
* whatever it is handed — which is how a log call becomes the thing that
|
|
102
|
+
* throws. A failure has a channel of its own, `cause`.
|
|
103
|
+
*/
|
|
104
|
+
type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
|
|
105
|
+
/**
|
|
106
|
+
* Every method takes the same three arguments in the same order:
|
|
107
|
+
* `(message, attributes?, cause?)`.
|
|
108
|
+
*
|
|
109
|
+
* Uniform on purpose. A failure is not a property of severity — an `info` line
|
|
110
|
+
* reporting a recovered fault carries one too — so every level can take a
|
|
111
|
+
* `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
|
|
112
|
+
* failure with nothing else to say.
|
|
113
|
+
*/
|
|
114
|
+
type LoggerService = {
|
|
115
|
+
readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
116
|
+
readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
117
|
+
readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
118
|
+
readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
119
|
+
readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
120
|
+
/** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
|
|
121
|
+
readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
122
|
+
readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
123
|
+
/** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
|
|
124
|
+
readonly with: (attributes: Attributes) => LoggerService;
|
|
125
|
+
/** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
|
|
126
|
+
readonly isEnabled: (level: Level) => boolean;
|
|
127
|
+
};
|
|
128
|
+
declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
|
|
129
|
+
/**
|
|
130
|
+
* The application's logger, as a port — declared here and implemented in
|
|
131
|
+
* `@btravstack/observability`, because a contract every package may depend on
|
|
132
|
+
* has to be reachable without installing an implementation, and because the
|
|
133
|
+
* correlation an implementation stamps per line is the kernel's own ambient
|
|
134
|
+
* record.
|
|
135
|
+
*
|
|
136
|
+
* An implementation must not throw: a logger that throws turns an observability
|
|
137
|
+
* problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
|
|
138
|
+
* one deliberate exemption, since a lost line is not a modeled error.
|
|
139
|
+
*/
|
|
140
|
+
declare class Logger extends Logger_base<LoggerService> {}
|
|
141
|
+
/** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
|
|
142
|
+
declare const SPAN_STATUS: {
|
|
143
|
+
readonly unset: 0;
|
|
144
|
+
readonly ok: 1;
|
|
145
|
+
readonly error: 2;
|
|
146
|
+
};
|
|
147
|
+
/** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
|
|
148
|
+
type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
|
|
149
|
+
/**
|
|
150
|
+
* One span, narrowed to what a framework package does with it: label it, say
|
|
151
|
+
* how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
|
|
152
|
+
* satisfies this structurally with no translation in between.
|
|
153
|
+
*/
|
|
154
|
+
type Span = {
|
|
155
|
+
readonly setAttributes: (attributes: Attributes) => unknown;
|
|
156
|
+
readonly setStatus: (status: {
|
|
157
|
+
readonly code: SpanStatusCode;
|
|
158
|
+
readonly message?: string;
|
|
159
|
+
}) => unknown;
|
|
160
|
+
readonly end: () => void;
|
|
161
|
+
};
|
|
162
|
+
/** What a {@link Tracer} does: start a span, by name. */
|
|
163
|
+
type TracerService = {
|
|
164
|
+
readonly startSpan: (name: string) => Span;
|
|
165
|
+
};
|
|
166
|
+
declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
|
|
167
|
+
/**
|
|
168
|
+
* The application's tracer, as a port — declared without naming OpenTelemetry,
|
|
169
|
+
* because a port typed as a vendor's type points the dependency arrow outwards
|
|
170
|
+
* and makes the whole family install that vendor.
|
|
171
|
+
*/
|
|
172
|
+
declare class Tracer extends Tracer_base<TracerService> {}
|
|
173
|
+
/** A monotonic count. */
|
|
174
|
+
type Counter = {
|
|
175
|
+
readonly add: (value: number, attributes?: Attributes) => void;
|
|
176
|
+
};
|
|
177
|
+
/** A distribution of measurements. */
|
|
178
|
+
type Histogram = {
|
|
179
|
+
readonly record: (value: number, attributes?: Attributes) => void;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* What a {@link Meter} does: mint the two instruments a framework package needs
|
|
183
|
+
* — it counts what happened and measures how long it took. A gauge is something
|
|
184
|
+
* an application declares about its own domain, reaching the vendor's meter for
|
|
185
|
+
* it as it would any other adapter.
|
|
186
|
+
*/
|
|
187
|
+
type MeterService = {
|
|
188
|
+
readonly createCounter: (name: string, options?: {
|
|
189
|
+
readonly description?: string;
|
|
190
|
+
readonly unit?: string;
|
|
191
|
+
}) => Counter;
|
|
192
|
+
readonly createHistogram: (name: string, options?: {
|
|
193
|
+
readonly description?: string;
|
|
194
|
+
readonly unit?: string;
|
|
195
|
+
}) => Histogram;
|
|
196
|
+
};
|
|
197
|
+
declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
|
|
198
|
+
/** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
|
|
199
|
+
declare class Meter extends Meter_base<MeterService> {}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/observation.d.ts
|
|
202
|
+
/**
|
|
203
|
+
* One operation worth observing, named before it runs.
|
|
204
|
+
*
|
|
205
|
+
* `component` is the starter's own word for itself — `"http"`, `"cache"`,
|
|
206
|
+
* `"amqp"` — and it is what lets an observer mint per-component instrument and
|
|
207
|
+
* span names without knowing anything about the component. `name` is the
|
|
208
|
+
* operation within it (`"get"`, `"unit"`), and `attributes` are the dimensions
|
|
209
|
+
* known before the call.
|
|
210
|
+
*
|
|
211
|
+
* The dimensions are the contributor's business and their **cardinality is its
|
|
212
|
+
* responsibility**: a request path or a workflow id here mints a time series
|
|
213
|
+
* per value, which is how a metrics bill becomes the incident.
|
|
214
|
+
*/
|
|
215
|
+
type Operation = {
|
|
216
|
+
readonly component: string;
|
|
217
|
+
readonly name: string;
|
|
218
|
+
/**
|
|
219
|
+
* The DIMENSIONS: bounded, and what a metrics observer may put on an
|
|
220
|
+
* instrument. A cache operation, an HTTP method, an activity type.
|
|
221
|
+
*
|
|
222
|
+
* Their cardinality is the contributor's responsibility — a request path or a
|
|
223
|
+
* cache key here mints a time series per value, which is how a metrics bill
|
|
224
|
+
* becomes the incident. That is what {@link Operation.details} is for.
|
|
225
|
+
*/
|
|
226
|
+
readonly attributes: Attributes;
|
|
227
|
+
/**
|
|
228
|
+
* Unbounded context: a cache key, a mail subject, a URL. It rides spans and
|
|
229
|
+
* log lines, where one more field costs one more field — and **never a
|
|
230
|
+
* metric**, where it would cost a time series per value.
|
|
231
|
+
*
|
|
232
|
+
* The split is the whole reason an observer can be shared: without it every
|
|
233
|
+
* contributor would have to choose between a useful span and a safe metric.
|
|
234
|
+
*/
|
|
235
|
+
readonly details?: Attributes;
|
|
236
|
+
/**
|
|
237
|
+
* Whether a tracing observer should open a span (default `true`).
|
|
238
|
+
*
|
|
239
|
+
* `false` is for a component whose spans already come from somewhere better:
|
|
240
|
+
* `@btravstack/prisma` says so because `@prisma/instrumentation` traces at the
|
|
241
|
+
* ENGINE level — the real SQL, the connection acquisition — all of it below
|
|
242
|
+
* what a client-level wrapper can see, so a second span would cost one more
|
|
243
|
+
* per query for strictly less information. Counting and timing still happen.
|
|
244
|
+
*
|
|
245
|
+
* Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
|
|
246
|
+
* why it rides the operation rather than being configured on the observer.
|
|
247
|
+
*/
|
|
248
|
+
readonly traced?: boolean;
|
|
249
|
+
};
|
|
250
|
+
/** How an operation came out, plus whatever was only knowable at the end. */
|
|
251
|
+
type Settled = {
|
|
252
|
+
readonly outcome: "ok" | "error";
|
|
253
|
+
/** Dimensions known only at the end, merged over the operation's own — an HTTP status, a cache hit or miss. */
|
|
254
|
+
readonly attributes?: Attributes;
|
|
255
|
+
/** The failure itself, for an observer that writes a line about it. */
|
|
256
|
+
readonly cause?: unknown;
|
|
257
|
+
};
|
|
258
|
+
/** What an observer answers: a finisher, called exactly once when the operation settles. */
|
|
259
|
+
type Settle = (settled: Settled) => void;
|
|
260
|
+
declare const Observers_base: import("@btravstack/di").ManyPortClass<"Observers">;
|
|
261
|
+
/**
|
|
262
|
+
* The set port a starter contributes its observability to, and reads whole.
|
|
263
|
+
*
|
|
264
|
+
* **Called at the START and answering a finisher**, rather than told about a
|
|
265
|
+
* finished operation. That is what lets an observer open a span before the work
|
|
266
|
+
* and end it after — a span reconstructed afterwards from a duration is not the
|
|
267
|
+
* parent of anything that ran inside it.
|
|
268
|
+
*
|
|
269
|
+
* A set port rather than a `Meter` in every starter's `needs`: an observer
|
|
270
|
+
* package an application never composed contributes nothing, and a starter
|
|
271
|
+
* costs a graph no ports to have observability available. That is the
|
|
272
|
+
* difference between "instrumented by default" and "instrumented if you asked
|
|
273
|
+
* for observability", and only the second is free.
|
|
274
|
+
*
|
|
275
|
+
* **A module that reads this port contributes a no-op member of its own**, the
|
|
276
|
+
* way `otel()` does for `Instrumentations`: a collector depending on a set port
|
|
277
|
+
* NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
|
|
278
|
+
* Several no-ops in one graph cost a call each and nothing else.
|
|
279
|
+
*/
|
|
280
|
+
declare class Observers extends Observers_base<(operation: Operation) => Settle> {}
|
|
281
|
+
/** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
|
|
282
|
+
declare const noObserver: () => Settle;
|
|
283
|
+
/**
|
|
284
|
+
* Run every observer around one operation, and hand back the one finisher that
|
|
285
|
+
* settles them all.
|
|
286
|
+
*
|
|
287
|
+
* Observers are started in order and settled in order, and the finisher is
|
|
288
|
+
* **guarded**: a second call is dropped. "Called exactly once" is therefore a
|
|
289
|
+
* property of this function rather than a rule each starter re-keeps — which
|
|
290
|
+
* matters because the settling call sites are error paths, where a `tap` and a
|
|
291
|
+
* `tapFailure` on the same chain, or a retry, is exactly the shape that fires
|
|
292
|
+
* twice and doubles a failure count.
|
|
293
|
+
*/
|
|
294
|
+
declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
|
|
295
|
+
//#endregion
|
|
86
296
|
//#region src/clock.d.ts
|
|
87
297
|
type Clock = {
|
|
88
298
|
readonly now: () => number;
|
|
@@ -105,7 +315,6 @@ type UnitRecord = {
|
|
|
105
315
|
readonly unitId: string;
|
|
106
316
|
readonly traceId: string;
|
|
107
317
|
readonly tenantId: string | undefined;
|
|
108
|
-
readonly deadline: number | undefined;
|
|
109
318
|
readonly signal: AbortSignal;
|
|
110
319
|
};
|
|
111
320
|
declare const currentUnit: () => UnitRecord | undefined;
|
|
@@ -129,7 +338,6 @@ type UnitMeta = {
|
|
|
129
338
|
readonly id: string;
|
|
130
339
|
readonly traceId?: string;
|
|
131
340
|
readonly tenantId?: string;
|
|
132
|
-
readonly deadline?: number;
|
|
133
341
|
};
|
|
134
342
|
type UnitWork<T, E> = (signal: AbortSignal) => AsyncResult<T, E> | Promise<Result<T, E>> | Result<T, E>;
|
|
135
343
|
type UnitRegistry = {
|
|
@@ -305,121 +513,6 @@ type KernelEvent = {
|
|
|
305
513
|
type EventSink = (event: KernelEvent) => void;
|
|
306
514
|
declare const stderrSink: EventSink;
|
|
307
515
|
//#endregion
|
|
308
|
-
//#region src/observability.d.ts
|
|
309
|
-
/**
|
|
310
|
-
* The severity of one line, and the whole of the set: six levels, ordered, with
|
|
311
|
-
* no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
|
|
312
|
-
* startup and `isEnabled` be a comparison rather than a lookup.
|
|
313
|
-
*/
|
|
314
|
-
type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
315
|
-
/** The levels in order, least severe first — what `isEnabled` compares through. */
|
|
316
|
-
declare const LEVELS: readonly Level[];
|
|
317
|
-
/**
|
|
318
|
-
* What a line, a span or a measurement carries besides itself: a flat record of
|
|
319
|
-
* scalars.
|
|
320
|
-
*
|
|
321
|
-
* Flat and scalar on purpose. A nested object is where a field's name stops
|
|
322
|
-
* being stable, and an `unknown` value is where a logger starts stringifying
|
|
323
|
-
* whatever it is handed — which is how a log call becomes the thing that
|
|
324
|
-
* throws. A failure has a channel of its own, `cause`.
|
|
325
|
-
*/
|
|
326
|
-
type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
|
|
327
|
-
/**
|
|
328
|
-
* Every method takes the same three arguments in the same order:
|
|
329
|
-
* `(message, attributes?, cause?)`.
|
|
330
|
-
*
|
|
331
|
-
* Uniform on purpose. A failure is not a property of severity — an `info` line
|
|
332
|
-
* reporting a recovered fault carries one too — so every level can take a
|
|
333
|
-
* `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
|
|
334
|
-
* failure with nothing else to say.
|
|
335
|
-
*/
|
|
336
|
-
type LoggerService = {
|
|
337
|
-
readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
338
|
-
readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
339
|
-
readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
340
|
-
readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
341
|
-
readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
342
|
-
/** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
|
|
343
|
-
readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
344
|
-
readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
345
|
-
/** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
|
|
346
|
-
readonly with: (attributes: Attributes) => LoggerService;
|
|
347
|
-
/** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
|
|
348
|
-
readonly isEnabled: (level: Level) => boolean;
|
|
349
|
-
};
|
|
350
|
-
declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
|
|
351
|
-
/**
|
|
352
|
-
* The application's logger, as a port — declared here and implemented in
|
|
353
|
-
* `@btravstack/observability`, because a contract every package may depend on
|
|
354
|
-
* has to be reachable without installing an implementation, and because the
|
|
355
|
-
* correlation an implementation stamps per line is the kernel's own ambient
|
|
356
|
-
* record.
|
|
357
|
-
*
|
|
358
|
-
* An implementation must not throw: a logger that throws turns an observability
|
|
359
|
-
* problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
|
|
360
|
-
* one deliberate exemption, since a lost line is not a modeled error.
|
|
361
|
-
*/
|
|
362
|
-
declare class Logger extends Logger_base<LoggerService> {}
|
|
363
|
-
/** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
|
|
364
|
-
declare const SPAN_STATUS: {
|
|
365
|
-
readonly unset: 0;
|
|
366
|
-
readonly ok: 1;
|
|
367
|
-
readonly error: 2;
|
|
368
|
-
};
|
|
369
|
-
/** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
|
|
370
|
-
type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
|
|
371
|
-
/**
|
|
372
|
-
* One span, narrowed to what a framework package does with it: label it, say
|
|
373
|
-
* how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
|
|
374
|
-
* satisfies this structurally with no translation in between.
|
|
375
|
-
*/
|
|
376
|
-
type Span = {
|
|
377
|
-
readonly setAttributes: (attributes: Attributes) => unknown;
|
|
378
|
-
readonly setStatus: (status: {
|
|
379
|
-
readonly code: SpanStatusCode;
|
|
380
|
-
readonly message?: string;
|
|
381
|
-
}) => unknown;
|
|
382
|
-
readonly end: () => void;
|
|
383
|
-
};
|
|
384
|
-
/** What a {@link Tracer} does: start a span, by name. */
|
|
385
|
-
type TracerService = {
|
|
386
|
-
readonly startSpan: (name: string) => Span;
|
|
387
|
-
};
|
|
388
|
-
declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
|
|
389
|
-
/**
|
|
390
|
-
* The application's tracer, as a port — declared without naming OpenTelemetry,
|
|
391
|
-
* because a port typed as a vendor's type points the dependency arrow outwards
|
|
392
|
-
* and makes the whole family install that vendor.
|
|
393
|
-
*/
|
|
394
|
-
declare class Tracer extends Tracer_base<TracerService> {}
|
|
395
|
-
/** A monotonic count. */
|
|
396
|
-
type Counter = {
|
|
397
|
-
readonly add: (value: number, attributes?: Attributes) => void;
|
|
398
|
-
};
|
|
399
|
-
/** A distribution of measurements. */
|
|
400
|
-
type Histogram = {
|
|
401
|
-
readonly record: (value: number, attributes?: Attributes) => void;
|
|
402
|
-
};
|
|
403
|
-
/**
|
|
404
|
-
* What a {@link Meter} does: mint the two instruments a framework package needs
|
|
405
|
-
* — it counts what happened and measures how long it took. A gauge is something
|
|
406
|
-
* an application declares about its own domain, reaching the vendor's meter for
|
|
407
|
-
* it as it would any other adapter.
|
|
408
|
-
*/
|
|
409
|
-
type MeterService = {
|
|
410
|
-
readonly createCounter: (name: string, options?: {
|
|
411
|
-
readonly description?: string;
|
|
412
|
-
readonly unit?: string;
|
|
413
|
-
}) => Counter;
|
|
414
|
-
readonly createHistogram: (name: string, options?: {
|
|
415
|
-
readonly description?: string;
|
|
416
|
-
readonly unit?: string;
|
|
417
|
-
}) => Histogram;
|
|
418
|
-
};
|
|
419
|
-
declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
|
|
420
|
-
/** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
|
|
421
|
-
declare class Meter extends Meter_base<MeterService> {}
|
|
422
|
-
//#endregion
|
|
423
516
|
//#region src/phase.d.ts
|
|
424
517
|
type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";
|
|
425
518
|
//#endregion
|
|
@@ -505,18 +598,38 @@ type RunningApp<E, Info = never> = {
|
|
|
505
598
|
};
|
|
506
599
|
/**
|
|
507
600
|
* The phantom marker `start`, `runMain` and `Boot` all intersect onto their
|
|
508
|
-
* `module` parameter: `unknown` — and invisible — when the module
|
|
509
|
-
* runtime, its exports cover what that
|
|
510
|
-
* unit module's needs;
|
|
511
|
-
* site as the parameter type the argument did
|
|
601
|
+
* `module` parameter: `unknown` — and invisible — when nothing the module
|
|
602
|
+
* needs is unprovided, it exports a runtime, its exports cover what that
|
|
603
|
+
* runtime resolves and they cover the unit module's needs; a diagnostic
|
|
604
|
+
* otherwise, printed at the call site as the parameter type the argument did
|
|
605
|
+
* not match.
|
|
512
606
|
*
|
|
513
607
|
* `unknown` is the satisfied arm because intersecting it leaves the module
|
|
514
608
|
* type untouched. A runtime's `resolves` is checked against the module's
|
|
515
609
|
* exports only, never the unit module's: `RuntimeHost.ctx` is the application
|
|
516
610
|
* context, so a unit-only port would resolve to nothing there.
|
|
611
|
+
*
|
|
612
|
+
* **Unmet needs are checked FIRST, and answered in di's own words rather than
|
|
613
|
+
* a sentence of the kernel's.** A root that forgot `provides: [router]` used
|
|
614
|
+
* to be told `Type 'HttpRouterPort' is not assignable to type 'Scope'` — an
|
|
615
|
+
* internal phantom nobody has heard of — or, when the port was missing from
|
|
616
|
+
* `exports` too, `UNSATISFIED RUNTIME PORTS`, which is a correct diagnosis of
|
|
617
|
+
* the second mistake that reads as a wrong one of the first and steers the fix
|
|
618
|
+
* into the wrong list. It is the same defect `Module.build` reports, so it
|
|
619
|
+
* prints the same sentence and ends on the port that is missing.
|
|
620
|
+
*/
|
|
621
|
+
/**
|
|
622
|
+
* A port's declared id, which is what a marker names it by: an id is the short
|
|
623
|
+
* string the application wrote in `Port("…")`, where the port TYPE of a
|
|
624
|
+
* starter's own generic port — `AmqpHandlers` over a contract — expands to
|
|
625
|
+
* hundreds of characters of the caller's own schema and truncates before the
|
|
626
|
+
* name is reached.
|
|
517
627
|
*/
|
|
518
|
-
type
|
|
519
|
-
|
|
628
|
+
type PortIdOf<P> = P extends PortInstance<infer Id, infer _Service> ? Id : P;
|
|
629
|
+
type StartGate<X, UnitNeeds = never, N = never> = [Exclude<N, Scope | Env>] extends [never] ? [Extract<X, RuntimeInstance>] extends [never] ? "NO RUNTIME — the module exports no port declared over RuntimePort" : [InstanceType<RuntimeResolvesOf<X>>] extends [X] ? [Exclude<UnitNeeds, X | Scope | Env>] extends [never] ? unknown : "UNSATISFIED UNIT NEEDS — the unit module needs a port the module does not export" : "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export" : {
|
|
630
|
+
readonly "UNSATISFIED DEPENDENCIES — nothing provides": PortIdOf<Exclude<N, Scope | Env>>;
|
|
631
|
+
};
|
|
632
|
+
declare const start: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>) => RunningApp<E, RuntimeInfoOf<X>>;
|
|
520
633
|
//#endregion
|
|
521
634
|
//#region src/run-main.d.ts
|
|
522
635
|
/**
|
|
@@ -551,6 +664,6 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
|
|
|
551
664
|
* await runMain(OrderApi);
|
|
552
665
|
* ```
|
|
553
666
|
*/
|
|
554
|
-
declare const runMain: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E,
|
|
667
|
+
declare const runMain: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
|
|
555
668
|
//#endregion
|
|
556
|
-
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 };
|
|
669
|
+
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, Observers, type Operation, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Settle, type Settled, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyPort, Context, Module, PortClass, Scope, ServiceOf } from "@btravstack/di";
|
|
1
|
+
import { AnyPort, Context, Module, PortClass, PortInstance, Scope, ServiceOf } from "@btravstack/di";
|
|
2
2
|
import { AsyncResult, Result } from "unthrown";
|
|
3
3
|
import { Env, Environment } from "@btravstack/config";
|
|
4
4
|
//#region src/instrumentation.d.ts
|
|
@@ -83,6 +83,216 @@ type HealthReport = {
|
|
|
83
83
|
*/
|
|
84
84
|
declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
|
|
85
85
|
//#endregion
|
|
86
|
+
//#region src/observability.d.ts
|
|
87
|
+
/**
|
|
88
|
+
* The severity of one line, and the whole of the set: six levels, ordered, with
|
|
89
|
+
* no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
|
|
90
|
+
* startup and `isEnabled` be a comparison rather than a lookup.
|
|
91
|
+
*/
|
|
92
|
+
type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
93
|
+
/** The levels in order, least severe first — what `isEnabled` compares through. */
|
|
94
|
+
declare const LEVELS: readonly Level[];
|
|
95
|
+
/**
|
|
96
|
+
* What a line, a span or a measurement carries besides itself: a flat record of
|
|
97
|
+
* scalars.
|
|
98
|
+
*
|
|
99
|
+
* Flat and scalar on purpose. A nested object is where a field's name stops
|
|
100
|
+
* being stable, and an `unknown` value is where a logger starts stringifying
|
|
101
|
+
* whatever it is handed — which is how a log call becomes the thing that
|
|
102
|
+
* throws. A failure has a channel of its own, `cause`.
|
|
103
|
+
*/
|
|
104
|
+
type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
|
|
105
|
+
/**
|
|
106
|
+
* Every method takes the same three arguments in the same order:
|
|
107
|
+
* `(message, attributes?, cause?)`.
|
|
108
|
+
*
|
|
109
|
+
* Uniform on purpose. A failure is not a property of severity — an `info` line
|
|
110
|
+
* reporting a recovered fault carries one too — so every level can take a
|
|
111
|
+
* `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
|
|
112
|
+
* failure with nothing else to say.
|
|
113
|
+
*/
|
|
114
|
+
type LoggerService = {
|
|
115
|
+
readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
116
|
+
readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
117
|
+
readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
118
|
+
readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
119
|
+
readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
120
|
+
/** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
|
|
121
|
+
readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
122
|
+
readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
123
|
+
/** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
|
|
124
|
+
readonly with: (attributes: Attributes) => LoggerService;
|
|
125
|
+
/** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
|
|
126
|
+
readonly isEnabled: (level: Level) => boolean;
|
|
127
|
+
};
|
|
128
|
+
declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
|
|
129
|
+
/**
|
|
130
|
+
* The application's logger, as a port — declared here and implemented in
|
|
131
|
+
* `@btravstack/observability`, because a contract every package may depend on
|
|
132
|
+
* has to be reachable without installing an implementation, and because the
|
|
133
|
+
* correlation an implementation stamps per line is the kernel's own ambient
|
|
134
|
+
* record.
|
|
135
|
+
*
|
|
136
|
+
* An implementation must not throw: a logger that throws turns an observability
|
|
137
|
+
* problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
|
|
138
|
+
* one deliberate exemption, since a lost line is not a modeled error.
|
|
139
|
+
*/
|
|
140
|
+
declare class Logger extends Logger_base<LoggerService> {}
|
|
141
|
+
/** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
|
|
142
|
+
declare const SPAN_STATUS: {
|
|
143
|
+
readonly unset: 0;
|
|
144
|
+
readonly ok: 1;
|
|
145
|
+
readonly error: 2;
|
|
146
|
+
};
|
|
147
|
+
/** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
|
|
148
|
+
type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
|
|
149
|
+
/**
|
|
150
|
+
* One span, narrowed to what a framework package does with it: label it, say
|
|
151
|
+
* how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
|
|
152
|
+
* satisfies this structurally with no translation in between.
|
|
153
|
+
*/
|
|
154
|
+
type Span = {
|
|
155
|
+
readonly setAttributes: (attributes: Attributes) => unknown;
|
|
156
|
+
readonly setStatus: (status: {
|
|
157
|
+
readonly code: SpanStatusCode;
|
|
158
|
+
readonly message?: string;
|
|
159
|
+
}) => unknown;
|
|
160
|
+
readonly end: () => void;
|
|
161
|
+
};
|
|
162
|
+
/** What a {@link Tracer} does: start a span, by name. */
|
|
163
|
+
type TracerService = {
|
|
164
|
+
readonly startSpan: (name: string) => Span;
|
|
165
|
+
};
|
|
166
|
+
declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
|
|
167
|
+
/**
|
|
168
|
+
* The application's tracer, as a port — declared without naming OpenTelemetry,
|
|
169
|
+
* because a port typed as a vendor's type points the dependency arrow outwards
|
|
170
|
+
* and makes the whole family install that vendor.
|
|
171
|
+
*/
|
|
172
|
+
declare class Tracer extends Tracer_base<TracerService> {}
|
|
173
|
+
/** A monotonic count. */
|
|
174
|
+
type Counter = {
|
|
175
|
+
readonly add: (value: number, attributes?: Attributes) => void;
|
|
176
|
+
};
|
|
177
|
+
/** A distribution of measurements. */
|
|
178
|
+
type Histogram = {
|
|
179
|
+
readonly record: (value: number, attributes?: Attributes) => void;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* What a {@link Meter} does: mint the two instruments a framework package needs
|
|
183
|
+
* — it counts what happened and measures how long it took. A gauge is something
|
|
184
|
+
* an application declares about its own domain, reaching the vendor's meter for
|
|
185
|
+
* it as it would any other adapter.
|
|
186
|
+
*/
|
|
187
|
+
type MeterService = {
|
|
188
|
+
readonly createCounter: (name: string, options?: {
|
|
189
|
+
readonly description?: string;
|
|
190
|
+
readonly unit?: string;
|
|
191
|
+
}) => Counter;
|
|
192
|
+
readonly createHistogram: (name: string, options?: {
|
|
193
|
+
readonly description?: string;
|
|
194
|
+
readonly unit?: string;
|
|
195
|
+
}) => Histogram;
|
|
196
|
+
};
|
|
197
|
+
declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
|
|
198
|
+
/** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
|
|
199
|
+
declare class Meter extends Meter_base<MeterService> {}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/observation.d.ts
|
|
202
|
+
/**
|
|
203
|
+
* One operation worth observing, named before it runs.
|
|
204
|
+
*
|
|
205
|
+
* `component` is the starter's own word for itself — `"http"`, `"cache"`,
|
|
206
|
+
* `"amqp"` — and it is what lets an observer mint per-component instrument and
|
|
207
|
+
* span names without knowing anything about the component. `name` is the
|
|
208
|
+
* operation within it (`"get"`, `"unit"`), and `attributes` are the dimensions
|
|
209
|
+
* known before the call.
|
|
210
|
+
*
|
|
211
|
+
* The dimensions are the contributor's business and their **cardinality is its
|
|
212
|
+
* responsibility**: a request path or a workflow id here mints a time series
|
|
213
|
+
* per value, which is how a metrics bill becomes the incident.
|
|
214
|
+
*/
|
|
215
|
+
type Operation = {
|
|
216
|
+
readonly component: string;
|
|
217
|
+
readonly name: string;
|
|
218
|
+
/**
|
|
219
|
+
* The DIMENSIONS: bounded, and what a metrics observer may put on an
|
|
220
|
+
* instrument. A cache operation, an HTTP method, an activity type.
|
|
221
|
+
*
|
|
222
|
+
* Their cardinality is the contributor's responsibility — a request path or a
|
|
223
|
+
* cache key here mints a time series per value, which is how a metrics bill
|
|
224
|
+
* becomes the incident. That is what {@link Operation.details} is for.
|
|
225
|
+
*/
|
|
226
|
+
readonly attributes: Attributes;
|
|
227
|
+
/**
|
|
228
|
+
* Unbounded context: a cache key, a mail subject, a URL. It rides spans and
|
|
229
|
+
* log lines, where one more field costs one more field — and **never a
|
|
230
|
+
* metric**, where it would cost a time series per value.
|
|
231
|
+
*
|
|
232
|
+
* The split is the whole reason an observer can be shared: without it every
|
|
233
|
+
* contributor would have to choose between a useful span and a safe metric.
|
|
234
|
+
*/
|
|
235
|
+
readonly details?: Attributes;
|
|
236
|
+
/**
|
|
237
|
+
* Whether a tracing observer should open a span (default `true`).
|
|
238
|
+
*
|
|
239
|
+
* `false` is for a component whose spans already come from somewhere better:
|
|
240
|
+
* `@btravstack/prisma` says so because `@prisma/instrumentation` traces at the
|
|
241
|
+
* ENGINE level — the real SQL, the connection acquisition — all of it below
|
|
242
|
+
* what a client-level wrapper can see, so a second span would cost one more
|
|
243
|
+
* per query for strictly less information. Counting and timing still happen.
|
|
244
|
+
*
|
|
245
|
+
* Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
|
|
246
|
+
* why it rides the operation rather than being configured on the observer.
|
|
247
|
+
*/
|
|
248
|
+
readonly traced?: boolean;
|
|
249
|
+
};
|
|
250
|
+
/** How an operation came out, plus whatever was only knowable at the end. */
|
|
251
|
+
type Settled = {
|
|
252
|
+
readonly outcome: "ok" | "error";
|
|
253
|
+
/** Dimensions known only at the end, merged over the operation's own — an HTTP status, a cache hit or miss. */
|
|
254
|
+
readonly attributes?: Attributes;
|
|
255
|
+
/** The failure itself, for an observer that writes a line about it. */
|
|
256
|
+
readonly cause?: unknown;
|
|
257
|
+
};
|
|
258
|
+
/** What an observer answers: a finisher, called exactly once when the operation settles. */
|
|
259
|
+
type Settle = (settled: Settled) => void;
|
|
260
|
+
declare const Observers_base: import("@btravstack/di").ManyPortClass<"Observers">;
|
|
261
|
+
/**
|
|
262
|
+
* The set port a starter contributes its observability to, and reads whole.
|
|
263
|
+
*
|
|
264
|
+
* **Called at the START and answering a finisher**, rather than told about a
|
|
265
|
+
* finished operation. That is what lets an observer open a span before the work
|
|
266
|
+
* and end it after — a span reconstructed afterwards from a duration is not the
|
|
267
|
+
* parent of anything that ran inside it.
|
|
268
|
+
*
|
|
269
|
+
* A set port rather than a `Meter` in every starter's `needs`: an observer
|
|
270
|
+
* package an application never composed contributes nothing, and a starter
|
|
271
|
+
* costs a graph no ports to have observability available. That is the
|
|
272
|
+
* difference between "instrumented by default" and "instrumented if you asked
|
|
273
|
+
* for observability", and only the second is free.
|
|
274
|
+
*
|
|
275
|
+
* **A module that reads this port contributes a no-op member of its own**, the
|
|
276
|
+
* way `otel()` does for `Instrumentations`: a collector depending on a set port
|
|
277
|
+
* NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
|
|
278
|
+
* Several no-ops in one graph cost a call each and nothing else.
|
|
279
|
+
*/
|
|
280
|
+
declare class Observers extends Observers_base<(operation: Operation) => Settle> {}
|
|
281
|
+
/** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
|
|
282
|
+
declare const noObserver: () => Settle;
|
|
283
|
+
/**
|
|
284
|
+
* Run every observer around one operation, and hand back the one finisher that
|
|
285
|
+
* settles them all.
|
|
286
|
+
*
|
|
287
|
+
* Observers are started in order and settled in order, and the finisher is
|
|
288
|
+
* **guarded**: a second call is dropped. "Called exactly once" is therefore a
|
|
289
|
+
* property of this function rather than a rule each starter re-keeps — which
|
|
290
|
+
* matters because the settling call sites are error paths, where a `tap` and a
|
|
291
|
+
* `tapFailure` on the same chain, or a retry, is exactly the shape that fires
|
|
292
|
+
* twice and doubles a failure count.
|
|
293
|
+
*/
|
|
294
|
+
declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
|
|
295
|
+
//#endregion
|
|
86
296
|
//#region src/clock.d.ts
|
|
87
297
|
type Clock = {
|
|
88
298
|
readonly now: () => number;
|
|
@@ -105,7 +315,6 @@ type UnitRecord = {
|
|
|
105
315
|
readonly unitId: string;
|
|
106
316
|
readonly traceId: string;
|
|
107
317
|
readonly tenantId: string | undefined;
|
|
108
|
-
readonly deadline: number | undefined;
|
|
109
318
|
readonly signal: AbortSignal;
|
|
110
319
|
};
|
|
111
320
|
declare const currentUnit: () => UnitRecord | undefined;
|
|
@@ -129,7 +338,6 @@ type UnitMeta = {
|
|
|
129
338
|
readonly id: string;
|
|
130
339
|
readonly traceId?: string;
|
|
131
340
|
readonly tenantId?: string;
|
|
132
|
-
readonly deadline?: number;
|
|
133
341
|
};
|
|
134
342
|
type UnitWork<T, E> = (signal: AbortSignal) => AsyncResult<T, E> | Promise<Result<T, E>> | Result<T, E>;
|
|
135
343
|
type UnitRegistry = {
|
|
@@ -305,121 +513,6 @@ type KernelEvent = {
|
|
|
305
513
|
type EventSink = (event: KernelEvent) => void;
|
|
306
514
|
declare const stderrSink: EventSink;
|
|
307
515
|
//#endregion
|
|
308
|
-
//#region src/observability.d.ts
|
|
309
|
-
/**
|
|
310
|
-
* The severity of one line, and the whole of the set: six levels, ordered, with
|
|
311
|
-
* no caller-defined additions — which is what lets `LOG_LEVEL` be validated at
|
|
312
|
-
* startup and `isEnabled` be a comparison rather than a lookup.
|
|
313
|
-
*/
|
|
314
|
-
type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
315
|
-
/** The levels in order, least severe first — what `isEnabled` compares through. */
|
|
316
|
-
declare const LEVELS: readonly Level[];
|
|
317
|
-
/**
|
|
318
|
-
* What a line, a span or a measurement carries besides itself: a flat record of
|
|
319
|
-
* scalars.
|
|
320
|
-
*
|
|
321
|
-
* Flat and scalar on purpose. A nested object is where a field's name stops
|
|
322
|
-
* being stable, and an `unknown` value is where a logger starts stringifying
|
|
323
|
-
* whatever it is handed — which is how a log call becomes the thing that
|
|
324
|
-
* throws. A failure has a channel of its own, `cause`.
|
|
325
|
-
*/
|
|
326
|
-
type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
|
|
327
|
-
/**
|
|
328
|
-
* Every method takes the same three arguments in the same order:
|
|
329
|
-
* `(message, attributes?, cause?)`.
|
|
330
|
-
*
|
|
331
|
-
* Uniform on purpose. A failure is not a property of severity — an `info` line
|
|
332
|
-
* reporting a recovered fault carries one too — so every level can take a
|
|
333
|
-
* `cause`, at the cost of `logger.error("boom", undefined, cause)` for a
|
|
334
|
-
* failure with nothing else to say.
|
|
335
|
-
*/
|
|
336
|
-
type LoggerService = {
|
|
337
|
-
readonly log: (level: Level, message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
338
|
-
readonly trace: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
339
|
-
readonly debug: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
340
|
-
readonly info: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
341
|
-
readonly warn: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
342
|
-
/** `cause` is the failure itself — an `Error`, an `unthrown` `Err`'s error, a rejected value. */
|
|
343
|
-
readonly error: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
344
|
-
readonly fatal: (message: string, attributes?: Attributes, cause?: unknown) => void;
|
|
345
|
-
/** A logger carrying `attributes` on every line it writes, on top of this one's. Never mutates this one. */
|
|
346
|
-
readonly with: (attributes: Attributes) => LoggerService;
|
|
347
|
-
/** Whether a line at `level` would be written — for a payload expensive enough to be worth not building. */
|
|
348
|
-
readonly isEnabled: (level: Level) => boolean;
|
|
349
|
-
};
|
|
350
|
-
declare const Logger_base: import("@btravstack/di").PortClass<"Logger">;
|
|
351
|
-
/**
|
|
352
|
-
* The application's logger, as a port — declared here and implemented in
|
|
353
|
-
* `@btravstack/observability`, because a contract every package may depend on
|
|
354
|
-
* has to be reachable without installing an implementation, and because the
|
|
355
|
-
* correlation an implementation stamps per line is the kernel's own ambient
|
|
356
|
-
* record.
|
|
357
|
-
*
|
|
358
|
-
* An implementation must not throw: a logger that throws turns an observability
|
|
359
|
-
* problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
|
|
360
|
-
* one deliberate exemption, since a lost line is not a modeled error.
|
|
361
|
-
*/
|
|
362
|
-
declare class Logger extends Logger_base<LoggerService> {}
|
|
363
|
-
/** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
|
|
364
|
-
declare const SPAN_STATUS: {
|
|
365
|
-
readonly unset: 0;
|
|
366
|
-
readonly ok: 1;
|
|
367
|
-
readonly error: 2;
|
|
368
|
-
};
|
|
369
|
-
/** The three codes {@link SPAN_STATUS} names, as the type a {@link Span} takes. */
|
|
370
|
-
type SpanStatusCode = (typeof SPAN_STATUS)[keyof typeof SPAN_STATUS];
|
|
371
|
-
/**
|
|
372
|
-
* One span, narrowed to what a framework package does with it: label it, say
|
|
373
|
-
* how it ended, end it. The numbers are OpenTelemetry's own, so an OTel span
|
|
374
|
-
* satisfies this structurally with no translation in between.
|
|
375
|
-
*/
|
|
376
|
-
type Span = {
|
|
377
|
-
readonly setAttributes: (attributes: Attributes) => unknown;
|
|
378
|
-
readonly setStatus: (status: {
|
|
379
|
-
readonly code: SpanStatusCode;
|
|
380
|
-
readonly message?: string;
|
|
381
|
-
}) => unknown;
|
|
382
|
-
readonly end: () => void;
|
|
383
|
-
};
|
|
384
|
-
/** What a {@link Tracer} does: start a span, by name. */
|
|
385
|
-
type TracerService = {
|
|
386
|
-
readonly startSpan: (name: string) => Span;
|
|
387
|
-
};
|
|
388
|
-
declare const Tracer_base: import("@btravstack/di").PortClass<"Tracer">;
|
|
389
|
-
/**
|
|
390
|
-
* The application's tracer, as a port — declared without naming OpenTelemetry,
|
|
391
|
-
* because a port typed as a vendor's type points the dependency arrow outwards
|
|
392
|
-
* and makes the whole family install that vendor.
|
|
393
|
-
*/
|
|
394
|
-
declare class Tracer extends Tracer_base<TracerService> {}
|
|
395
|
-
/** A monotonic count. */
|
|
396
|
-
type Counter = {
|
|
397
|
-
readonly add: (value: number, attributes?: Attributes) => void;
|
|
398
|
-
};
|
|
399
|
-
/** A distribution of measurements. */
|
|
400
|
-
type Histogram = {
|
|
401
|
-
readonly record: (value: number, attributes?: Attributes) => void;
|
|
402
|
-
};
|
|
403
|
-
/**
|
|
404
|
-
* What a {@link Meter} does: mint the two instruments a framework package needs
|
|
405
|
-
* — it counts what happened and measures how long it took. A gauge is something
|
|
406
|
-
* an application declares about its own domain, reaching the vendor's meter for
|
|
407
|
-
* it as it would any other adapter.
|
|
408
|
-
*/
|
|
409
|
-
type MeterService = {
|
|
410
|
-
readonly createCounter: (name: string, options?: {
|
|
411
|
-
readonly description?: string;
|
|
412
|
-
readonly unit?: string;
|
|
413
|
-
}) => Counter;
|
|
414
|
-
readonly createHistogram: (name: string, options?: {
|
|
415
|
-
readonly description?: string;
|
|
416
|
-
readonly unit?: string;
|
|
417
|
-
}) => Histogram;
|
|
418
|
-
};
|
|
419
|
-
declare const Meter_base: import("@btravstack/di").PortClass<"Meter">;
|
|
420
|
-
/** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
|
|
421
|
-
declare class Meter extends Meter_base<MeterService> {}
|
|
422
|
-
//#endregion
|
|
423
516
|
//#region src/phase.d.ts
|
|
424
517
|
type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";
|
|
425
518
|
//#endregion
|
|
@@ -505,18 +598,38 @@ type RunningApp<E, Info = never> = {
|
|
|
505
598
|
};
|
|
506
599
|
/**
|
|
507
600
|
* The phantom marker `start`, `runMain` and `Boot` all intersect onto their
|
|
508
|
-
* `module` parameter: `unknown` — and invisible — when the module
|
|
509
|
-
* runtime, its exports cover what that
|
|
510
|
-
* unit module's needs;
|
|
511
|
-
* site as the parameter type the argument did
|
|
601
|
+
* `module` parameter: `unknown` — and invisible — when nothing the module
|
|
602
|
+
* needs is unprovided, it exports a runtime, its exports cover what that
|
|
603
|
+
* runtime resolves and they cover the unit module's needs; a diagnostic
|
|
604
|
+
* otherwise, printed at the call site as the parameter type the argument did
|
|
605
|
+
* not match.
|
|
512
606
|
*
|
|
513
607
|
* `unknown` is the satisfied arm because intersecting it leaves the module
|
|
514
608
|
* type untouched. A runtime's `resolves` is checked against the module's
|
|
515
609
|
* exports only, never the unit module's: `RuntimeHost.ctx` is the application
|
|
516
610
|
* context, so a unit-only port would resolve to nothing there.
|
|
611
|
+
*
|
|
612
|
+
* **Unmet needs are checked FIRST, and answered in di's own words rather than
|
|
613
|
+
* a sentence of the kernel's.** A root that forgot `provides: [router]` used
|
|
614
|
+
* to be told `Type 'HttpRouterPort' is not assignable to type 'Scope'` — an
|
|
615
|
+
* internal phantom nobody has heard of — or, when the port was missing from
|
|
616
|
+
* `exports` too, `UNSATISFIED RUNTIME PORTS`, which is a correct diagnosis of
|
|
617
|
+
* the second mistake that reads as a wrong one of the first and steers the fix
|
|
618
|
+
* into the wrong list. It is the same defect `Module.build` reports, so it
|
|
619
|
+
* prints the same sentence and ends on the port that is missing.
|
|
620
|
+
*/
|
|
621
|
+
/**
|
|
622
|
+
* A port's declared id, which is what a marker names it by: an id is the short
|
|
623
|
+
* string the application wrote in `Port("…")`, where the port TYPE of a
|
|
624
|
+
* starter's own generic port — `AmqpHandlers` over a contract — expands to
|
|
625
|
+
* hundreds of characters of the caller's own schema and truncates before the
|
|
626
|
+
* name is reached.
|
|
517
627
|
*/
|
|
518
|
-
type
|
|
519
|
-
|
|
628
|
+
type PortIdOf<P> = P extends PortInstance<infer Id, infer _Service> ? Id : P;
|
|
629
|
+
type StartGate<X, UnitNeeds = never, N = never> = [Exclude<N, Scope | Env>] extends [never] ? [Extract<X, RuntimeInstance>] extends [never] ? "NO RUNTIME — the module exports no port declared over RuntimePort" : [InstanceType<RuntimeResolvesOf<X>>] extends [X] ? [Exclude<UnitNeeds, X | Scope | Env>] extends [never] ? unknown : "UNSATISFIED UNIT NEEDS — the unit module needs a port the module does not export" : "UNSATISFIED RUNTIME PORTS — the runtime resolves a port the module does not export" : {
|
|
630
|
+
readonly "UNSATISFIED DEPENDENCIES — nothing provides": PortIdOf<Exclude<N, Scope | Env>>;
|
|
631
|
+
};
|
|
632
|
+
declare const start: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>) => RunningApp<E, RuntimeInfoOf<X>>;
|
|
520
633
|
//#endregion
|
|
521
634
|
//#region src/run-main.d.ts
|
|
522
635
|
/**
|
|
@@ -551,6 +664,6 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
|
|
|
551
664
|
* await runMain(OrderApi);
|
|
552
665
|
* ```
|
|
553
666
|
*/
|
|
554
|
-
declare const runMain: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X, E,
|
|
667
|
+
declare const runMain: <X, E, N, UnitX = never, UnitNeeds = never>(module: Module<X, E, N> & StartGate<X, UnitNeeds, N>, options?: StartOptions<UnitX, UnitNeeds>, exit?: (code: number) => void) => Promise<void>;
|
|
555
668
|
//#endregion
|
|
556
|
-
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 };
|
|
669
|
+
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, Observers, type Operation, type Phase, type RunUnit, type RunningApp, type Runtime, type RuntimeHost, type RuntimeInfoOf, RuntimePort, RuntimeStartFailed, SPAN_STATUS, type Serving, type Settle, type Settled, type Span, type SpanStatusCode, type StartGate, type StartOptions, type TeardownError, Tracer, type TracerService, type UnitMeta, type UnitRecord, type UnitRegistry, type UnitWork, currentUnit, noObserver, observe, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
|
package/dist/index.mjs
CHANGED
|
@@ -60,6 +60,50 @@ const runHealthChecks = (checks) => allAsync(checks.map((health) => OkAsync().fl
|
|
|
60
60
|
components
|
|
61
61
|
}));
|
|
62
62
|
//#endregion
|
|
63
|
+
//#region src/observation.ts
|
|
64
|
+
/**
|
|
65
|
+
* The set port a starter contributes its observability to, and reads whole.
|
|
66
|
+
*
|
|
67
|
+
* **Called at the START and answering a finisher**, rather than told about a
|
|
68
|
+
* finished operation. That is what lets an observer open a span before the work
|
|
69
|
+
* and end it after — a span reconstructed afterwards from a duration is not the
|
|
70
|
+
* parent of anything that ran inside it.
|
|
71
|
+
*
|
|
72
|
+
* A set port rather than a `Meter` in every starter's `needs`: an observer
|
|
73
|
+
* package an application never composed contributes nothing, and a starter
|
|
74
|
+
* costs a graph no ports to have observability available. That is the
|
|
75
|
+
* difference between "instrumented by default" and "instrumented if you asked
|
|
76
|
+
* for observability", and only the second is free.
|
|
77
|
+
*
|
|
78
|
+
* **A module that reads this port contributes a no-op member of its own**, the
|
|
79
|
+
* way `otel()` does for `Instrumentations`: a collector depending on a set port
|
|
80
|
+
* NOTHING provides is an unmet dependency, at plan time and in `Needs` alike.
|
|
81
|
+
* Several no-ops in one graph cost a call each and nothing else.
|
|
82
|
+
*/
|
|
83
|
+
var Observers = class extends Port.many("Observers") {};
|
|
84
|
+
/** The no-op member every reader of {@link Observers} contributes, so the set is never empty. */
|
|
85
|
+
const noObserver = () => () => {};
|
|
86
|
+
/**
|
|
87
|
+
* Run every observer around one operation, and hand back the one finisher that
|
|
88
|
+
* settles them all.
|
|
89
|
+
*
|
|
90
|
+
* Observers are started in order and settled in order, and the finisher is
|
|
91
|
+
* **guarded**: a second call is dropped. "Called exactly once" is therefore a
|
|
92
|
+
* property of this function rather than a rule each starter re-keeps — which
|
|
93
|
+
* matters because the settling call sites are error paths, where a `tap` and a
|
|
94
|
+
* `tapFailure` on the same chain, or a retry, is exactly the shape that fires
|
|
95
|
+
* twice and doubles a failure count.
|
|
96
|
+
*/
|
|
97
|
+
const observe = (observers, operation) => {
|
|
98
|
+
const settlers = observers.map((observer) => observer(operation));
|
|
99
|
+
let done = false;
|
|
100
|
+
return (settled) => {
|
|
101
|
+
if (done) return;
|
|
102
|
+
done = true;
|
|
103
|
+
for (const settle of settlers) settle(settled);
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
//#endregion
|
|
63
107
|
//#region src/clock.ts
|
|
64
108
|
const systemClock = {
|
|
65
109
|
now: () => Date.now(),
|
|
@@ -341,7 +385,6 @@ const createUnitRegistry = () => {
|
|
|
341
385
|
unitId: nextId(),
|
|
342
386
|
traceId: meta.traceId ?? meta.id,
|
|
343
387
|
tenantId: meta.tenantId,
|
|
344
|
-
deadline: meta.deadline,
|
|
345
388
|
signal: controller.signal
|
|
346
389
|
};
|
|
347
390
|
return fromSafePromise(runWithUnit(record, async () => {
|
|
@@ -645,4 +688,4 @@ const runMain = async (module, options = {}, exit = (code) => {
|
|
|
645
688
|
await awaitExit(start(module, options), exit);
|
|
646
689
|
};
|
|
647
690
|
//#endregion
|
|
648
|
-
export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
|
|
691
|
+
export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, noObserver, observe, 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.11.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/
|
|
57
|
-
"@btravstack/
|
|
56
|
+
"@btravstack/di": "0.11.0",
|
|
57
|
+
"@btravstack/config": "0.11.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"unthrown": "^5.0.0",
|
|
61
|
-
"@btravstack/config": "^0.
|
|
62
|
-
"@btravstack/di": "^0.
|
|
61
|
+
"@btravstack/config": "^0.11.0",
|
|
62
|
+
"@btravstack/di": "^0.11.0"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22"
|