@dxos/tracing 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
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 +157 -36
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +157 -36
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/api.d.ts +3 -1
- package/dist/types/src/api.d.ts.map +1 -1
- package/dist/types/src/remote/tracing.d.ts +34 -0
- package/dist/types/src/remote/tracing.d.ts.map +1 -1
- package/dist/types/src/trace-processor.d.ts +7 -2
- package/dist/types/src/trace-processor.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/api.ts +19 -12
- package/src/remote/tracing.ts +133 -11
- package/src/trace-processor.ts +25 -13
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.bcb3aa67d6",
|
|
4
4
|
"description": "Async utilities.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@dxos/async": "0.8.4-main.
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/invariant": "0.8.4-main.
|
|
38
|
-
"@dxos/log": "0.8.4-main.
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/util": "0.8.4-main.
|
|
34
|
+
"@dxos/async": "0.8.4-main.bcb3aa67d6",
|
|
35
|
+
"@dxos/codec-protobuf": "0.8.4-main.bcb3aa67d6",
|
|
36
|
+
"@dxos/context": "0.8.4-main.bcb3aa67d6",
|
|
37
|
+
"@dxos/invariant": "0.8.4-main.bcb3aa67d6",
|
|
38
|
+
"@dxos/log": "0.8.4-main.bcb3aa67d6",
|
|
39
|
+
"@dxos/node-std": "0.8.4-main.bcb3aa67d6",
|
|
40
|
+
"@dxos/protocols": "0.8.4-main.bcb3aa67d6",
|
|
41
|
+
"@dxos/util": "0.8.4-main.bcb3aa67d6"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
package/src/api.ts
CHANGED
|
@@ -68,6 +68,8 @@ const mark = (name: string) => {
|
|
|
68
68
|
|
|
69
69
|
export type SpanOptions = {
|
|
70
70
|
showInBrowserTimeline?: boolean;
|
|
71
|
+
/** When false the span is not exported to remote OTLP collectors. Defaults to true. */
|
|
72
|
+
showInRemoteTracing?: boolean;
|
|
71
73
|
op?: string;
|
|
72
74
|
attributes?: Record<string, any>;
|
|
73
75
|
};
|
|
@@ -76,30 +78,35 @@ export type SpanOptions = {
|
|
|
76
78
|
* Decorator that creates a span for the execution duration of the decorated method.
|
|
77
79
|
*/
|
|
78
80
|
const span =
|
|
79
|
-
({ showInBrowserTimeline = false, op, attributes }: SpanOptions = {}) =>
|
|
81
|
+
({ showInBrowserTimeline = false, showInRemoteTracing = true, op, attributes }: SpanOptions = {}) =>
|
|
80
82
|
(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<(...args: any) => any>) => {
|
|
81
83
|
const method = descriptor.value!;
|
|
82
84
|
|
|
83
85
|
descriptor.value = async function (this: any, ...args: any) {
|
|
84
|
-
const
|
|
86
|
+
const explicitCtx = args[0] instanceof Context ? args[0] : null;
|
|
87
|
+
|
|
85
88
|
const span = TRACE_PROCESSOR.traceSpan({
|
|
86
|
-
parentCtx,
|
|
89
|
+
parentCtx: explicitCtx,
|
|
87
90
|
methodName: propertyKey,
|
|
88
91
|
instance: this,
|
|
89
92
|
showInBrowserTimeline,
|
|
93
|
+
showInRemoteTracing,
|
|
90
94
|
op,
|
|
91
95
|
attributes,
|
|
92
96
|
});
|
|
93
97
|
|
|
94
|
-
const callArgs =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
const callArgs = explicitCtx ? [span.ctx, ...args.slice(1)] : args;
|
|
99
|
+
|
|
100
|
+
return TRACE_PROCESSOR.remoteTracing.wrapExecution(span, async () => {
|
|
101
|
+
try {
|
|
102
|
+
return await method.apply(this, callArgs);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
span.markError(err);
|
|
105
|
+
throw err;
|
|
106
|
+
} finally {
|
|
107
|
+
span.markSuccess();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
103
110
|
};
|
|
104
111
|
};
|
|
105
112
|
|
package/src/remote/tracing.ts
CHANGED
|
@@ -6,48 +6,170 @@ import { type TracingSpan } from '../trace-processor';
|
|
|
6
6
|
|
|
7
7
|
type RemoteSpan = {
|
|
8
8
|
end: () => void;
|
|
9
|
+
/** Wraps execution so that this span becomes the active parent for child spans. */
|
|
10
|
+
wrapExecution?: <T>(fn: () => T) => T;
|
|
11
|
+
/** Opaque context object for the backend (e.g., OTEL Context with this span active). */
|
|
12
|
+
spanContext?: unknown;
|
|
9
13
|
};
|
|
10
14
|
|
|
11
15
|
export type StartSpanOptions = {
|
|
12
16
|
name: string;
|
|
13
17
|
op?: string;
|
|
14
18
|
attributes?: Record<string, any>;
|
|
19
|
+
/** Opaque parent context from a parent RemoteSpan (bridges async DXOS Context to OTEL). */
|
|
20
|
+
parentContext?: unknown;
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
interface TracingMethods {
|
|
18
24
|
startSpan: (options: StartSpanOptions) => RemoteSpan;
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
const MAX_ENDED_CONTEXTS = 10_000;
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* Allows traces to be recorded within SDK code without requiring specific consumers.
|
|
31
|
+
*
|
|
32
|
+
* Preserves OTEL span contexts after spans end so that long-lived DXOS Contexts
|
|
33
|
+
* (e.g., `this._ctx` stored during `open()`) can still serve as parents for
|
|
34
|
+
* later child spans and outbound trace-context injection.
|
|
23
35
|
*/
|
|
24
|
-
// TODO(wittjosiah): Should probably just use otel. Use `any` for now to not depend on Sentry directly.
|
|
25
36
|
export class RemoteTracing {
|
|
26
37
|
private _tracing: TracingMethods | undefined;
|
|
27
38
|
private _spanMap = new Map<TracingSpan, RemoteSpan>();
|
|
39
|
+
private _idToSpan = new Map<number, TracingSpan>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Retains OTEL span contexts after spans end so that periodic/background
|
|
43
|
+
* operations referencing a completed parent (via `this._ctx`) still produce
|
|
44
|
+
* correlated child spans instead of orphaned root traces.
|
|
45
|
+
*/
|
|
46
|
+
private _endedSpanContexts = new Map<number, unknown>();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Buffers flushSpan calls that arrive before a processor is registered,
|
|
50
|
+
* so early startup spans are not silently dropped.
|
|
51
|
+
*/
|
|
52
|
+
private _pendingFlushes: Array<{ span: TracingSpan; isEnd: boolean }> | null = [];
|
|
28
53
|
|
|
29
54
|
registerProcessor(processor: TracingMethods): void {
|
|
30
55
|
this._tracing = processor;
|
|
56
|
+
|
|
57
|
+
const pending = this._pendingFlushes;
|
|
58
|
+
this._pendingFlushes = null;
|
|
59
|
+
if (pending) {
|
|
60
|
+
for (const { span, isEnd } of pending) {
|
|
61
|
+
this._replayFlush(span, isEnd);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Returns the opaque OTEL context for the given DXOS span ID, if one exists. */
|
|
67
|
+
getSpanContext(spanId: number): unknown | undefined {
|
|
68
|
+
const tracingSpan = this._idToSpan.get(spanId);
|
|
69
|
+
if (tracingSpan) {
|
|
70
|
+
return this._spanMap.get(tracingSpan)?.spanContext;
|
|
71
|
+
}
|
|
72
|
+
return this._endedSpanContexts.get(spanId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Wraps execution so that the remote span is active as parent context. */
|
|
76
|
+
wrapExecution<T>(span: TracingSpan, fn: () => T): T {
|
|
77
|
+
const remoteSpan = this._spanMap.get(span);
|
|
78
|
+
if (remoteSpan?.wrapExecution) {
|
|
79
|
+
return remoteSpan.wrapExecution(fn);
|
|
80
|
+
}
|
|
81
|
+
return fn();
|
|
31
82
|
}
|
|
32
83
|
|
|
33
84
|
flushSpan(span: TracingSpan): void {
|
|
85
|
+
if (!span.showInRemoteTracing) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
34
89
|
if (!this._tracing) {
|
|
90
|
+
this._pendingFlushes?.push({ span, isEnd: !!span.endTs });
|
|
35
91
|
return;
|
|
36
92
|
}
|
|
37
93
|
|
|
38
94
|
if (!span.endTs) {
|
|
39
|
-
|
|
40
|
-
name: span.methodName,
|
|
41
|
-
op: span.op ?? 'function',
|
|
42
|
-
attributes: span.attributes,
|
|
43
|
-
});
|
|
44
|
-
this._spanMap.set(span, remoteSpan);
|
|
95
|
+
this._startRemoteSpan(span);
|
|
45
96
|
} else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
97
|
+
this._endRemoteSpan(span);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private _startRemoteSpan(span: TracingSpan): void {
|
|
102
|
+
let parentContext: unknown;
|
|
103
|
+
if (span.parentId != null) {
|
|
104
|
+
const parentTracingSpan = this._idToSpan.get(span.parentId);
|
|
105
|
+
if (parentTracingSpan) {
|
|
106
|
+
parentContext = this._spanMap.get(parentTracingSpan)?.spanContext;
|
|
107
|
+
}
|
|
108
|
+
if (parentContext == null) {
|
|
109
|
+
parentContext = this._endedSpanContexts.get(span.parentId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const attributes: Record<string, any> = {};
|
|
114
|
+
if (span.sanitizedClassName) {
|
|
115
|
+
attributes.entryPoint = span.sanitizedClassName;
|
|
116
|
+
}
|
|
117
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
118
|
+
attributes[key.startsWith('ctx.') ? key : `ctx.${key}`] = value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const remoteSpan = this._tracing!.startSpan({
|
|
122
|
+
name: span.name,
|
|
123
|
+
op: span.op ?? 'function',
|
|
124
|
+
attributes,
|
|
125
|
+
parentContext,
|
|
126
|
+
});
|
|
127
|
+
this._spanMap.set(span, remoteSpan);
|
|
128
|
+
this._idToSpan.set(span.id, span);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private _endRemoteSpan(span: TracingSpan): void {
|
|
132
|
+
const remoteSpan = this._spanMap.get(span);
|
|
133
|
+
if (remoteSpan) {
|
|
134
|
+
if (remoteSpan.spanContext != null) {
|
|
135
|
+
this._endedSpanContexts.set(span.id, remoteSpan.spanContext);
|
|
136
|
+
this._evictEndedContexts();
|
|
137
|
+
}
|
|
138
|
+
remoteSpan.end();
|
|
139
|
+
this._spanMap.delete(span);
|
|
140
|
+
this._idToSpan.delete(span.id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Replays a buffered flush that was queued before the processor was registered.
|
|
146
|
+
* For spans that started AND ended before registration, replays both events.
|
|
147
|
+
*/
|
|
148
|
+
private _replayFlush(span: TracingSpan, isEnd: boolean): void {
|
|
149
|
+
if (!isEnd) {
|
|
150
|
+
this._startRemoteSpan(span);
|
|
151
|
+
if (span.endTs != null) {
|
|
152
|
+
this._endRemoteSpan(span);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
if (!this._spanMap.has(span)) {
|
|
156
|
+
this._startRemoteSpan(span);
|
|
157
|
+
}
|
|
158
|
+
this._endRemoteSpan(span);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private _evictEndedContexts(): void {
|
|
163
|
+
if (this._endedSpanContexts.size <= MAX_ENDED_CONTEXTS) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const iterator = this._endedSpanContexts.keys();
|
|
167
|
+
while (this._endedSpanContexts.size > MAX_ENDED_CONTEXTS) {
|
|
168
|
+
const oldest = iterator.next();
|
|
169
|
+
if (oldest.done) {
|
|
170
|
+
break;
|
|
50
171
|
}
|
|
172
|
+
this._endedSpanContexts.delete(oldest.value);
|
|
51
173
|
}
|
|
52
174
|
}
|
|
53
175
|
}
|
package/src/trace-processor.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { unrefTimeout } from '@dxos/async';
|
|
6
|
-
import {
|
|
6
|
+
import { Context } from '@dxos/context';
|
|
7
7
|
import { LogLevel, type LogProcessor, getContextFromEntry, log } from '@dxos/log';
|
|
8
8
|
import { type LogEntry } from '@dxos/protocols/proto/dxos/client/services';
|
|
9
9
|
import { type Error as SerializedError } from '@dxos/protocols/proto/dxos/error';
|
|
@@ -37,6 +37,7 @@ export type TraceSpanProps = {
|
|
|
37
37
|
methodName: string;
|
|
38
38
|
parentCtx: Context | null;
|
|
39
39
|
showInBrowserTimeline: boolean;
|
|
40
|
+
showInRemoteTracing?: boolean;
|
|
40
41
|
op?: string;
|
|
41
42
|
attributes?: Record<string, any>;
|
|
42
43
|
};
|
|
@@ -377,7 +378,8 @@ export class TracingSpan {
|
|
|
377
378
|
error: SerializedError | null = null;
|
|
378
379
|
|
|
379
380
|
private _showInBrowserTimeline: boolean;
|
|
380
|
-
private
|
|
381
|
+
private _showInRemoteTracing: boolean;
|
|
382
|
+
private readonly _ctx: Context;
|
|
381
383
|
|
|
382
384
|
constructor(
|
|
383
385
|
private _traceProcessor: TraceProcessor,
|
|
@@ -388,15 +390,15 @@ export class TracingSpan {
|
|
|
388
390
|
this.resourceId = _traceProcessor.getResourceId(params.instance);
|
|
389
391
|
this.startTs = performance.now();
|
|
390
392
|
this._showInBrowserTimeline = params.showInBrowserTimeline;
|
|
393
|
+
this._showInRemoteTracing = params.showInRemoteTracing ?? true;
|
|
391
394
|
this.op = params.op;
|
|
392
395
|
this.attributes = params.attributes ?? {};
|
|
393
396
|
|
|
397
|
+
const baseCtx = params.parentCtx ?? new Context();
|
|
398
|
+
this._ctx = this._showInRemoteTracing
|
|
399
|
+
? baseCtx.derive({ attributes: { [TRACE_SPAN_ATTRIBUTE]: this.id } })
|
|
400
|
+
: baseCtx.derive();
|
|
394
401
|
if (params.parentCtx) {
|
|
395
|
-
this._ctx = params.parentCtx.derive({
|
|
396
|
-
attributes: {
|
|
397
|
-
[TRACE_SPAN_ATTRIBUTE]: this.id,
|
|
398
|
-
},
|
|
399
|
-
});
|
|
400
402
|
const parentId = params.parentCtx.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
401
403
|
if (typeof parentId === 'number') {
|
|
402
404
|
this.parentId = parentId;
|
|
@@ -409,10 +411,20 @@ export class TracingSpan {
|
|
|
409
411
|
return resource ? `${resource.sanitizedClassName}#${resource.data.instanceId}.${this.methodName}` : this.methodName;
|
|
410
412
|
}
|
|
411
413
|
|
|
412
|
-
|
|
414
|
+
/** Sanitized class name of the owning resource, if available. */
|
|
415
|
+
get sanitizedClassName(): string | undefined {
|
|
416
|
+
const resource = this.resourceId != null ? this._traceProcessor.resources.get(this.resourceId) : undefined;
|
|
417
|
+
return resource?.sanitizedClassName;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
get ctx(): Context {
|
|
413
421
|
return this._ctx;
|
|
414
422
|
}
|
|
415
423
|
|
|
424
|
+
get showInRemoteTracing(): boolean {
|
|
425
|
+
return this._showInRemoteTracing;
|
|
426
|
+
}
|
|
427
|
+
|
|
416
428
|
markSuccess(): void {
|
|
417
429
|
this.endTs = performance.now();
|
|
418
430
|
this._traceProcessor._flushSpan(this);
|
|
@@ -537,13 +549,13 @@ const areEqualShallow = (a: any, b: any) => {
|
|
|
537
549
|
};
|
|
538
550
|
|
|
539
551
|
export const sanitizeClassName = (className: string) => {
|
|
552
|
+
let name = className.replace(/^_+/, '');
|
|
540
553
|
const SANITIZE_REGEX = /[^_](\d+)$/;
|
|
541
|
-
const m =
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
} else {
|
|
545
|
-
return className.slice(0, -m[1].length);
|
|
554
|
+
const m = name.match(SANITIZE_REGEX);
|
|
555
|
+
if (m) {
|
|
556
|
+
name = name.slice(0, -m[1].length);
|
|
546
557
|
}
|
|
558
|
+
return name;
|
|
547
559
|
};
|
|
548
560
|
|
|
549
561
|
const isSetLike = (value: any): value is Set<any> =>
|