@ontrails/observability 1.0.0-beta.42

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/CHANGELOG.md ADDED
@@ -0,0 +1,202 @@
1
+ # @ontrails/observability
2
+
3
+ ## 1.0.0-beta.42
4
+
5
+ ## 1.0.0-beta.41
6
+
7
+ ## 1.0.0-beta.40
8
+
9
+ ### Major Changes
10
+
11
+ - [`b94cf85`](https://github.com/outfitter-dev/trails/commit/b94cf85011dfe18d34dfdaa74efe381b6295d5e1): Fold LogTape and Pino log forwarding into `@ontrails/observe/logtape` and `@ontrails/observe/pino`, and remove the standalone `@ontrails/logtape` and `@ontrails/pino` workspaces.
12
+
13
+ ## 1.0.0-beta.39
14
+
15
+ ## 1.0.0-beta.38
16
+
17
+ ## 1.0.0-beta.37
18
+
19
+ ## 1.0.0-beta.36
20
+
21
+ ## 1.0.0-beta.35
22
+
23
+ ### Patch Changes
24
+
25
+ - [`417bd84`](https://github.com/outfitter-dev/trails/commit/417bd8471d0f0f47ad5f33cd2ac1c606eccd72f8): Promote signal trace helpers from tracing compatibility code to core exports, and make tracing's memory sink wrapper use the observe-owned implementation.
26
+
27
+ ## 1.0.0-beta.34
28
+
29
+ ## 1.0.0-beta.33
30
+
31
+ ## 1.0.0-beta.32
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [3e5c0fc]
36
+ - Updated dependencies [f3c4fef]
37
+ - Updated dependencies [cb0a9d8]
38
+ - Updated dependencies [21c6dda]
39
+ - Updated dependencies [fe72b84]
40
+ - @ontrails/core@1.0.0-beta.32
41
+
42
+ ## 1.0.0-beta.31
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [4cd5d4e]
47
+ - Updated dependencies [38907cc]
48
+ - @ontrails/core@1.0.0-beta.31
49
+
50
+ ## 1.0.0-beta.30
51
+
52
+ ### Patch Changes
53
+
54
+ - @ontrails/core@1.0.0-beta.30
55
+
56
+ ## 1.0.0-beta.29
57
+
58
+ ### Patch Changes
59
+
60
+ - @ontrails/core@1.0.0-beta.29
61
+
62
+ ## 1.0.0-beta.28
63
+
64
+ ### Patch Changes
65
+
66
+ - @ontrails/core@1.0.0-beta.28
67
+
68
+ ## 1.0.0-beta.27
69
+
70
+ ### Patch Changes
71
+
72
+ - @ontrails/core@1.0.0-beta.27
73
+
74
+ ## 1.0.0-beta.26
75
+
76
+ ### Patch Changes
77
+
78
+ - Updated dependencies [1307568]
79
+ - Updated dependencies [371d19e]
80
+ - @ontrails/core@1.0.0-beta.26
81
+
82
+ ## 1.0.0-beta.25
83
+
84
+ ### Patch Changes
85
+
86
+ - Updated dependencies [c36aca9]
87
+ - Updated dependencies [3befcf1]
88
+ - Updated dependencies [a4f9cf6]
89
+ - Updated dependencies [9bcf34e]
90
+ - @ontrails/core@1.0.0-beta.25
91
+
92
+ ## 1.0.0-beta.24
93
+
94
+ ### Patch Changes
95
+
96
+ - @ontrails/core@1.0.0-beta.24
97
+
98
+ ## 1.0.0-beta.23
99
+
100
+ ### Patch Changes
101
+
102
+ - @ontrails/core@1.0.0-beta.23
103
+
104
+ ## 1.0.0-beta.22
105
+
106
+ ### Patch Changes
107
+
108
+ - @ontrails/core@1.0.0-beta.22
109
+
110
+ ## 1.0.0-beta.21
111
+
112
+ ### Patch Changes
113
+
114
+ - Updated dependencies [99523f2]
115
+ - @ontrails/core@1.0.0-beta.21
116
+
117
+ ## 1.0.0-beta.20
118
+
119
+ ### Patch Changes
120
+
121
+ - Updated dependencies [851a2a3]
122
+ - @ontrails/core@1.0.0-beta.20
123
+
124
+ ## 1.0.0-beta.19
125
+
126
+ ### Patch Changes
127
+
128
+ - 1eb5bdc: Rename first-class trail composition from the `cross` API family to the `compose` family across core contracts, testing helpers, topo projections, Warden rules, CLI scaffolds, and docs. `composes`, `ctx.compose`, `composeInput`, and `Compose*` type names are now the public authoring vocabulary; topo persistence migrates legacy composition rows and graph keys forward.
129
+ - Updated dependencies [e41c382]
130
+ - Updated dependencies [1eb5bdc]
131
+ - Updated dependencies [f8d80b9]
132
+ - Updated dependencies [846a597]
133
+ - Updated dependencies [223aaad]
134
+ - Updated dependencies [3125f4d]
135
+ - Updated dependencies [2494dc6]
136
+ - Updated dependencies [2d53717]
137
+ - Updated dependencies [16cb740]
138
+ - Updated dependencies [8894ecb]
139
+ - Updated dependencies [fdf7ec9]
140
+ - Updated dependencies [d76be13]
141
+ - Updated dependencies [84f56a5]
142
+ - Updated dependencies [431b04c]
143
+ - Updated dependencies [5d88104]
144
+ - Updated dependencies [f04a9ef]
145
+ - @ontrails/core@1.0.0-beta.19
146
+
147
+ ## 1.0.0-beta.18
148
+
149
+ ### Patch Changes
150
+
151
+ - bf44972: Document Pino sink usage and publish-readiness checks.
152
+ - e0ae995: Document the v1 `@ontrails/tracing/otel` OpenTelemetry adapter boundary, including callback export, stable `trails.*` attributes, flush behavior, and the absence of a standalone `@ontrails/otel` package.
153
+ - @ontrails/core@1.0.0-beta.18
154
+
155
+ ## 1.0.0-beta.17
156
+
157
+ ### Patch Changes
158
+
159
+ - Updated dependencies [3dc8254]
160
+ - @ontrails/core@1.0.0-beta.17
161
+
162
+ ## 1.0.0-beta.16
163
+
164
+ ### Minor Changes
165
+
166
+ - d40430d: Remove the retired `@ontrails/logging` workspace from the prerelease package set. Use `@ontrails/observe` for log and trace sink contracts and `@ontrails/logtape` for LogTape forwarding.
167
+ - 9cdb0f2: Add `renderTraceTree(records: readonly TraceRecord[]): string` — a pure post-execution renderer that builds a readable execution tree from `TraceRecord` entries. Renders root spans with `●`, children with `├──`/`└──`, status glyphs (`✓`/`✗`/`⊘`), durations, and parallel-branch detection (overlapping siblings render as a bracketed group with wall-vs-total summary). Tolerates forward-compatible record shapes (signal/activation kinds, `attrs.layer` from upcoming layer composition) without crashing — unknown kinds fall through to a generic span renderer. No live streaming; the tree is drawn once after the trail completes.
168
+ - 22c6c06: Accept ADR-0041 Unified Observability and ship the first activation and
169
+ observability primitives it depends on: activation trace records, topo-level
170
+ observe configuration, webhook activation materialization, signal/webhook
171
+ warden coaching, the `@ontrails/observe` package, sink composition, and
172
+ zero-dependency observe sinks.
173
+
174
+ ### Patch Changes
175
+
176
+ - 6300f70: Refresh source comments and test labels for retired connector terminology as adapter guardrails become strict.
177
+ - e898cc4: Add repo-level Knip dead-code detection and remove stale internal exports and unused package dependencies surfaced by the new check.
178
+ - a8997ed: Add migration guidance for the retired `@ontrails/logging` package and align observability README examples around `@ontrails/observe`, `@ontrails/tracing`, and `@ontrails/logtape`.
179
+ - fe03945: Document the v1 observability package boundary: `@ontrails/observe` is the production sink contract package, while `@ontrails/tracing` remains the compatibility and developer-state package with the supported `@ontrails/tracing/otel` adapter subpath.
180
+ - 49c2e7d: Refresh published package README taxonomy to use adapter language instead of retired connector vocabulary.
181
+ - Updated dependencies [73622ae]
182
+ - Updated dependencies [6300f70]
183
+ - Updated dependencies [d172013]
184
+ - Updated dependencies [c3fc5c3]
185
+ - Updated dependencies [20d7a5c]
186
+ - Updated dependencies [be5fb46]
187
+ - Updated dependencies [e898cc4]
188
+ - Updated dependencies [3395234]
189
+ - Updated dependencies [bcdc484]
190
+ - Updated dependencies [331e3a9]
191
+ - Updated dependencies [4399fdb]
192
+ - Updated dependencies [4b8d13b]
193
+ - Updated dependencies [112b9f2]
194
+ - Updated dependencies [893025e]
195
+ - Updated dependencies [eec5e9d]
196
+ - Updated dependencies [ebd4434]
197
+ - Updated dependencies [863d473]
198
+ - Updated dependencies [344f2f7]
199
+ - Updated dependencies [26f9ffd]
200
+ - Updated dependencies [10eae9a]
201
+ - Updated dependencies [22c6c06]
202
+ - @ontrails/core@1.0.0-beta.16
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @ontrails/observability
2
+
3
+ Primitive observability contracts for Trails.
4
+
5
+ This package is the public home for log and trace sink shapes used by Trails apps and adapters. It includes zero-dependency sinks for local and server baselines, plus adapter composition for production observability.
6
+
7
+ ## V1 package boundary
8
+
9
+ Use `@ontrails/observability` for app-facing observability contracts and sinks: `LogSink`, `TraceSink`, `combine(...)`, console/file sinks, bounded memory sinks, and trace rendering.
10
+
11
+ `@ontrails/core` owns intrinsic tracing execution: `TraceRecord`, `ctx.trace()`, trace context propagation, and the process-level trace sink registry.
12
+
13
+ `@ontrails/observability/dev` owns developer-state tooling: query/status trails, the SQLite dev store, sampling helpers, and state maintenance. Import intrinsic trace records, contexts, and the process-level sink registry from `@ontrails/core`.
14
+
15
+ For v1, OpenTelemetry trace export lives at `@ontrails/observability/otel`; there is no standalone `@ontrails/otel` package. That adapter translates Trails-native `TraceRecord` values to callback-delivered OTel-shaped spans without requiring the OpenTelemetry SDK as a runtime dependency.
16
+
17
+ ```typescript
18
+ import {
19
+ combine,
20
+ createConsoleSink,
21
+ createFileSink,
22
+ createMemorySink,
23
+ } from '@ontrails/observability';
24
+
25
+ const sink = combine(
26
+ createConsoleSink(),
27
+ createFileSink('./logs/app.log'),
28
+ createMemorySink({ maxRecords: 500 })
29
+ );
30
+ ```
31
+
32
+ `createFileSink()` is append-only and does not rotate files. Use external log rotation or a production adapter when retention policy matters.
33
+
34
+ ## Extracted logging adapters
35
+
36
+ Pino and LogTape are foreign-library boundaries, so they live in extracted adapter packages rather than observability subpaths. Use `@ontrails/pino` for the real Pino integration and `@ontrails/logtape` for the real LogTape integration. `@ontrails/observability` intentionally has no dependency on either library.
37
+
38
+ ## Migration from `@ontrails/logging`
39
+
40
+ `@ontrails/logging` was retired before v1. Move sink contracts, console/file sinks, formatters, and bounded memory sinks to `@ontrails/observability`. Use `@ontrails/logtape` for LogTape forwarding, `@ontrails/pino` for Pino forwarding, `@ontrails/observability/dev` for developer-state tooling, and `@ontrails/core` for intrinsic trace contracts.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ontrails/observability",
3
+ "version": "1.0.0-beta.42",
4
+ "files": [
5
+ "src/**/*.ts",
6
+ "!src/**/__tests__/**",
7
+ "!src/**/*.test.ts",
8
+ "!src/**/*.test-d.ts",
9
+ "README.md",
10
+ "CHANGELOG.md"
11
+ ],
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./dev": "./src/dev.ts",
16
+ "./otel": "./src/otel.ts",
17
+ "./package.json": "./package.json",
18
+ "./testing": "./src/testing.ts"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -b",
22
+ "test": "bun test",
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "oxlint ./src",
25
+ "clean": "rm -rf dist *.tsbuildinfo"
26
+ },
27
+ "peerDependencies": {
28
+ "@ontrails/core": "^1.0.0-beta.42",
29
+ "zod": "^4.3.5"
30
+ },
31
+ "trails": {
32
+ "adapterTargets": {
33
+ "observability": {
34
+ "conformance": {
35
+ "adapterType": "ObservabilityAdapterConformanceAdapter",
36
+ "casesFactory": "createObservabilityAdapterConformanceCases",
37
+ "runner": "runConformance"
38
+ },
39
+ "placements": [
40
+ "extracted"
41
+ ],
42
+ "testingImport": "@ontrails/observability/testing"
43
+ }
44
+ }
45
+ }
46
+ }
package/src/combine.ts ADDED
@@ -0,0 +1,280 @@
1
+ import { ValidationError } from '@ontrails/core';
2
+ import type {
3
+ LogRecord,
4
+ LogSink,
5
+ ObserveCapabilities,
6
+ TraceRecord,
7
+ TraceSink,
8
+ } from '@ontrails/core';
9
+
10
+ type ObserveRecord = LogRecord | TraceRecord;
11
+ type ObserveSink = LogSink | TraceSink;
12
+ type SinkWrite = (record: ObserveRecord) => void | Promise<void>;
13
+ type FlushableSink = ObserveSink & { readonly flush?: () => Promise<void> };
14
+
15
+ interface SinkFailure {
16
+ readonly error: unknown;
17
+ readonly index: number;
18
+ readonly sinkName: string | undefined;
19
+ }
20
+
21
+ export interface CombinedSink {
22
+ readonly name: string;
23
+ readonly observes: ObserveCapabilities;
24
+ write(record: LogRecord): void;
25
+ write(record: TraceRecord): void | Promise<void>;
26
+ flush(): Promise<void>;
27
+ }
28
+
29
+ const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
30
+ typeof value === 'object' &&
31
+ value !== null &&
32
+ typeof (value as { readonly then?: unknown }).then === 'function';
33
+
34
+ const isLogSink = (sink: ObserveSink): sink is LogSink =>
35
+ 'name' in sink && typeof sink.name === 'string';
36
+
37
+ const isLogRecord = (record: ObserveRecord): record is LogRecord =>
38
+ 'level' in record && 'message' in record && 'timestamp' in record;
39
+
40
+ const readObserveCapabilities = (
41
+ sink: ObserveSink
42
+ ): ObserveCapabilities | undefined => {
43
+ const capabilities = (sink as { readonly observes?: unknown }).observes;
44
+ if (typeof capabilities !== 'object' || capabilities === null) {
45
+ return undefined;
46
+ }
47
+ const log = (capabilities as ObserveCapabilities).log === true;
48
+ const trace = (capabilities as ObserveCapabilities).trace === true;
49
+ if (!log && !trace) {
50
+ return undefined;
51
+ }
52
+ return Object.freeze({
53
+ ...(log ? { log: true as const } : {}),
54
+ ...(trace ? { trace: true as const } : {}),
55
+ });
56
+ };
57
+
58
+ const capabilitiesForSink = (sink: ObserveSink): ObserveCapabilities =>
59
+ readObserveCapabilities(sink) ??
60
+ Object.freeze(
61
+ isLogSink(sink) ? { log: true as const } : { trace: true as const }
62
+ );
63
+
64
+ const combineCapabilities = (
65
+ sinks: readonly ObserveSink[]
66
+ ): ObserveCapabilities => {
67
+ let log = false;
68
+ let trace = false;
69
+ for (const sink of sinks) {
70
+ const capabilities = capabilitiesForSink(sink);
71
+ log ||= capabilities.log === true;
72
+ trace ||= capabilities.trace === true;
73
+ }
74
+ return Object.freeze({
75
+ ...(log ? { log: true as const } : {}),
76
+ ...(trace ? { trace: true as const } : {}),
77
+ });
78
+ };
79
+
80
+ const sinkName = (sink: ObserveSink): string | undefined =>
81
+ 'name' in sink && typeof sink.name === 'string' ? sink.name : undefined;
82
+
83
+ const flushForSink = (sink: ObserveSink): (() => Promise<void>) | undefined => {
84
+ const { flush } = sink as FlushableSink;
85
+ return typeof flush === 'function' ? flush.bind(sink) : undefined;
86
+ };
87
+
88
+ const canReceiveRecord = (
89
+ sink: ObserveSink,
90
+ record: ObserveRecord
91
+ ): boolean => {
92
+ const capabilities = capabilitiesForSink(sink);
93
+ return isLogRecord(record)
94
+ ? capabilities.log === true
95
+ : capabilities.trace === true;
96
+ };
97
+
98
+ const describeError = (error: unknown): string =>
99
+ error instanceof Error ? error.message : String(error);
100
+
101
+ const createFailureRecord = (
102
+ failure: SinkFailure,
103
+ record: ObserveRecord
104
+ ): LogRecord => ({
105
+ category: 'observe.combine',
106
+ level: 'warn',
107
+ message: 'Observe sink write failed; continuing with remaining sinks',
108
+ metadata: {
109
+ error: describeError(failure.error),
110
+ sinkIndex: failure.index,
111
+ ...(failure.sinkName === undefined ? {} : { sinkName: failure.sinkName }),
112
+ ...(isLogRecord(record)
113
+ ? { recordCategory: record.category }
114
+ : { recordId: record.id, traceId: record.traceId }),
115
+ },
116
+ timestamp: new Date(),
117
+ });
118
+
119
+ const writeToSink = (
120
+ sink: ObserveSink,
121
+ record: ObserveRecord
122
+ ): void | Promise<void> => {
123
+ // Call via the sink so class-based sinks keep `this` bound. Detaching the
124
+ // method (e.g. `(sink.write as SinkWrite)(record)`) would lose `this` and
125
+ // any class-method `write` would throw — silently dropping records since
126
+ // combine() swallows child errors. Mirrors flushForSink's binding.
127
+ const write = sink.write as SinkWrite;
128
+ return write.call(sink, record);
129
+ };
130
+
131
+ const ignoreReportFailure = async (
132
+ result: PromiseLike<unknown>
133
+ ): Promise<void> => {
134
+ try {
135
+ await result;
136
+ } catch {
137
+ // Error reporting is best-effort.
138
+ }
139
+ };
140
+
141
+ const reportFailures = (
142
+ sinks: readonly ObserveSink[],
143
+ failures: readonly SinkFailure[],
144
+ record: ObserveRecord
145
+ ): void => {
146
+ if (failures.length === 0) {
147
+ return;
148
+ }
149
+
150
+ const logSinks = sinks
151
+ .map((sink, index) => ({ index, sink }))
152
+ .filter(
153
+ (entry): entry is { readonly index: number; readonly sink: LogSink } =>
154
+ capabilitiesForSink(entry.sink).log === true && isLogSink(entry.sink)
155
+ );
156
+
157
+ for (const failure of failures) {
158
+ const failureRecord = createFailureRecord(failure, record);
159
+ for (const entry of logSinks) {
160
+ if (entry.index === failure.index) {
161
+ continue;
162
+ }
163
+ try {
164
+ const result = entry.sink.write(failureRecord);
165
+ if (isPromiseLike(result)) {
166
+ void ignoreReportFailure(result);
167
+ }
168
+ } catch {
169
+ // Error reporting is best-effort; the original write path is already isolated.
170
+ }
171
+ }
172
+ }
173
+ };
174
+
175
+ const createFailure = (
176
+ sink: ObserveSink,
177
+ index: number,
178
+ error: unknown
179
+ ): SinkFailure => ({
180
+ error,
181
+ index,
182
+ sinkName: sinkName(sink),
183
+ });
184
+
185
+ const isolateAsyncWriteFailure = async (
186
+ result: PromiseLike<unknown>,
187
+ sink: ObserveSink,
188
+ index: number,
189
+ failures: SinkFailure[]
190
+ ): Promise<void> => {
191
+ try {
192
+ await result;
193
+ } catch (error) {
194
+ failures.push(createFailure(sink, index, error));
195
+ }
196
+ };
197
+
198
+ const reportAfterPendingWrites = async (
199
+ pending: readonly Promise<void>[],
200
+ sinks: readonly ObserveSink[],
201
+ failures: readonly SinkFailure[],
202
+ record: ObserveRecord
203
+ ): Promise<void> => {
204
+ await Promise.all(pending);
205
+ reportFailures(sinks, failures, record);
206
+ };
207
+
208
+ /**
209
+ * Compose multiple observability sinks into one fan-out sink.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const sink = combine(otelSink, fileSink)
214
+ * const app = topo('app', trails, { observe: sink })
215
+ * ```
216
+ *
217
+ * @remarks
218
+ * A child sink failure never prevents sibling sinks from receiving the same
219
+ * record. Failures are swallowed and reported to log-capable sibling sinks
220
+ * when one is present.
221
+ */
222
+ export function combine(...sinks: readonly LogSink[]): CombinedSink;
223
+ export function combine(...sinks: readonly TraceSink[]): CombinedSink;
224
+ export function combine(...sinks: readonly ObserveSink[]): CombinedSink;
225
+ export function combine(...sinks: readonly ObserveSink[]): CombinedSink {
226
+ if (sinks.length === 0) {
227
+ throw new ValidationError(
228
+ 'combine() requires at least one sink; an empty composition has no observe capabilities and would fail topo validation.'
229
+ );
230
+ }
231
+
232
+ const observes = combineCapabilities(sinks);
233
+
234
+ return {
235
+ async flush(): Promise<void> {
236
+ await Promise.all(
237
+ sinks.map(async (sink) => {
238
+ const flush = flushForSink(sink);
239
+ if (flush === undefined) {
240
+ return;
241
+ }
242
+ try {
243
+ await flush();
244
+ } catch {
245
+ // Flush follows write isolation: one broken sink should not block shutdown.
246
+ }
247
+ })
248
+ );
249
+ },
250
+ name: 'combined',
251
+ observes,
252
+ write(record: ObserveRecord): void | Promise<void> {
253
+ const failures: SinkFailure[] = [];
254
+ const pending: Promise<void>[] = [];
255
+
256
+ for (const [index, sink] of sinks.entries()) {
257
+ if (!canReceiveRecord(sink, record)) {
258
+ continue;
259
+ }
260
+ try {
261
+ const result = writeToSink(sink, record);
262
+ if (isPromiseLike(result)) {
263
+ pending.push(
264
+ isolateAsyncWriteFailure(result, sink, index, failures)
265
+ );
266
+ }
267
+ } catch (error) {
268
+ failures.push(createFailure(sink, index, error));
269
+ }
270
+ }
271
+
272
+ if (pending.length === 0) {
273
+ reportFailures(sinks, failures, record);
274
+ return undefined;
275
+ }
276
+
277
+ return reportAfterPendingWrites(pending, sinks, failures, record);
278
+ },
279
+ };
280
+ }