@btravstack/observability 0.16.0 → 0.18.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
@@ -21,8 +21,6 @@ const logLevel = (options = {}) => {
21
21
  parse: (raw) => text.parse(raw).ensure(isLevel, (value) => new _btravstack_config.ConfigFieldInvalid({ reason: `must be one of ${_btravstack_core.LEVELS.join(", ")}, got ${JSON.stringify(value)}` }))
22
22
  };
23
23
  };
24
- /** The schema `observability()` binds `LoggerConfig` through — one field today, and the place a second one lands. */
25
- const loggerSchema = (level) => _btravstack_config.Config.object({ level: _btravstack_config.Config.pinned(level, logLevel()) });
26
24
  //#endregion
27
25
  //#region src/json-sink.ts
28
26
  /**
@@ -62,15 +60,22 @@ const jsonSink = (stream = process.stdout) => (line) => {
62
60
  };
63
61
  stream.write(`${safeStringify(rendered)}\n`);
64
62
  };
63
+ const serialises = (key, value) => {
64
+ try {
65
+ JSON.stringify({ [key]: value });
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ };
65
71
  const safeStringify = (rendered) => {
66
72
  try {
67
73
  return JSON.stringify(rendered);
68
74
  } catch {
75
+ const kept = Object.entries(rendered).filter(([key, value]) => serialises(key, value));
69
76
  return JSON.stringify({
70
- time: rendered["time"],
71
- level: rendered["level"],
72
- message: rendered["message"],
73
- cause: "[unserialisable]"
77
+ ...Object.fromEntries(kept),
78
+ unserialisable: Object.keys(rendered).filter((key) => !serialises(key, rendered[key]))
74
79
  });
75
80
  }
76
81
  };
@@ -115,7 +120,6 @@ const createLogger = (sink, level = "info") => {
115
120
  } catch {}
116
121
  };
117
122
  return {
118
- log: (lineLevel, message, attributes, cause) => write(lineLevel, message, attributes, cause),
119
123
  trace: (message, attributes, cause) => write("trace", message, attributes, cause),
120
124
  debug: (message, attributes, cause) => write("debug", message, attributes, cause),
121
125
  info: (message, attributes, cause) => write("info", message, attributes, cause),
@@ -151,7 +155,7 @@ var LoggerConfig = class extends (0, _btravstack_di.Port)("LoggerConfig") {};
151
155
  const observability = (options = {}) => (0, _btravstack_di.Module)("Observability")({
152
156
  needs: [_btravstack_config.Env],
153
157
  provides: [
154
- _btravstack_config.Config.provider(LoggerConfig)(loggerSchema(options.level)),
158
+ _btravstack_config.Config.provider(LoggerConfig)(_btravstack_config.Config.object({ level: _btravstack_config.Config.pinned(options.level, logLevel()) })),
155
159
  (0, _btravstack_di.Provider)(_btravstack_core.Logger)({
156
160
  inject: { config: LoggerConfig },
157
161
  sync: ({ config }) => createLogger(options.sink ?? jsonSink(), config.level)
package/dist/index.mjs CHANGED
@@ -20,8 +20,6 @@ const logLevel = (options = {}) => {
20
20
  parse: (raw) => text.parse(raw).ensure(isLevel, (value) => new ConfigFieldInvalid({ reason: `must be one of ${LEVELS.join(", ")}, got ${JSON.stringify(value)}` }))
21
21
  };
22
22
  };
23
- /** The schema `observability()` binds `LoggerConfig` through — one field today, and the place a second one lands. */
24
- const loggerSchema = (level) => Config.object({ level: Config.pinned(level, logLevel()) });
25
23
  //#endregion
26
24
  //#region src/json-sink.ts
