@dxos/tracing 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc
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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +331 -393
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +331 -393
- 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 +36 -17
- 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 +2 -2
- 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 +16 -52
- 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 +14 -13
- package/src/api.ts +237 -35
- package/src/buffering-backend.ts +112 -0
- package/src/diagnostic.ts +2 -2
- package/src/index.ts +1 -2
- package/src/remote/index.ts +0 -1
- package/src/symbols.ts +0 -2
- package/src/trace-processor.ts +58 -258
- package/src/tracing-types.ts +77 -0
- package/src/tracing.test.ts +513 -4
- package/dist/types/src/remote/tracing.d.ts +0 -23
- 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 -53
- package/src/trace-sender.ts +0 -88
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/tracing",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.bc2380dfbc",
|
|
4
4
|
"description": "Async utilities.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
13
|
"sideEffects": true,
|
|
10
14
|
"type": "module",
|
|
@@ -19,22 +23,19 @@
|
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"types": "dist/types/src/index.d.ts",
|
|
22
|
-
"typesVersions": {
|
|
23
|
-
"*": {}
|
|
24
|
-
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
|
27
28
|
"src"
|
|
28
29
|
],
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@dxos/
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
31
|
+
"@dxos/codec-protobuf": "0.8.4-main.bc2380dfbc",
|
|
32
|
+
"@dxos/async": "0.8.4-main.bc2380dfbc",
|
|
33
|
+
"@dxos/context": "0.8.4-main.bc2380dfbc",
|
|
34
|
+
"@dxos/log": "0.8.4-main.bc2380dfbc",
|
|
35
|
+
"@dxos/invariant": "0.8.4-main.bc2380dfbc",
|
|
36
|
+
"@dxos/node-std": "0.8.4-main.bc2380dfbc",
|
|
37
|
+
"@dxos/protocols": "0.8.4-main.bc2380dfbc",
|
|
38
|
+
"@dxos/util": "0.8.4-main.bc2380dfbc"
|
|
38
39
|
},
|
|
39
40
|
"publishConfig": {
|
|
40
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
|
/**
|
|
@@ -68,63 +180,157 @@ const mark = (name: string) => {
|
|
|
68
180
|
|
|
69
181
|
export type SpanOptions = {
|
|
70
182
|
showInBrowserTimeline?: boolean;
|
|
183
|
+
/** When false the span is not exported to remote OTLP collectors. Defaults to true. */
|
|
184
|
+
showInRemoteTracing?: boolean;
|
|
71
185
|
op?: string;
|
|
72
186
|
attributes?: Record<string, any>;
|
|
73
187
|
};
|
|
74
188
|
|
|
75
189
|
/**
|
|
76
190
|
* Decorator that creates a span for the execution duration of the decorated method.
|
|
191
|
+
* Calls the TracingBackend directly; no custom TracingSpan objects.
|
|
77
192
|
*/
|
|
78
193
|
const span =
|
|
79
|
-
({ showInBrowserTimeline = false, op, attributes }: SpanOptions = {}) =>
|
|
194
|
+
({ showInBrowserTimeline = false, showInRemoteTracing = true, op, attributes }: SpanOptions = {}) =>
|
|
80
195
|
(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<(...args: any) => any>) => {
|
|
81
196
|
const method = descriptor.value!;
|
|
82
197
|
|
|
83
198
|
descriptor.value = async function (this: any, ...args: any) {
|
|
84
199
|
const parentCtx = args[0] instanceof Context ? args[0] : null;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
200
|
+
const startTs = performance.now();
|
|
201
|
+
|
|
202
|
+
const parentSpanContext = parentCtx?.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
203
|
+
|
|
204
|
+
const resourceEntry = TRACE_PROCESSOR.resourceInstanceIndex.get(this);
|
|
205
|
+
const className = resourceEntry?.sanitizedClassName ?? sanitizeClassName(target.constructor?.name ?? 'unknown');
|
|
206
|
+
const spanName = `${className}.${propertyKey}`;
|
|
207
|
+
|
|
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
|
+
|
|
95
237
|
try {
|
|
96
238
|
return await method.apply(this, callArgs);
|
|
97
239
|
} catch (err) {
|
|
98
|
-
|
|
240
|
+
remoteSpan?.setError?.(err);
|
|
99
241
|
throw err;
|
|
100
242
|
} finally {
|
|
101
|
-
|
|
243
|
+
remoteSpan?.end();
|
|
244
|
+
if (showInBrowserTimeline && typeof globalThis?.performance?.measure === 'function') {
|
|
245
|
+
performance.measure(spanName, { start: startTs, end: performance.now() });
|
|
246
|
+
}
|
|
102
247
|
}
|
|
103
248
|
};
|
|
104
249
|
};
|
|
105
250
|
|
|
106
|
-
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
|
+
};
|
|
107
264
|
|
|
108
265
|
/**
|
|
109
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.
|
|
110
275
|
*/
|
|
111
|
-
const spanStart = (params:
|
|
112
|
-
if (
|
|
113
|
-
return;
|
|
276
|
+
const spanStart = (params: ManualSpanParams): Context | null => {
|
|
277
|
+
if (manualSpans.has(params.id) || manualSpanTimestamps.has(params.id)) {
|
|
278
|
+
return params.parentCtx;
|
|
279
|
+
}
|
|
280
|
+
|
|
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
|
+
}
|
|
114
304
|
}
|
|
115
305
|
|
|
116
|
-
const
|
|
117
|
-
|
|
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;
|
|
118
318
|
};
|
|
119
319
|
|
|
120
320
|
/**
|
|
121
321
|
* Ends a span that was started manually.
|
|
122
322
|
*/
|
|
123
323
|
const spanEnd = (id: string) => {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
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);
|
|
128
334
|
}
|
|
129
335
|
};
|
|
130
336
|
|
|
@@ -141,10 +347,8 @@ const addLink = (parent: any, child: any, opts: AddLinkOptions = {}) => {
|
|
|
141
347
|
TRACE_PROCESSOR.addLink(parent, child, opts);
|
|
142
348
|
};
|
|
143
349
|
|
|
144
|
-
export type
|
|
145
|
-
/**
|
|
146
|
-
* Unique ID.
|
|
147
|
-
*/
|
|
350
|
+
export type TraceDiagnosticProps<T> = {
|
|
351
|
+
/** Unique ID. */
|
|
148
352
|
id: string;
|
|
149
353
|
|
|
150
354
|
/**
|
|
@@ -153,9 +357,7 @@ export type TraceDiagnosticParams<T> = {
|
|
|
153
357
|
*/
|
|
154
358
|
name?: string;
|
|
155
359
|
|
|
156
|
-
/**
|
|
157
|
-
* Function that will be called to fetch the diagnostic data.
|
|
158
|
-
*/
|
|
360
|
+
/** Function that will be called to fetch the diagnostic data. */
|
|
159
361
|
fetch: () => MaybePromise<T>;
|
|
160
362
|
};
|
|
161
363
|
|
|
@@ -167,7 +369,7 @@ export interface TraceDiagnostic {
|
|
|
167
369
|
/**
|
|
168
370
|
* Register a diagnostic that could be queried.
|
|
169
371
|
*/
|
|
170
|
-
const diagnostic = <T>(params:
|
|
372
|
+
const diagnostic = <T>(params: TraceDiagnosticProps<T>): TraceDiagnostic => {
|
|
171
373
|
return TRACE_PROCESSOR.diagnostics.registerDiagnostic(params);
|
|
172
374
|
};
|
|
173
375
|
|
|
@@ -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/diagnostic.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { asyncTimeout } from '@dxos/async';
|
|
6
6
|
import { invariant } from '@dxos/invariant';
|
|
7
7
|
|
|
8
|
-
import { type TraceDiagnostic, type
|
|
8
|
+
import { type TraceDiagnostic, type TraceDiagnosticProps } from './api';
|
|
9
9
|
import { createId } from './util';
|
|
10
10
|
|
|
11
11
|
export const DIAGNOSTICS_TIMEOUT = 10_000;
|
|
@@ -58,7 +58,7 @@ export class DiagnosticsManager {
|
|
|
58
58
|
this._instanceTag = tag;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
registerDiagnostic(params:
|
|
61
|
+
registerDiagnostic(params: TraceDiagnosticProps<any>): TraceDiagnostic {
|
|
62
62
|
const impl = new TraceDiagnosticImpl(params.id, params.fetch, params.name ?? params.id, () => {
|
|
63
63
|
if (this.registry.get(params.id) === impl) {
|
|
64
64
|
this.registry.delete(params.id);
|
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