@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/tracing",
3
- "version": "0.8.4-main.bc674ce",
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.bc674ce",
35
- "@dxos/context": "0.8.4-main.bc674ce",
36
- "@dxos/codec-protobuf": "0.8.4-main.bc674ce",
37
- "@dxos/invariant": "0.8.4-main.bc674ce",
38
- "@dxos/log": "0.8.4-main.bc674ce",
39
- "@dxos/protocols": "0.8.4-main.bc674ce",
40
- "@dxos/node-std": "0.8.4-main.bc674ce",
41
- "@dxos/util": "0.8.4-main.bc674ce"
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 parentCtx = args[0] instanceof Context ? args[0] : null;
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 = span.ctx ? [span.ctx, ...args.slice(1)] : args;
95
- try {
96
- return await method.apply(this, callArgs);
97
- } catch (err) {
98
- span.markError(err);
99
- throw err;
100
- } finally {
101
- span.markSuccess();
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
 
@@ -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
- const remoteSpan = this._tracing.startSpan({
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
- const remoteSpan = this._spanMap.get(span);
47
- if (remoteSpan) {
48
- remoteSpan.end();
49
- this._spanMap.delete(span);
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
  }
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { unrefTimeout } from '@dxos/async';
6
- import { type Context } from '@dxos/context';
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 readonly _ctx: Context | null = null;
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
- get ctx(): Context | null {
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 = className.match(SANITIZE_REGEX);
542
- if (!m) {
543
- return className;
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> =>