27
25
  /**
@@ -61,15 +59,22 @@ const jsonSink = (stream = process.stdout) => (line) => {
61
59
  };
62
60
  stream.write(`${safeStringify(rendered)}\n`);
63
61
  };
62
+ const serialises = (key, value) => {
63
+ try {
64
+ JSON.stringify({ [key]: value });
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ };
64
70
  const safeStringify = (rendered) => {
65
71
  try {
66
72
  return JSON.stringify(rendered);
67
73
  } catch {
74
+ const kept = Object.entries(rendered).filter(([key, value]) => serialises(key, value));
68
75
  return JSON.stringify({
69
- time: rendered["time"],
70
- level: rendered["level"],
71
- message: rendered["message"],
72
- cause: "[unserialisable]"
76
+ ...Object.fromEntries(kept),
77
+ unserialisable: Object.keys(rendered).filter((key) => !serialises(key, rendered[key]))
73
78
  });
74
79
  }
75
80
  };
@@ -114,7 +119,6 @@ const createLogger = (sink, level = "info") => {
114
119
  } catch {}
115
120
  };
116
121
  return {
117
- log: (lineLevel, message, attributes, cause) => write(lineLevel, message, attributes, cause),
118
122
  trace: (message, attributes, cause) => write("trace", message, attributes, cause),
119
123
  debug: (message, attributes, cause) => write("debug", message, attributes, cause),
120
124
  info: (message, attributes, cause) => write("info", message, attributes, cause),
@@ -150,7 +154,7 @@ var LoggerConfig = class extends Port("LoggerConfig") {};
150
154
  const observability = (options = {}) => Module("Observability")({
151
155
  needs: [Env],
152
156
  provides: [
153
- Config.provider(LoggerConfig)(loggerSchema(options.level)),
157
+ Config.provider(LoggerConfig)(Config.object({ level: Config.pinned(options.level, logLevel()) })),
154
158
  Provider(Logger)({
155
159
  inject: { config: LoggerConfig },
156
160
  sync: ({ config }) => createLogger(options.sink ?? jsonSink(), config.level)
package/dist/otel.cjs CHANGED
@@ -7,6 +7,38 @@ let unthrown = require("unthrown");
7
7
  //#region src/otel.ts
8
8
  var OtelSdk = class extends (0, _btravstack_di.Port)("OtelSdk") {};
9
9
  /**
10
+ * OTel semantic conventions' duration buckets, in seconds. The SDK's default
11
+ * boundaries were drawn for milliseconds and would put every operation under a
12
+ * second into the first bucket.
13
+ */
14
+ const SECONDS_BUCKETS = [
15
+ .005,
16
+ .01,
17
+ .025,
18
+ .05,
19
+ .075,
20
+ .1,
21
+ .25,
22
+ .5,
23
+ .75,
24
+ 1,
25
+ 2.5,
26
+ 5,
27
+ 7.5,
28
+ 10
29
+ ];
30
+ /**
31
+ * Each open unit's span, by the record it was opened for — so an operation
32
+ * inside the unit parents on it while the observer still injects nothing.
33
+ * Weak, so an ended unit costs nothing once its record is gone.
34
+ */
35
+ const unitSpans = /* @__PURE__ */ new WeakMap();
36
+ const unitAttributes = (unit) => unit === void 0 ? {} : {
37
+ "btravstack.unit_id": unit.unitId,
38
+ "btravstack.trace_id": unit.traceId,
39
+ ...unit.tenantId === void 0 ? {} : { "btravstack.tenant_id": unit.tenantId }
40
+ };
41
+ /**
10
42
  * The OTel starter: a module providing `Tracer` and `Meter` over a `NodeSDK`
11
43
  * opened with the scope and flushed on close. Compose it beside
12
44
  * `observability()` in a root's `imports`.
@@ -54,16 +86,25 @@ const otel = (options) => (0, _btravstack_di.Module)("Otel")({
54
86
  };
55
87
  return ({ component, name, attributes, details, traced }) => {
56
88
  const meter = _opentelemetry_api.metrics.getMeter("@btravstack/observability");
57
- const span = traced === false ? void 0 : tracer.startSpan(`${component}.${name}`);
58
- span?.setAttributes({
59
- ...attributes,
60
- ...details
61
- });
89
+ const unit = (0, _btravstack_core.currentUnit)();
62
90
  const startedAt = performance.now();
91
+ const open = (startTime) => {
92
+ const parent = unit === void 0 ? void 0 : unitSpans.get(unit);
93
+ const opened = tracer.startSpan(`${component}.${name}`, startTime === void 0 ? {} : { startTime }, parent === void 0 ? void 0 : _opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), parent));
94
+ opened.setAttributes({
95
+ ...attributes,
96
+ ...details,
97
+ ...unitAttributes(unit)
98
+ });
99
+ return opened;
100
+ };
101
+ const deferred = traced !== false && unit !== void 0 && !unitSpans.has(unit);
102
+ const started = traced === false || deferred ? void 0 : open();
63
103
  const operations = instrument(counters, component, () => meter.createCounter(`btravstack.${component}.operations`, { description: `${component} operations, by operation and outcome` }));
64
104
  const duration = instrument(durations, component, () => meter.createHistogram(`btravstack.${component}.duration`, {
65
105
  description: `${component} operation duration`,
66
- unit: "ms"
106
+ unit: "s",
107
+ advice: { explicitBucketBoundaries: SECONDS_BUCKETS }
67
108
  }));
68
109
  return ({ outcome, attributes: settled }) => {
69
110
  const all = {
@@ -72,9 +113,15 @@ const otel = (options) => (0, _btravstack_di.Module)("Otel")({
72
113
  outcome
73
114
  };
74
115
  operations.add(1, all);
75
- duration.record(performance.now() - startedAt, all);
76
- if (outcome === "error") span?.setStatus({ code: _btravstack_core.SPAN_STATUS.error });
77
- span?.end();
116
+ const endedAt = performance.now();
117
+ duration.record((endedAt - startedAt) / 1e3, all);
118
+ const finish = (span) => {
119
+ if (outcome === "error") span?.setStatus({ code: _btravstack_core.SPAN_STATUS.error });
120
+ span?.end(endedAt);
121
+ };
122
+ if (!deferred) finish(started);
123
+ else if (unitSpans.has(unit)) finish(open(startedAt));
124
+ else queueMicrotask(() => finish(open(startedAt)));
78
125
  };
79
126
  };
80
127
  }
@@ -114,7 +161,9 @@ var UnitSpan = class extends (0, _btravstack_di.Port)("UnitSpan") {};
114
161
  * and `onStop` ends it on every path out.
115
162
  *
116
163
  * The correlation is the ambient record's own, carried as attributes so a span
117
- * joins the same query the logger's lines answer. The remote PARENT is
164
+ * joins the same query the logger's lines answer. Every operation observed
165
+ * inside the unit — a cache read, a stored object, a sent mail — is this
166
+ * span's CHILD and carries the same attributes. The remote PARENT is
118
167
  * deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
119
168
  * id alone, so this correlates by attribute rather than pretending to a W3C
120
169
  * parent-child edge it cannot prove.
@@ -129,14 +178,19 @@ const UnitSpanModule = (0, _btravstack_di.Module)("UnitSpan")({
129
178
  sync: ({ tracer }) => {
130
179
  const unit = (0, _btravstack_core.currentUnit)();
131
180
  const span = tracer.startSpan("unit");
132
- if (unit !== void 0) span.setAttributes({
133
- "btravstack.unit_id": unit.unitId,
134
- "btravstack.trace_id": unit.traceId,
135
- ...unit.tenantId === void 0 ? {} : { "btravstack.tenant_id": unit.tenantId }
136
- });
181
+ span.setAttributes(unitAttributes(unit));
182
+ if (unit !== void 0 && "spanContext" in span) unitSpans.set(unit, span);
137
183
  return span;
138
184
  },
139
- onStop: (span) => span.end()
185
+ onStop: (span) => {
186
+ const unit = (0, _btravstack_core.currentUnit)();
187
+ if (unit?.signal.aborted === true) span.setStatus({
188
+ code: _btravstack_core.SPAN_STATUS.error,
189
+ message: "aborted"
190
+ });
191
+ else if (unit !== void 0 && (0, _btravstack_core.unitOutcome)() !== "ok") span.setStatus({ code: _btravstack_core.SPAN_STATUS.error });
192
+ span.end();
193
+ }
140
194
  })],
141
195
  exports: [UnitSpan]
142
196
  });
package/dist/otel.d.cts CHANGED
@@ -39,7 +39,9 @@ export declare class UnitSpan extends UnitSpan_base<Span> {}
39
39
  * and `onStop` ends it on every path out.
40
40
  *
41
41
  * The correlation is the ambient record's own, carried as attributes so a span
42
- * joins the same query the logger's lines answer. The remote PARENT is
42
+ * joins the same query the logger's lines answer. Every operation observed
43
+ * inside the unit — a cache read, a stored object, a sent mail — is this
44
+ * span's CHILD and carries the same attributes. The remote PARENT is
43
45
  * deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
44
46
  * id alone, so this correlates by attribute rather than pretending to a W3C
45
47
  * parent-child edge it cannot prove.
package/dist/otel.d.mts CHANGED
@@ -39,7 +39,9 @@ export declare class UnitSpan extends UnitSpan_base<Span> {}
39
39
  * and `onStop` ends it on every path out.
40
40
  *
41
41
  * The correlation is the ambient record's own, carried as attributes so a span
42
- * joins the same query the logger's lines answer. The remote PARENT is
42
+ * joins the same query the logger's lines answer. Every operation observed
43
+ * inside the unit — a cache read, a stored object, a sent mail — is this
44
+ * span's CHILD and carries the same attributes. The remote PARENT is
43
45
  * deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
44
46
  * id alone, so this correlates by attribute rather than pretending to a W3C
45
47
  * parent-child edge it cannot prove.
package/dist/otel.mjs CHANGED
@@ -1,11 +1,43 @@
1
- import { Instrumentations, Meter, Observers, SPAN_STATUS, Tracer, currentUnit } from "@btravstack/core";
1
+ import { Instrumentations, Meter, Observers, SPAN_STATUS, Tracer, currentUnit, unitOutcome } from "@btravstack/core";
2
2
  import { Module, Port, Provider } from "@btravstack/di";
3
- import { metrics, trace } from "@opentelemetry/api";
3
+ import { context, metrics, trace } from "@opentelemetry/api";
4
4
  import { NodeSDK } from "@opentelemetry/sdk-node";
5
5
  import { fromSafePromise } from "unthrown";
6
6
  //#region src/otel.ts
7
7
  var OtelSdk = class extends Port("OtelSdk") {};
8
8
  /**
9
+ * OTel semantic conventions' duration buckets, in seconds. The SDK's default
10
+ * boundaries were drawn for milliseconds and would put every operation under a
11
+ * second into the first bucket.
12
+ */
13
+ const SECONDS_BUCKETS = [
14
+ .005,
15
+ .01,
16
+ .025,
17
+ .05,
18
+ .075,
19
+ .1,
20
+ .25,
21
+ .5,
22
+ .75,
23
+ 1,
24
+ 2.5,
25
+ 5,
26
+ 7.5,
27
+ 10
28
+ ];
29
+ /**
30
+ * Each open unit's span, by the record it was opened for — so an operation
31
+ * inside the unit parents on it while the observer still injects nothing.
32
+ * Weak, so an ended unit costs nothing once its record is gone.
33
+ */
34
+ const unitSpans = /* @__PURE__ */ new WeakMap();
35
+ const unitAttributes = (unit) => unit === void 0 ? {} : {
36
+ "btravstack.unit_id": unit.unitId,
37
+ "btravstack.trace_id": unit.traceId,
38
+ ...unit.tenantId === void 0 ? {} : { "btravstack.tenant_id": unit.tenantId }
39
+ };
40
+ /**
9
41
  * The OTel starter: a module providing `Tracer` and `Meter` over a `NodeSDK`
10
42
  * opened with the scope and flushed on close. Compose it beside
11
43
  * `observability()` in a root's `imports`.
@@ -53,16 +85,25 @@ const otel = (options) => Module("Otel")({
53
85
  };
54
86
  return ({ component, name, attributes, details, traced }) => {
55
87
  const meter = metrics.getMeter("@btravstack/observability");
56
- const span = traced === false ? void 0 : tracer.startSpan(`${component}.${name}`);
57
- span?.setAttributes({
58
- ...attributes,
59
- ...details
60
- });
88
+ const unit = currentUnit();
61
89
  const startedAt = performance.now();
90
+ const open = (startTime) => {
91
+ const parent = unit === void 0 ? void 0 : unitSpans.get(unit);
92
+ const opened = tracer.startSpan(`${component}.${name}`, startTime === void 0 ? {} : { startTime }, parent === void 0 ? void 0 : trace.setSpan(context.active(), parent));
93
+ opened.setAttributes({
94
+ ...attributes,
95
+ ...details,
96
+ ...unitAttributes(unit)
97
+ });
98
+ return opened;
99
+ };
100
+ const deferred = traced !== false && unit !== void 0 && !unitSpans.has(unit);
101
+ const started = traced === false || deferred ? void 0 : open();
62
102
  const operations = instrument(counters, component, () => meter.createCounter(`btravstack.${component}.operations`, { description: `${component} operations, by operation and outcome` }));
63
103
  const duration = instrument(durations, component, () => meter.createHistogram(`btravstack.${component}.duration`, {
64
104
  description: `${component} operation duration`,
65
- unit: "ms"
105
+ unit: "s",
106
+ advice: { explicitBucketBoundaries: SECONDS_BUCKETS }
66
107
  }));
67
108
  return ({ outcome, attributes: settled }) => {
68
109
  const all = {
@@ -71,9 +112,15 @@ const otel = (options) => Module("Otel")({
71
112
  outcome
72
113
  };
73
114
  operations.add(1, all);
74
- duration.record(performance.now() - startedAt, all);
75
- if (outcome === "error") span?.setStatus({ code: SPAN_STATUS.error });
76
- span?.end();
115
+ const endedAt = performance.now();
116
+ duration.record((endedAt - startedAt) / 1e3, all);
117
+ const finish = (span) => {
118
+ if (outcome === "error") span?.setStatus({ code: SPAN_STATUS.error });
119
+ span?.end(endedAt);
120
+ };
121
+ if (!deferred) finish(started);
122
+ else if (unitSpans.has(unit)) finish(open(startedAt));
123
+ else queueMicrotask(() => finish(open(startedAt)));
77
124
  };
78
125
  };
79
126
  }
@@ -113,7 +160,9 @@ var UnitSpan = class extends Port("UnitSpan") {};
113
160
  * and `onStop` ends it on every path out.
114
161
  *
115
162
  * The correlation is the ambient record's own, carried as attributes so a span
116
- * joins the same query the logger's lines answer. The remote PARENT is
163
+ * joins the same query the logger's lines answer. Every operation observed
164
+ * inside the unit — a cache read, a stored object, a sent mail — is this
165
+ * span's CHILD and carries the same attributes. The remote PARENT is
117
166
  * deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
118
167
  * id alone, so this correlates by attribute rather than pretending to a W3C
119
168
  * parent-child edge it cannot prove.
@@ -128,14 +177,19 @@ const UnitSpanModule = Module("UnitSpan")({
128
177
  sync: ({ tracer }) => {
129
178
  const unit = currentUnit();
130
179
  const span = tracer.startSpan("unit");
131
- if (unit !== void 0) span.setAttributes({
132
- "btravstack.unit_id": unit.unitId,
133
- "btravstack.trace_id": unit.traceId,
134
- ...unit.tenantId === void 0 ? {} : { "btravstack.tenant_id": unit.tenantId }
135
- });
180
+ span.setAttributes(unitAttributes(unit));
181
+ if (unit !== void 0 && "spanContext" in span) unitSpans.set(unit, span);
136
182
  return span;
137
183
  },
138
- onStop: (span) => span.end()
184
+ onStop: (span) => {
185
+ const unit = currentUnit();
186
+ if (unit?.signal.aborted === true) span.setStatus({
187
+ code: SPAN_STATUS.error,
188
+ message: "aborted"
189
+ });
190
+ else if (unit !== void 0 && unitOutcome() !== "ok") span.setStatus({ code: SPAN_STATUS.error });
191
+ span.end();
192
+ }
139
193
  })],
140
194
  exports: [UnitSpan]
141
195
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/observability",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Observability for @btravstack/core: a strict Logger port correlated with the kernel's units, a dependency-free JSON sink, and the lifecycle events as log lines",
5
5
  "keywords": [
6
6
  "dependency-injection",
@@ -65,27 +65,27 @@
65
65
  "./package.json": "./package.json"
66
66
  },
67
67
  "devDependencies": {
68
- "@btravstack/config": "0.16.0",
69
- "@btravstack/core": "0.16.0",
70
- "@btravstack/di": "0.16.0",
71
- "@btravstack/testing": "0.16.0",
68
+ "@btravstack/config": "0.18.0",
69
+ "@btravstack/core": "0.18.0",
70
+ "@btravstack/di": "0.18.0",
71
+ "@btravstack/testing": "0.18.0",
72
72
  "@btravstack/tsconfig": "0.4.0",
73
73
  "@opentelemetry/api": "1.9.1",
74
74
  "@opentelemetry/sdk-node": "0.222.0",
75
75
  "@opentelemetry/sdk-trace": "2.11.0",
76
- "@types/node": "26.4.1",
77
- "@unthrown/vitest": "5.8.0",
78
- "@vitest/coverage-v8": "4.1.11",
79
- "pino": "10.3.1",
76
+ "@types/node": "26.6.4",
77
+ "@unthrown/vitest": "5.12.0",
78
+ "@vitest/coverage-v8": "5.0.3",
79
+ "pino": "10.4.0",
80
80
  "tsdown": "0.23.0",
81
81
  "typescript": "7.0.2",
82
- "unthrown": "5.8.0",
83
- "vitest": "4.1.11"
82
+ "unthrown": "5.12.0",
83
+ "vitest": "5.0.3"
84
84
  },
85
85
  "peerDependencies": {
86
- "@btravstack/config": "^0.16.0",
87
- "@btravstack/core": "^0.16.0",
88
- "@btravstack/di": "^0.16.0",
86
+ "@btravstack/config": "^0.18.0",
87
+ "@btravstack/core": "^0.18.0",
88
+ "@btravstack/di": "^0.18.0",
89
89
  "@opentelemetry/api": "^1.9.0",
90
90
  "@opentelemetry/sdk-node": "^0.222.0",
91
91
  "pino": "^10",