@dxos/tracing 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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/lib/browser/index.mjs +330 -513
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +330 -513
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/api.d.ts +31 -14
- package/dist/types/src/api.d.ts.map +1 -1
- package/dist/types/src/buffering-backend.d.ts +24 -0
- package/dist/types/src/buffering-backend.d.ts.map +1 -0
- package/dist/types/src/diagnostic.d.ts.map +1 -1
- package/dist/types/src/diagnostics-channel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/metrics/base.d.ts.map +1 -1
- package/dist/types/src/metrics/custom-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/map-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/time-series-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/time-usage-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/unary-counter.d.ts.map +1 -1
- package/dist/types/src/remote/index.d.ts +0 -1
- package/dist/types/src/remote/index.d.ts.map +1 -1
- package/dist/types/src/remote/metrics.d.ts.map +1 -1
- package/dist/types/src/symbols.d.ts +0 -1
- package/dist/types/src/symbols.d.ts.map +1 -1
- package/dist/types/src/trace-processor.d.ts +15 -56
- package/dist/types/src/trace-processor.d.ts.map +1 -1
- package/dist/types/src/tracing-types.d.ts +67 -0
- package/dist/types/src/tracing-types.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -12
- package/src/api.ts +235 -40
- package/src/buffering-backend.ts +112 -0
- package/src/index.ts +1 -2
- package/src/remote/index.ts +0 -1
- package/src/symbols.ts +0 -2
- package/src/trace-processor.ts +51 -263
- package/src/tracing-types.ts +77 -0
- package/src/tracing.test.ts +513 -4
- package/dist/types/src/remote/tracing.d.ts +0 -57
- package/dist/types/src/remote/tracing.d.ts.map +0 -1
- package/dist/types/src/trace-sender.d.ts +0 -9
- package/dist/types/src/trace-sender.d.ts.map +0 -1
- package/src/remote/tracing.ts +0 -175
- package/src/trace-sender.ts +0 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/tracing",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.abd8ff62ef",
|
|
4
4
|
"description": "Async utilities.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -23,22 +23,19 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"types": "dist/types/src/index.d.ts",
|
|
26
|
-
"typesVersions": {
|
|
27
|
-
"*": {}
|
|
28
|
-
},
|
|
29
26
|
"files": [
|
|
30
27
|
"dist",
|
|
31
28
|
"src"
|
|
32
29
|
],
|
|
33
30
|
"dependencies": {
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/context": "0.8.4-main.
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/log": "0.8.4-main.
|
|
39
|
-
"@dxos/protocols": "0.8.4-main.
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
31
|
+
"@dxos/codec-protobuf": "0.8.4-main.abd8ff62ef",
|
|
32
|
+
"@dxos/async": "0.8.4-main.abd8ff62ef",
|
|
33
|
+
"@dxos/context": "0.8.4-main.abd8ff62ef",
|
|
34
|
+
"@dxos/node-std": "0.8.4-main.abd8ff62ef",
|
|
35
|
+
"@dxos/log": "0.8.4-main.abd8ff62ef",
|
|
36
|
+
"@dxos/protocols": "0.8.4-main.abd8ff62ef",
|
|
37
|
+
"@dxos/util": "0.8.4-main.abd8ff62ef",
|
|
38
|
+
"@dxos/invariant": "0.8.4-main.abd8ff62ef"
|
|
42
39
|
},
|
|
43
40
|
"publishConfig": {
|
|
44
41
|
"access": "public"
|
package/src/api.ts
CHANGED
|
@@ -2,19 +2,66 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Context } from '@dxos/context';
|
|
5
|
+
import { Context, LifecycleState, Resource, TRACE_SPAN_ATTRIBUTE } from '@dxos/context';
|
|
6
6
|
import { type MaybePromise } from '@dxos/util';
|
|
7
7
|
|
|
8
8
|
import { getTracingContext } from './symbols';
|
|
9
|
-
import { TRACE_PROCESSOR,
|
|
9
|
+
import { TRACE_PROCESSOR, sanitizeClassName } from './trace-processor';
|
|
10
|
+
import type { RemoteSpan } from './tracing-types';
|
|
11
|
+
|
|
12
|
+
const LIFECYCLE_SPAN = Symbol('dxos.tracing.lifecycle-span');
|
|
13
|
+
|
|
14
|
+
/** localStorage key that switches the browser OTEL sampler from 30% to 100%. */
|
|
15
|
+
export const TRACE_ALL_KEY = 'dxos.debug.traceAll';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reads `@trace.info({ spanAttribute: true })` properties from the instance
|
|
19
|
+
* and writes them into the span attributes map.
|
|
20
|
+
*/
|
|
21
|
+
const collectSpanAttributes = (instance: any, spanAttributes: Record<string, any>) => {
|
|
22
|
+
const proto = Object.getPrototypeOf(instance);
|
|
23
|
+
if (!proto) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const tracingContext = getTracingContext(proto);
|
|
27
|
+
for (const [key, { options }] of Object.entries(tracingContext.infoProperties)) {
|
|
28
|
+
if (!options.spanAttribute) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const value = typeof instance[key] === 'function' ? instance[key]() : instance[key];
|
|
33
|
+
if (value != null) {
|
|
34
|
+
const resolved = options.enum ? options.enum[value] : String(value);
|
|
35
|
+
spanAttributes[`ctx.${key}`] = resolved;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Skip properties that throw (e.g. uninitialized).
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ResourceOptions = {
|
|
44
|
+
annotation?: symbol;
|
|
45
|
+
/**
|
|
46
|
+
* Start a lifecycle span on `open()` and end it on `close()`.
|
|
47
|
+
* `this._ctx` carries the lifecycle span's trace context, so background work
|
|
48
|
+
* (subscriptions, timers) becomes children of the lifecycle span.
|
|
49
|
+
* Direct calls within `_open` use the `_open` span's context as usual.
|
|
50
|
+
* Requires the class to extend {@link Resource}.
|
|
51
|
+
*/
|
|
52
|
+
lifecycle?: boolean;
|
|
53
|
+
};
|
|
10
54
|
|
|
11
55
|
/**
|
|
12
56
|
* Annotates a class as a tracked resource.
|
|
13
57
|
*/
|
|
14
58
|
const resource =
|
|
15
|
-
(options?:
|
|
59
|
+
(options?: ResourceOptions) =>
|
|
16
60
|
<T extends { new (...args: any[]): {} }>(constructor: T) => {
|
|
17
|
-
|
|
61
|
+
if (options?.lifecycle && !(constructor.prototype instanceof Resource)) {
|
|
62
|
+
throw new Error(`@trace.resource({ lifecycle: true }) requires ${constructor.name} to extend Resource`);
|
|
63
|
+
}
|
|
64
|
+
|
|
18
65
|
const klass = (() =>
|
|
19
66
|
class extends constructor {
|
|
20
67
|
constructor(...rest: any[]) {
|
|
@@ -22,6 +69,68 @@ const resource =
|
|
|
22
69
|
TRACE_PROCESSOR.createTraceResource({ constructor, annotation: options?.annotation, instance: this });
|
|
23
70
|
}
|
|
24
71
|
})();
|
|
72
|
+
|
|
73
|
+
if (options?.lifecycle) {
|
|
74
|
+
const sanitizedName = sanitizeClassName(constructor.name);
|
|
75
|
+
const proto = klass.prototype as any;
|
|
76
|
+
const originalOpen = proto.open;
|
|
77
|
+
const originalClose = proto.close;
|
|
78
|
+
|
|
79
|
+
proto.open = async function (ctx?: Context): Promise<any> {
|
|
80
|
+
const self = this as any;
|
|
81
|
+
|
|
82
|
+
if (self._lifecycleState !== LifecycleState.CLOSED) {
|
|
83
|
+
return originalOpen.call(this, ctx);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parentSpanContext = ctx?.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
87
|
+
const resourceEntry = TRACE_PROCESSOR.resourceInstanceIndex.get(this);
|
|
88
|
+
const spanAttributes: Record<string, any> = {};
|
|
89
|
+
if (resourceEntry) {
|
|
90
|
+
spanAttributes.entryPoint = resourceEntry.sanitizedClassName;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const remoteSpan = TRACE_PROCESSOR.tracingBackend?.startSpan({
|
|
94
|
+
name: `${sanitizedName}.lifecycle`,
|
|
95
|
+
op: 'lifecycle',
|
|
96
|
+
attributes: spanAttributes,
|
|
97
|
+
parentContext: parentSpanContext,
|
|
98
|
+
});
|
|
99
|
+
self[LIFECYCLE_SPAN] = remoteSpan;
|
|
100
|
+
|
|
101
|
+
let openCtx = ctx;
|
|
102
|
+
if (remoteSpan?.spanContext != null) {
|
|
103
|
+
const traceAttrs = { [TRACE_SPAN_ATTRIBUTE]: remoteSpan.spanContext };
|
|
104
|
+
openCtx = ctx ? ctx.derive({ attributes: traceAttrs }) : new Context({ attributes: traceAttrs });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
return await originalOpen.call(this, openCtx);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
remoteSpan?.setError?.(err);
|
|
111
|
+
remoteSpan?.end();
|
|
112
|
+
self[LIFECYCLE_SPAN] = undefined;
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
proto.close = async function (ctx?: Context): Promise<any> {
|
|
118
|
+
const self = this as any;
|
|
119
|
+
const remoteSpan: RemoteSpan | undefined = self[LIFECYCLE_SPAN];
|
|
120
|
+
try {
|
|
121
|
+
return await originalClose.call(this, ctx);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
remoteSpan?.setError?.(err);
|
|
124
|
+
throw err;
|
|
125
|
+
} finally {
|
|
126
|
+
if (remoteSpan) {
|
|
127
|
+
remoteSpan.end();
|
|
128
|
+
self[LIFECYCLE_SPAN] = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
25
134
|
Object.defineProperty(klass, 'name', { value: constructor.name });
|
|
26
135
|
return klass;
|
|
27
136
|
};
|
|
@@ -51,6 +160,9 @@ export type InfoOptions = {
|
|
|
51
160
|
* Default: 0 - objects will be stringified with toString.
|
|
52
161
|
*/
|
|
53
162
|
depth?: number | null;
|
|
163
|
+
|
|
164
|
+
/** When true, the property value is also set as an OTEL span attribute on every span created by this resource. */
|
|
165
|
+
spanAttribute?: boolean;
|
|
54
166
|
};
|
|
55
167
|
|
|
56
168
|
/**
|
|
@@ -76,6 +188,7 @@ export type SpanOptions = {
|
|
|
76
188
|
|
|
77
189
|
/**
|
|
78
190
|
* Decorator that creates a span for the execution duration of the decorated method.
|
|
191
|
+
* Calls the TracingBackend directly; no custom TracingSpan objects.
|
|
79
192
|
*/
|
|
80
193
|
const span =
|
|
81
194
|
({ showInBrowserTimeline = false, showInRemoteTracing = true, op, attributes }: SpanOptions = {}) =>
|
|
@@ -83,55 +196,141 @@ const span =
|
|
|
83
196
|
const method = descriptor.value!;
|
|
84
197
|
|
|
85
198
|
descriptor.value = async function (this: any, ...args: any) {
|
|
86
|
-
const
|
|
199
|
+
const parentCtx = args[0] instanceof Context ? args[0] : null;
|
|
200
|
+
const startTs = performance.now();
|
|
87
201
|
|
|
88
|
-
const
|
|
89
|
-
parentCtx: explicitCtx,
|
|
90
|
-
methodName: propertyKey,
|
|
91
|
-
instance: this,
|
|
92
|
-
showInBrowserTimeline,
|
|
93
|
-
showInRemoteTracing,
|
|
94
|
-
op,
|
|
95
|
-
attributes,
|
|
96
|
-
});
|
|
202
|
+
const parentSpanContext = parentCtx?.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
97
203
|
|
|
98
|
-
const
|
|
204
|
+
const resourceEntry = TRACE_PROCESSOR.resourceInstanceIndex.get(this);
|
|
205
|
+
const className = resourceEntry?.sanitizedClassName ?? sanitizeClassName(target.constructor?.name ?? 'unknown');
|
|
206
|
+
const spanName = `${className}.${propertyKey}`;
|
|
99
207
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
208
|
+
const spanAttributes: Record<string, any> = {};
|
|
209
|
+
if (resourceEntry) {
|
|
210
|
+
spanAttributes.entryPoint = resourceEntry.sanitizedClassName;
|
|
211
|
+
}
|
|
212
|
+
collectSpanAttributes(this, spanAttributes);
|
|
213
|
+
if (attributes) {
|
|
214
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
215
|
+
spanAttributes[key.startsWith('ctx.') ? key : `ctx.${key}`] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const remoteSpan = showInRemoteTracing
|
|
220
|
+
? TRACE_PROCESSOR.tracingBackend?.startSpan({
|
|
221
|
+
name: spanName,
|
|
222
|
+
op: op ?? 'function',
|
|
223
|
+
attributes: spanAttributes,
|
|
224
|
+
parentContext: parentSpanContext,
|
|
225
|
+
})
|
|
226
|
+
: undefined;
|
|
227
|
+
|
|
228
|
+
let callArgs = args;
|
|
229
|
+
if (parentCtx) {
|
|
230
|
+
const childCtx =
|
|
231
|
+
remoteSpan?.spanContext != null
|
|
232
|
+
? parentCtx.derive({ attributes: { [TRACE_SPAN_ATTRIBUTE]: remoteSpan.spanContext } })
|
|
233
|
+
: parentCtx.derive();
|
|
234
|
+
callArgs = [childCtx, ...args.slice(1)];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
return await method.apply(this, callArgs);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
remoteSpan?.setError?.(err);
|
|
241
|
+
throw err;
|
|
242
|
+
} finally {
|
|
243
|
+
remoteSpan?.end();
|
|
244
|
+
if (showInBrowserTimeline && typeof globalThis?.performance?.measure === 'function') {
|
|
245
|
+
performance.measure(spanName, { start: startTs, end: performance.now() });
|
|
108
246
|
}
|
|
109
|
-
}
|
|
247
|
+
}
|
|
110
248
|
};
|
|
111
249
|
};
|
|
112
250
|
|
|
113
|
-
const
|
|
251
|
+
const manualSpans = new Map<string, RemoteSpan>();
|
|
252
|
+
const manualSpanTimestamps = new Map<string, { name: string; startTs: number }>();
|
|
253
|
+
|
|
254
|
+
export type ManualSpanParams = {
|
|
255
|
+
id: string;
|
|
256
|
+
instance: any;
|
|
257
|
+
methodName: string;
|
|
258
|
+
parentCtx: Context | null;
|
|
259
|
+
showInBrowserTimeline?: boolean;
|
|
260
|
+
showInRemoteTracing?: boolean;
|
|
261
|
+
op?: string;
|
|
262
|
+
attributes?: Record<string, any>;
|
|
263
|
+
};
|
|
114
264
|
|
|
115
265
|
/**
|
|
116
266
|
* Creates a span that must be ended manually.
|
|
267
|
+
*
|
|
268
|
+
* Returns a child Context that carries the new span's `TRACE_SPAN_ATTRIBUTE`.
|
|
269
|
+
* Callers should use the returned ctx for downstream work so that nested
|
|
270
|
+
* `@trace.span` methods and RPC calls inherit this span as their parent
|
|
271
|
+
* and land in the same trace (rather than starting a new root).
|
|
272
|
+
*
|
|
273
|
+
* When the new span cannot be created (duplicate id, no backend, or
|
|
274
|
+
* `showInRemoteTracing: false`), the parentCtx is returned unchanged.
|
|
117
275
|
*/
|
|
118
|
-
const spanStart = (params:
|
|
119
|
-
if (
|
|
120
|
-
return;
|
|
276
|
+
const spanStart = (params: ManualSpanParams): Context | null => {
|
|
277
|
+
if (manualSpans.has(params.id) || manualSpanTimestamps.has(params.id)) {
|
|
278
|
+
return params.parentCtx;
|
|
121
279
|
}
|
|
122
280
|
|
|
123
|
-
const
|
|
124
|
-
|
|
281
|
+
const resourceEntry = TRACE_PROCESSOR.resourceInstanceIndex.get(params.instance);
|
|
282
|
+
const className = resourceEntry?.sanitizedClassName ?? 'unknown';
|
|
283
|
+
const spanName = `${className}.${params.methodName}`;
|
|
284
|
+
|
|
285
|
+
if (params.showInBrowserTimeline) {
|
|
286
|
+
manualSpanTimestamps.set(params.id, { name: spanName, startTs: performance.now() });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (params.showInRemoteTracing === false || !TRACE_PROCESSOR.tracingBackend) {
|
|
290
|
+
return params.parentCtx;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const parentSpanContext = params.parentCtx?.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
294
|
+
|
|
295
|
+
const spanAttributes: Record<string, any> = {};
|
|
296
|
+
if (resourceEntry) {
|
|
297
|
+
spanAttributes.entryPoint = resourceEntry.sanitizedClassName;
|
|
298
|
+
}
|
|
299
|
+
collectSpanAttributes(params.instance, spanAttributes);
|
|
300
|
+
if (params.attributes) {
|
|
301
|
+
for (const [key, value] of Object.entries(params.attributes)) {
|
|
302
|
+
spanAttributes[key.startsWith('ctx.') ? key : `ctx.${key}`] = value;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const remoteSpan = TRACE_PROCESSOR.tracingBackend.startSpan({
|
|
307
|
+
name: spanName,
|
|
308
|
+
op: params.op ?? 'function',
|
|
309
|
+
attributes: spanAttributes,
|
|
310
|
+
parentContext: parentSpanContext,
|
|
311
|
+
});
|
|
312
|
+
manualSpans.set(params.id, remoteSpan);
|
|
313
|
+
|
|
314
|
+
if (params.parentCtx && remoteSpan.spanContext != null) {
|
|
315
|
+
return params.parentCtx.derive({ attributes: { [TRACE_SPAN_ATTRIBUTE]: remoteSpan.spanContext } });
|
|
316
|
+
}
|
|
317
|
+
return params.parentCtx;
|
|
125
318
|
};
|
|
126
319
|
|
|
127
320
|
/**
|
|
128
321
|
* Ends a span that was started manually.
|
|
129
322
|
*/
|
|
130
323
|
const spanEnd = (id: string) => {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
324
|
+
const remoteSpan = manualSpans.get(id);
|
|
325
|
+
if (remoteSpan) {
|
|
326
|
+
remoteSpan.end();
|
|
327
|
+
manualSpans.delete(id);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const timestamps = manualSpanTimestamps.get(id);
|
|
331
|
+
if (timestamps && typeof globalThis?.performance?.measure === 'function') {
|
|
332
|
+
performance.measure(timestamps.name, { start: timestamps.startTs, end: performance.now() });
|
|
333
|
+
manualSpanTimestamps.delete(id);
|
|
135
334
|
}
|
|
136
335
|
};
|
|
137
336
|
|
|
@@ -149,9 +348,7 @@ const addLink = (parent: any, child: any, opts: AddLinkOptions = {}) => {
|
|
|
149
348
|
};
|
|
150
349
|
|
|
151
350
|
export type TraceDiagnosticProps<T> = {
|
|
152
|
-
/**
|
|
153
|
-
* Unique ID.
|
|
154
|
-
*/
|
|
351
|
+
/** Unique ID. */
|
|
155
352
|
id: string;
|
|
156
353
|
|
|
157
354
|
/**
|
|
@@ -160,9 +357,7 @@ export type TraceDiagnosticProps<T> = {
|
|
|
160
357
|
*/
|
|
161
358
|
name?: string;
|
|
162
359
|
|
|
163
|
-
/**
|
|
164
|
-
* Function that will be called to fetch the diagnostic data.
|
|
165
|
-
*/
|
|
360
|
+
/** Function that will be called to fetch the diagnostic data. */
|
|
166
361
|
fetch: () => MaybePromise<T>;
|
|
167
362
|
};
|
|
168
363
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type TraceContextData } from '@dxos/context';
|
|
6
|
+
|
|
7
|
+
import type { RemoteSpan, StartSpanOptions, TracingBackend } from './tracing-types';
|
|
8
|
+
|
|
9
|
+
export const BUFFERED_PREFIX = 'buffered-';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Span handle that records operations while no real OTEL backend is available.
|
|
13
|
+
* Once a real backend is registered, the buffered span is replayed and a
|
|
14
|
+
* {@link delegate} is set so future calls forward to the real span.
|
|
15
|
+
*/
|
|
16
|
+
class BufferedSpan implements RemoteSpan {
|
|
17
|
+
readonly spanContext: TraceContextData;
|
|
18
|
+
readonly startTime: number;
|
|
19
|
+
delegate?: RemoteSpan;
|
|
20
|
+
|
|
21
|
+
#ended = false;
|
|
22
|
+
#endTime?: number;
|
|
23
|
+
#error?: unknown;
|
|
24
|
+
#hasError = false;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
readonly options: StartSpanOptions,
|
|
28
|
+
id: number,
|
|
29
|
+
) {
|
|
30
|
+
this.spanContext = { traceparent: `${BUFFERED_PREFIX}${id}` };
|
|
31
|
+
this.startTime = Date.now();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
end(endTime?: number): void {
|
|
35
|
+
if (this.delegate) {
|
|
36
|
+
this.delegate.end(endTime);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.#endTime = endTime ?? Date.now();
|
|
40
|
+
this.#ended = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setError(err: unknown): void {
|
|
44
|
+
if (this.delegate) {
|
|
45
|
+
this.delegate.setError?.(err);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.#error = err;
|
|
49
|
+
this.#hasError = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
replay(real: RemoteSpan): void {
|
|
53
|
+
if (this.#hasError) {
|
|
54
|
+
real.setError?.(this.#error);
|
|
55
|
+
}
|
|
56
|
+
if (this.#ended) {
|
|
57
|
+
real.end(this.#endTime);
|
|
58
|
+
} else {
|
|
59
|
+
this.delegate = real;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A {@link TracingBackend} that buffers span operations until a real backend
|
|
66
|
+
* registers. On {@link drain}, buffered spans are replayed in FIFO order with
|
|
67
|
+
* parent IDs translated from synthetic `buffered-*` traceparents to real OTEL
|
|
68
|
+
* IDs, preserving the trace hierarchy.
|
|
69
|
+
*/
|
|
70
|
+
export class BufferingTracingBackend implements TracingBackend {
|
|
71
|
+
readonly #pending: BufferedSpan[] = [];
|
|
72
|
+
#counter = 0;
|
|
73
|
+
|
|
74
|
+
startSpan(options: StartSpanOptions): RemoteSpan {
|
|
75
|
+
const span = new BufferedSpan(options, ++this.#counter);
|
|
76
|
+
this.#pending.push(span);
|
|
77
|
+
return span;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Discard all buffered spans without replaying them. */
|
|
81
|
+
clear(): void {
|
|
82
|
+
this.#pending.length = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Replay all buffered spans into {@link backend}.
|
|
87
|
+
*
|
|
88
|
+
* @returns Map from synthetic buffered traceparent to real {@link TraceContextData},
|
|
89
|
+
* used by the post-drain translating wrapper to resolve stale buffered IDs
|
|
90
|
+
* still present on in-flight {@link Context} objects.
|
|
91
|
+
*/
|
|
92
|
+
drain(backend: TracingBackend): Map<string, TraceContextData> {
|
|
93
|
+
const idMap = new Map<string, TraceContextData>();
|
|
94
|
+
|
|
95
|
+
for (const buffered of this.#pending) {
|
|
96
|
+
let parentContext = buffered.options.parentContext;
|
|
97
|
+
if (parentContext && parentContext.traceparent.startsWith(BUFFERED_PREFIX)) {
|
|
98
|
+
parentContext = idMap.get(parentContext.traceparent) ?? parentContext;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const real = backend.startSpan({ ...buffered.options, parentContext, startTime: buffered.startTime });
|
|
102
|
+
|
|
103
|
+
if (real.spanContext) {
|
|
104
|
+
idMap.set(buffered.spanContext.traceparent, real.spanContext);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
buffered.replay(real);
|
|
108
|
+
}
|
|
109
|
+
this.#pending.length = 0;
|
|
110
|
+
return idMap;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,11 +7,10 @@ import { trace } from './api';
|
|
|
7
7
|
export * from './api';
|
|
8
8
|
export * from './symbols';
|
|
9
9
|
export * from './trace-processor';
|
|
10
|
-
export * from './
|
|
10
|
+
export * from './tracing-types';
|
|
11
11
|
export * from './metrics';
|
|
12
12
|
export * from './diagnostic';
|
|
13
13
|
export * from './diagnostics-channel';
|
|
14
|
-
export * from './remote/tracing';
|
|
15
14
|
export * from './remote/metrics';
|
|
16
15
|
|
|
17
16
|
trace.diagnostic({
|
package/src/remote/index.ts
CHANGED