@ontrails/observability 0.2.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/CHANGELOG.md +291 -0
- package/README.md +40 -0
- package/package.json +51 -0
- package/src/combine.ts +280 -0
- package/src/dev/internal/dev-state.ts +179 -0
- package/src/dev/sampling.ts +30 -0
- package/src/dev/store.ts +260 -0
- package/src/dev/tracing-resource.ts +35 -0
- package/src/dev/tracing-state.ts +47 -0
- package/src/dev/trails/tracing-query.ts +108 -0
- package/src/dev/trails/tracing-status.ts +44 -0
- package/src/dev.ts +37 -0
- package/src/formatters.ts +160 -0
- package/src/index.ts +41 -0
- package/src/memory.ts +61 -0
- package/src/otel.ts +274 -0
- package/src/renderer.ts +373 -0
- package/src/sinks.ts +176 -0
- package/src/testing.ts +49 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# @ontrails/observability
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`b3ddf91`](https://github.com/outfitter-dev/trails/commit/b3ddf918ada8211f44983512e2bbdfbc5b66d722): Prepare the first normal Trails release at `0.2.0` on `latest`, replacing the unpublished 1.0.0 source release. The target advances the original `0.1.0` source minor, which was never published under the current package names. Public packages remain in lockstep. Update consumer installation guidance and provide a temporary manifest bridge for old 1.0 beta sources. Published beta versions remain unchanged; minor 0.x releases may carry documented breaking changes.
|
|
8
|
+
|
|
9
|
+
The `1.0.0` section retained below records an unpublished preparation, not an npm release. Its accumulated changes are included in `0.2.0`; the section remains as source history alongside the published beta entries.
|
|
10
|
+
|
|
11
|
+
## 1.0.0
|
|
12
|
+
|
|
13
|
+
### Major Changes
|
|
14
|
+
|
|
15
|
+
- [`113aed6`](https://github.com/outfitter-dev/trails/commit/113aed62d20041e35b0cf9d6c1b1a18df4b88f57): Rename the dependency-light observability owner from `@ontrails/observe` to
|
|
16
|
+
`@ontrails/observability` as a pre-v1 hard cut. Update dependent packages,
|
|
17
|
+
documentation, package discovery, and the governed Regrade route; no
|
|
18
|
+
compatibility package or old import route is retained.
|
|
19
|
+
- [`0938e7b`](https://github.com/outfitter-dev/trails/commit/0938e7badc0c5470d194139d642b673658d099e0): Fold the removed `@ontrails/tracing` package into the truthful existing
|
|
20
|
+
owners: intrinsic trace contracts remain in core, developer-state tooling now
|
|
21
|
+
lives at `@ontrails/observability/dev`, and the dependency-light OTel adapter
|
|
22
|
+
lives at `@ontrails/observability/otel`. There is intentionally no root-package
|
|
23
|
+
compatibility redirect because the former root had more than one owner.
|
|
24
|
+
- [`50e2779`](https://github.com/outfitter-dev/trails/commit/50e27796d074851bccd57d7df009db749757b457): Extract the real `@ontrails/logtape` and `@ontrails/pino` adapters from the
|
|
25
|
+
temporary observability subpaths. The new packages own their namesake foreign
|
|
26
|
+
dependencies and preserve Trails record metadata, levels, redaction boundaries,
|
|
27
|
+
and lifecycle behavior; the old subpaths are removed in the pre-v1 hard cut.
|
|
28
|
+
|
|
29
|
+
Add governed Regrade transitions for both exact import replacements and expose
|
|
30
|
+
the observability adapter target through the shared adapter readiness check.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- [`88a6a62`](https://github.com/outfitter-dev/trails/commit/88a6a62a9e9e230ca6d368fa78dc3ece6c816204): Complete the v1 classification-first cutover from projection/project vocabulary
|
|
35
|
+
to derive/derived for contract-owned fact production and render/rendered for
|
|
36
|
+
surface presentation. Public type, helper, rule, relation, and report names move
|
|
37
|
+
without compatibility aliases; ordinary repository/project nouns remain
|
|
38
|
+
explicit preserves or structured review inventory.
|
|
39
|
+
- [`b1fbe57`](https://github.com/outfitter-dev/trails/commit/b1fbe574e6f44d1fecb5e3a000270955c0a77b7b): Publish Bun-validated package tarballs through an npm trusted-publishing adapter
|
|
40
|
+
binding, add exact repository metadata for each public workspace package, and
|
|
41
|
+
correct the native Bun release descriptor to its pack-only runtime boundary.
|
|
42
|
+
|
|
43
|
+
## 1.0.0-beta.50
|
|
44
|
+
|
|
45
|
+
## 1.0.0-beta.49
|
|
46
|
+
|
|
47
|
+
## 1.0.0-beta.48
|
|
48
|
+
|
|
49
|
+
## 1.0.0-beta.47
|
|
50
|
+
|
|
51
|
+
## 1.0.0-beta.46
|
|
52
|
+
|
|
53
|
+
## 1.0.0-beta.45
|
|
54
|
+
|
|
55
|
+
## 1.0.0-beta.44
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- [`b1fbe57`](https://github.com/outfitter-dev/trails/commit/b1fbe574e6f44d1fecb5e3a000270955c0a77b7b): Publish Bun-validated package tarballs through an npm trusted-publishing adapter
|
|
60
|
+
binding, add exact repository metadata for each public workspace package, and
|
|
61
|
+
correct the native Bun release descriptor to its pack-only runtime boundary.
|
|
62
|
+
|
|
63
|
+
## 1.0.0-beta.43
|
|
64
|
+
|
|
65
|
+
### Major Changes
|
|
66
|
+
|
|
67
|
+
- [`113aed6`](https://github.com/outfitter-dev/trails/commit/113aed62d20041e35b0cf9d6c1b1a18df4b88f57): Rename the dependency-light observability owner from `@ontrails/observe` to
|
|
68
|
+
`@ontrails/observability` as a pre-v1 hard cut. Update dependent packages,
|
|
69
|
+
documentation, package discovery, and the governed Regrade route; no
|
|
70
|
+
compatibility package or old import route is retained.
|
|
71
|
+
- [`0938e7b`](https://github.com/outfitter-dev/trails/commit/0938e7badc0c5470d194139d642b673658d099e0): Fold the removed `@ontrails/tracing` package into the truthful existing
|
|
72
|
+
owners: intrinsic trace contracts remain in core, developer-state tooling now
|
|
73
|
+
lives at `@ontrails/observability/dev`, and the dependency-light OTel adapter
|
|
74
|
+
lives at `@ontrails/observability/otel`. There is intentionally no root-package
|
|
75
|
+
compatibility redirect because the former root had more than one owner.
|
|
76
|
+
- [`50e2779`](https://github.com/outfitter-dev/trails/commit/50e27796d074851bccd57d7df009db749757b457): Extract the real `@ontrails/logtape` and `@ontrails/pino` adapters from the
|
|
77
|
+
temporary observability subpaths. The new packages own their namesake foreign
|
|
78
|
+
dependencies and preserve Trails record metadata, levels, redaction boundaries,
|
|
79
|
+
and lifecycle behavior; the old subpaths are removed in the pre-v1 hard cut.
|
|
80
|
+
|
|
81
|
+
Add governed Regrade transitions for both exact import replacements and expose
|
|
82
|
+
the observability adapter target through the shared adapter readiness check.
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- [`88a6a62`](https://github.com/outfitter-dev/trails/commit/88a6a62a9e9e230ca6d368fa78dc3ece6c816204): Complete the v1 classification-first cutover from projection/project vocabulary
|
|
87
|
+
to derive/derived for contract-owned fact production and render/rendered for
|
|
88
|
+
surface presentation. Public type, helper, rule, relation, and report names move
|
|
89
|
+
without compatibility aliases; ordinary repository/project nouns remain
|
|
90
|
+
explicit preserves or structured review inventory.
|
|
91
|
+
|
|
92
|
+
## 1.0.0-beta.42
|
|
93
|
+
|
|
94
|
+
## 1.0.0-beta.41
|
|
95
|
+
|
|
96
|
+
## 1.0.0-beta.40
|
|
97
|
+
|
|
98
|
+
### Major Changes
|
|
99
|
+
|
|
100
|
+
- [`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.
|
|
101
|
+
|
|
102
|
+
## 1.0.0-beta.39
|
|
103
|
+
|
|
104
|
+
## 1.0.0-beta.38
|
|
105
|
+
|
|
106
|
+
## 1.0.0-beta.37
|
|
107
|
+
|
|
108
|
+
## 1.0.0-beta.36
|
|
109
|
+
|
|
110
|
+
## 1.0.0-beta.35
|
|
111
|
+
|
|
112
|
+
### Patch Changes
|
|
113
|
+
|
|
114
|
+
- [`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.
|
|
115
|
+
|
|
116
|
+
## 1.0.0-beta.34
|
|
117
|
+
|
|
118
|
+
## 1.0.0-beta.33
|
|
119
|
+
|
|
120
|
+
## 1.0.0-beta.32
|
|
121
|
+
|
|
122
|
+
### Patch Changes
|
|
123
|
+
|
|
124
|
+
- Updated dependencies [3e5c0fc]
|
|
125
|
+
- Updated dependencies [f3c4fef]
|
|
126
|
+
- Updated dependencies [cb0a9d8]
|
|
127
|
+
- Updated dependencies [21c6dda]
|
|
128
|
+
- Updated dependencies [fe72b84]
|
|
129
|
+
- @ontrails/core@1.0.0-beta.32
|
|
130
|
+
|
|
131
|
+
## 1.0.0-beta.31
|
|
132
|
+
|
|
133
|
+
### Patch Changes
|
|
134
|
+
|
|
135
|
+
- Updated dependencies [4cd5d4e]
|
|
136
|
+
- Updated dependencies [38907cc]
|
|
137
|
+
- @ontrails/core@1.0.0-beta.31
|
|
138
|
+
|
|
139
|
+
## 1.0.0-beta.30
|
|
140
|
+
|
|
141
|
+
### Patch Changes
|
|
142
|
+
|
|
143
|
+
- @ontrails/core@1.0.0-beta.30
|
|
144
|
+
|
|
145
|
+
## 1.0.0-beta.29
|
|
146
|
+
|
|
147
|
+
### Patch Changes
|
|
148
|
+
|
|
149
|
+
- @ontrails/core@1.0.0-beta.29
|
|
150
|
+
|
|
151
|
+
## 1.0.0-beta.28
|
|
152
|
+
|
|
153
|
+
### Patch Changes
|
|
154
|
+
|
|
155
|
+
- @ontrails/core@1.0.0-beta.28
|
|
156
|
+
|
|
157
|
+
## 1.0.0-beta.27
|
|
158
|
+
|
|
159
|
+
### Patch Changes
|
|
160
|
+
|
|
161
|
+
- @ontrails/core@1.0.0-beta.27
|
|
162
|
+
|
|
163
|
+
## 1.0.0-beta.26
|
|
164
|
+
|
|
165
|
+
### Patch Changes
|
|
166
|
+
|
|
167
|
+
- Updated dependencies [1307568]
|
|
168
|
+
- Updated dependencies [371d19e]
|
|
169
|
+
- @ontrails/core@1.0.0-beta.26
|
|
170
|
+
|
|
171
|
+
## 1.0.0-beta.25
|
|
172
|
+
|
|
173
|
+
### Patch Changes
|
|
174
|
+
|
|
175
|
+
- Updated dependencies [c36aca9]
|
|
176
|
+
- Updated dependencies [3befcf1]
|
|
177
|
+
- Updated dependencies [a4f9cf6]
|
|
178
|
+
- Updated dependencies [9bcf34e]
|
|
179
|
+
- @ontrails/core@1.0.0-beta.25
|
|
180
|
+
|
|
181
|
+
## 1.0.0-beta.24
|
|
182
|
+
|
|
183
|
+
### Patch Changes
|
|
184
|
+
|
|
185
|
+
- @ontrails/core@1.0.0-beta.24
|
|
186
|
+
|
|
187
|
+
## 1.0.0-beta.23
|
|
188
|
+
|
|
189
|
+
### Patch Changes
|
|
190
|
+
|
|
191
|
+
- @ontrails/core@1.0.0-beta.23
|
|
192
|
+
|
|
193
|
+
## 1.0.0-beta.22
|
|
194
|
+
|
|
195
|
+
### Patch Changes
|
|
196
|
+
|
|
197
|
+
- @ontrails/core@1.0.0-beta.22
|
|
198
|
+
|
|
199
|
+
## 1.0.0-beta.21
|
|
200
|
+
|
|
201
|
+
### Patch Changes
|
|
202
|
+
|
|
203
|
+
- Updated dependencies [99523f2]
|
|
204
|
+
- @ontrails/core@1.0.0-beta.21
|
|
205
|
+
|
|
206
|
+
## 1.0.0-beta.20
|
|
207
|
+
|
|
208
|
+
### Patch Changes
|
|
209
|
+
|
|
210
|
+
- Updated dependencies [851a2a3]
|
|
211
|
+
- @ontrails/core@1.0.0-beta.20
|
|
212
|
+
|
|
213
|
+
## 1.0.0-beta.19
|
|
214
|
+
|
|
215
|
+
### Patch Changes
|
|
216
|
+
|
|
217
|
+
- 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.
|
|
218
|
+
- Updated dependencies [e41c382]
|
|
219
|
+
- Updated dependencies [1eb5bdc]
|
|
220
|
+
- Updated dependencies [f8d80b9]
|
|
221
|
+
- Updated dependencies [846a597]
|
|
222
|
+
- Updated dependencies [223aaad]
|
|
223
|
+
- Updated dependencies [3125f4d]
|
|
224
|
+
- Updated dependencies [2494dc6]
|
|
225
|
+
- Updated dependencies [2d53717]
|
|
226
|
+
- Updated dependencies [16cb740]
|
|
227
|
+
- Updated dependencies [8894ecb]
|
|
228
|
+
- Updated dependencies [fdf7ec9]
|
|
229
|
+
- Updated dependencies [d76be13]
|
|
230
|
+
- Updated dependencies [84f56a5]
|
|
231
|
+
- Updated dependencies [431b04c]
|
|
232
|
+
- Updated dependencies [5d88104]
|
|
233
|
+
- Updated dependencies [f04a9ef]
|
|
234
|
+
- @ontrails/core@1.0.0-beta.19
|
|
235
|
+
|
|
236
|
+
## 1.0.0-beta.18
|
|
237
|
+
|
|
238
|
+
### Patch Changes
|
|
239
|
+
|
|
240
|
+
- bf44972: Document Pino sink usage and publish-readiness checks.
|
|
241
|
+
- 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.
|
|
242
|
+
- @ontrails/core@1.0.0-beta.18
|
|
243
|
+
|
|
244
|
+
## 1.0.0-beta.17
|
|
245
|
+
|
|
246
|
+
### Patch Changes
|
|
247
|
+
|
|
248
|
+
- Updated dependencies [3dc8254]
|
|
249
|
+
- @ontrails/core@1.0.0-beta.17
|
|
250
|
+
|
|
251
|
+
## 1.0.0-beta.16
|
|
252
|
+
|
|
253
|
+
### Minor Changes
|
|
254
|
+
|
|
255
|
+
- 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.
|
|
256
|
+
- 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.
|
|
257
|
+
- 22c6c06: Accept ADR-0041 Unified Observability and ship the first activation and
|
|
258
|
+
observability primitives it depends on: activation trace records, topo-level
|
|
259
|
+
observe configuration, webhook activation materialization, signal/webhook
|
|
260
|
+
warden coaching, the `@ontrails/observe` package, sink composition, and
|
|
261
|
+
zero-dependency observe sinks.
|
|
262
|
+
|
|
263
|
+
### Patch Changes
|
|
264
|
+
|
|
265
|
+
- 6300f70: Refresh source comments and test labels for retired connector terminology as adapter guardrails become strict.
|
|
266
|
+
- e898cc4: Add repo-level Knip dead-code detection and remove stale internal exports and unused package dependencies surfaced by the new check.
|
|
267
|
+
- a8997ed: Add migration guidance for the retired `@ontrails/logging` package and align observability README examples around `@ontrails/observe`, `@ontrails/tracing`, and `@ontrails/logtape`.
|
|
268
|
+
- 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.
|
|
269
|
+
- 49c2e7d: Refresh published package README taxonomy to use adapter language instead of retired connector vocabulary.
|
|
270
|
+
- Updated dependencies [73622ae]
|
|
271
|
+
- Updated dependencies [6300f70]
|
|
272
|
+
- Updated dependencies [d172013]
|
|
273
|
+
- Updated dependencies [c3fc5c3]
|
|
274
|
+
- Updated dependencies [20d7a5c]
|
|
275
|
+
- Updated dependencies [be5fb46]
|
|
276
|
+
- Updated dependencies [e898cc4]
|
|
277
|
+
- Updated dependencies [3395234]
|
|
278
|
+
- Updated dependencies [bcdc484]
|
|
279
|
+
- Updated dependencies [331e3a9]
|
|
280
|
+
- Updated dependencies [4399fdb]
|
|
281
|
+
- Updated dependencies [4b8d13b]
|
|
282
|
+
- Updated dependencies [112b9f2]
|
|
283
|
+
- Updated dependencies [893025e]
|
|
284
|
+
- Updated dependencies [eec5e9d]
|
|
285
|
+
- Updated dependencies [ebd4434]
|
|
286
|
+
- Updated dependencies [863d473]
|
|
287
|
+
- Updated dependencies [344f2f7]
|
|
288
|
+
- Updated dependencies [26f9ffd]
|
|
289
|
+
- Updated dependencies [10eae9a]
|
|
290
|
+
- Updated dependencies [22c6c06]
|
|
291
|
+
- @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,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ontrails/observability",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/outfitter-dev/trails.git",
|
|
7
|
+
"directory": "packages/observability"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/**/*.ts",
|
|
11
|
+
"!src/**/__tests__/**",
|
|
12
|
+
"!src/**/*.test.ts",
|
|
13
|
+
"!src/**/*.test-d.ts",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.ts",
|
|
20
|
+
"./dev": "./src/dev.ts",
|
|
21
|
+
"./otel": "./src/otel.ts",
|
|
22
|
+
"./package.json": "./package.json",
|
|
23
|
+
"./testing": "./src/testing.ts"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -b",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"lint": "oxlint ./src",
|
|
30
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@ontrails/core": "^0.2.0",
|
|
34
|
+
"zod": "^4.3.5"
|
|
35
|
+
},
|
|
36
|
+
"trails": {
|
|
37
|
+
"adapterTargets": {
|
|
38
|
+
"observability": {
|
|
39
|
+
"conformance": {
|
|
40
|
+
"adapterType": "ObservabilityAdapterConformanceAdapter",
|
|
41
|
+
"casesFactory": "createObservabilityAdapterConformanceCases",
|
|
42
|
+
"runner": "runConformance"
|
|
43
|
+
},
|
|
44
|
+
"placements": [
|
|
45
|
+
"extracted"
|
|
46
|
+
],
|
|
47
|
+
"testingImport": "@ontrails/observability/testing"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
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
|
+
}
|