@btravstack/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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(),
@@ -652,11 +696,14 @@ exports.Instrumentations = Instrumentations;
652
696
  exports.LEVELS = LEVELS;
653
697
  exports.Logger = Logger;
654
698
  exports.Meter = Meter;
699
+ exports.Observers = Observers;
655
700
  exports.RuntimePort = RuntimePort;
656
701
  exports.RuntimeStartFailed = RuntimeStartFailed;
657
702
  exports.SPAN_STATUS = SPAN_STATUS;
658
703
  exports.Tracer = Tracer;
659
704
  exports.currentUnit = currentUnit;
705
+ exports.noObserver = noObserver;
706
+ exports.observe = observe;
660
707
  exports.releasedBy = releasedBy;
661
708
  exports.runHealthChecks = runHealthChecks;
662
709
  exports.runMain = runMain;
package/dist/index.d.cts CHANGED
@@ -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;
@@ -305,121 +515,6 @@ type KernelEvent = {
305
515
  type EventSink = (event: KernelEvent) => void;
306
516
  declare const stderrSink: EventSink;
307
517
  //#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
518
  //#region src/phase.d.ts
424
519
  type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";
425
520
  //#endregion
@@ -553,4 +648,4 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
553
648
  */
554
649
  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>;
555
650
  //#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 };
651
+ 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
@@ -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;
@@ -305,121 +515,6 @@ type KernelEvent = {
305
515
  type EventSink = (event: KernelEvent) => void;
306
516
  declare const stderrSink: EventSink;
307
517
  //#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
518
  //#region src/phase.d.ts
424
519
  type Phase = "building" | "starting" | "serving" | "draining" | "stopping" | "exited";
425
520
  //#endregion
@@ -553,4 +648,4 @@ declare const start: <X, E, UnitX = never, UnitNeeds = never>(module: Module<X,
553
648
  */
554
649
  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>;
555
650
  //#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 };
651
+ 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(),
@@ -645,4 +689,4 @@ const runMain = async (module, options = {}, exit = (code) => {
645
689
  await awaitExit(start(module, options), exit);
646
690
  };
647
691
  //#endregion
648
- export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock };
692
+ 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.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The application kernel: boot a di module into a running process, and stop it without losing work",
5
5
  "keywords": [
6
6
  "application",
@@ -53,13 +53,13 @@
53
53
  "typescript": "7.0.2",
54
54
  "unthrown": "5.7.0",
55
55
  "vitest": "4.1.11",
56
- "@btravstack/config": "0.9.0",
57
- "@btravstack/di": "0.9.0"
56
+ "@btravstack/config": "0.10.0",
57
+ "@btravstack/di": "0.10.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "unthrown": "^5.0.0",
61
- "@btravstack/config": "^0.9.0",
62
- "@btravstack/di": "^0.9.0"
61
+ "@btravstack/config": "^0.10.0",
62
+ "@btravstack/di": "^0.10.0"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=22"