@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 +202 -0
- package/README.md +40 -0
- package/package.json +46 -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,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
|
+
}
|