@dxos/tracing 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5

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.
Files changed (39) hide show
  1. package/dist/lib/browser/index.mjs +392 -384
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +392 -384
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/api.d.ts +36 -17
  8. package/dist/types/src/api.d.ts.map +1 -1
  9. package/dist/types/src/buffering-backend.d.ts +24 -0
  10. package/dist/types/src/buffering-backend.d.ts.map +1 -0
  11. package/dist/types/src/diagnostic.d.ts +2 -2
  12. package/dist/types/src/diagnostic.d.ts.map +1 -1
  13. package/dist/types/src/index.d.ts +1 -2
  14. package/dist/types/src/index.d.ts.map +1 -1
  15. package/dist/types/src/remote/index.d.ts +0 -1
  16. package/dist/types/src/remote/index.d.ts.map +1 -1
  17. package/dist/types/src/symbols.d.ts +0 -1
  18. package/dist/types/src/symbols.d.ts.map +1 -1
  19. package/dist/types/src/trace-processor.d.ts +16 -52
  20. package/dist/types/src/trace-processor.d.ts.map +1 -1
  21. package/dist/types/src/tracing-types.d.ts +67 -0
  22. package/dist/types/src/tracing-types.d.ts.map +1 -0
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +15 -11
  25. package/src/api.ts +237 -35
  26. package/src/buffering-backend.ts +112 -0
  27. package/src/diagnostic.ts +2 -2
  28. package/src/index.ts +1 -2
  29. package/src/remote/index.ts +0 -1
  30. package/src/symbols.ts +0 -2
  31. package/src/trace-processor.ts +51 -252
  32. package/src/tracing-types.ts +77 -0
  33. package/src/tracing.test.ts +513 -4
  34. package/dist/types/src/remote/tracing.d.ts +0 -23
  35. package/dist/types/src/remote/tracing.d.ts.map +0 -1
  36. package/dist/types/src/trace-sender.d.ts +0 -9
  37. package/dist/types/src/trace-sender.d.ts.map +0 -1
  38. package/src/remote/tracing.ts +0 -53
  39. package/src/trace-sender.ts +0 -88
@@ -2,45 +2,32 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { unrefTimeout } from '@dxos/async';
6
- import { type Context } from '@dxos/context';
7
5
  import { LogLevel, type LogProcessor, getContextFromEntry, log } from '@dxos/log';
8
6
  import { type LogEntry } from '@dxos/protocols/proto/dxos/client/services';
9
- import { type Error as SerializedError } from '@dxos/protocols/proto/dxos/error';
10
- import { type Metric, type Resource, type Span } from '@dxos/protocols/proto/dxos/tracing';
7
+ import { type Metric, type Resource } from '@dxos/protocols/proto/dxos/tracing';
11
8
  import { getPrototypeSpecificInstanceId } from '@dxos/util';
12
9
 
13
10
  import type { AddLinkOptions, TimeAware } from './api';
11
+ import { BUFFERED_PREFIX, BufferingTracingBackend } from './buffering-backend';
14
12
  import { DiagnosticsManager } from './diagnostic';
15
13
  import { DiagnosticsChannel } from './diagnostics-channel';
16
14
  import { type BaseCounter } from './metrics';
17
- import { RemoteMetrics, RemoteTracing } from './remote';
18
- import { TRACE_SPAN_ATTRIBUTE, getTracingContext } from './symbols';
19
- import { TraceSender } from './trace-sender';
15
+ import { RemoteMetrics } from './remote/metrics';
16
+ import { getTracingContext } from './symbols';
17
+ import type { RemoteSpan, StartSpanOptions, TracingBackend } from './tracing-types';
20
18
  import { WeakRef } from './weak-ref';
21
19
 
22
20
  export type Diagnostics = {
23
21
  resources: Record<string, Resource>;
24
- spans: Span[];
25
22
  logs: LogEntry[];
26
23
  };
27
24
 
28
- export type TraceResourceConstructorParams = {
25
+ export type TraceResourceConstructorProps = {
29
26
  constructor: { new (...args: any[]): {} };
30
27
  instance: any;
31
28
  annotation?: symbol;
32
29
  };
33
30
 
34
- export type TraceSpanParams = {
35
- instance: any;
36
- // TODO(wittjosiah): Rename to `name`.
37
- methodName: string;
38
- parentCtx: Context | null;
39
- showInBrowserTimeline: boolean;
40
- op?: string;
41
- attributes?: Record<string, any>;
42
- };
43
-
44
31
  export class ResourceEntry {
45
32
  /**
46
33
  * Sometimes bundlers mangle class names: WebFile -> WebFile2.
@@ -62,39 +49,57 @@ export class ResourceEntry {
62
49
  }
63
50
  }
64
51
 
65
- export type TraceSubscription = {
66
- flush: () => void;
67
-
68
- dirtyResources: Set<number>;
69
- dirtySpans: Set<number>;
70
- newLogs: LogEntry[];
71
- };
72
-
73
52
  const MAX_RESOURCE_RECORDS = 2_000;
74
- const MAX_SPAN_RECORDS = 1_000;
75
53
  const MAX_LOG_RECORDS = 1_000;
76
54
 
77
- const REFRESH_INTERVAL = 1_000;
78
-
79
55
  const MAX_INFO_OBJECT_DEPTH = 8;
80
56
 
81
- const IS_CLOUDFLARE_WORKERS = !!globalThis?.navigator?.userAgent?.includes('Cloudflare-Workers');
82
-
83
57
  export class TraceProcessor {
84
58
  public readonly diagnostics = new DiagnosticsManager();
85
59
  public readonly diagnosticsChannel = new DiagnosticsChannel();
86
60
  public readonly remoteMetrics = new RemoteMetrics();
87
- public readonly remoteTracing = new RemoteTracing();
88
61
 
89
- readonly subscriptions: Set<TraceSubscription> = new Set();
62
+ readonly #bufferingBackend = new BufferingTracingBackend();
63
+ #activeBackend: TracingBackend = this.#bufferingBackend;
64
+
65
+ /**
66
+ * Tracing backend. Initially a buffering backend that records spans;
67
+ * once the observability package sets a real backend, the buffer is drained
68
+ * and a thin translating wrapper is installed that resolves stale buffered
69
+ * parent IDs still held by in-flight {@link Context} objects.
70
+ *
71
+ * The wrapper only allocates when a `buffered-*` parent is actually encountered;
72
+ * the common path is a single `startsWith` check and direct passthrough.
73
+ */
74
+ get tracingBackend(): TracingBackend {
75
+ return this.#activeBackend;
76
+ }
77
+
78
+ set tracingBackend(backend: TracingBackend | undefined) {
79
+ if (!backend || backend === this.#bufferingBackend) {
80
+ this.#bufferingBackend.clear();
81
+ this.#activeBackend = this.#bufferingBackend;
82
+ return;
83
+ }
84
+ const idMap = this.#bufferingBackend.drain(backend);
85
+ this.#activeBackend = {
86
+ startSpan: (options: StartSpanOptions): RemoteSpan => {
87
+ const parent = options.parentContext;
88
+ if (parent?.traceparent.startsWith(BUFFERED_PREFIX)) {
89
+ const translated = idMap.get(parent.traceparent);
90
+ if (translated) {
91
+ return backend.startSpan({ ...options, parentContext: translated });
92
+ }
93
+ }
94
+ return backend.startSpan(options);
95
+ },
96
+ };
97
+ }
90
98
 
91
99
  readonly resources = new Map<number, ResourceEntry>();
92
100
  readonly resourceInstanceIndex = new WeakMap<any, ResourceEntry>();
93
101
  readonly resourceIdList: number[] = [];
94
102
 
95
- readonly spans = new Map<number, Span>();
96
- readonly spanIdList: number[] = [];
97
-
98
103
  readonly logs: LogEntry[] = [];
99
104
 
100
105
  private _instanceTag: string | null = null;
@@ -102,11 +107,6 @@ export class TraceProcessor {
102
107
  constructor() {
103
108
  log.addProcessor(this._logProcessor.bind(this));
104
109
 
105
- if (!IS_CLOUDFLARE_WORKERS) {
106
- const refreshInterval = setInterval(this.refresh.bind(this), REFRESH_INTERVAL);
107
- unrefTimeout(refreshInterval);
108
- }
109
-
110
110
  if (DiagnosticsChannel.supported) {
111
111
  this.diagnosticsChannel.serve(this.diagnostics);
112
112
  }
@@ -118,14 +118,10 @@ export class TraceProcessor {
118
118
  this.diagnostics.setInstanceTag(tag);
119
119
  }
120
120
 
121
- /**
122
- * @internal
123
- */
124
- // TODO(burdon): Comment.
125
- createTraceResource(params: TraceResourceConstructorParams): void {
121
+ /** @internal */
122
+ createTraceResource(params: TraceResourceConstructorProps): void {
126
123
  const id = this.resources.size;
127
124
 
128
- // Init metrics counters.
129
125
  const tracingContext = getTracingContext(Object.getPrototypeOf(params.instance));
130
126
  for (const key of Object.keys(tracingContext.metricsProperties)) {
131
127
  (params.instance[key] as BaseCounter)._assign(params.instance, key);
@@ -150,29 +146,11 @@ export class TraceProcessor {
150
146
  if (this.resourceIdList.length > MAX_RESOURCE_RECORDS) {
151
147
  this._clearResources();
152
148
  }
153
-
154
- this._markResourceDirty(id);
155
- }
156
-
157
- createTraceSender(): TraceSender {
158
- return new TraceSender(this);
159
- }
160
-
161
- traceSpan(params: TraceSpanParams): TracingSpan {
162
- const span = new TracingSpan(this, params);
163
- this._flushSpan(span);
164
- return span;
165
149
  }
166
150
 
167
151
  // TODO(burdon): Not implemented.
168
152
  addLink(parent: any, child: any, opts: AddLinkOptions): void {}
169
153
 
170
- //
171
- // Getters
172
- //
173
-
174
- // TODO(burdon): Define type.
175
- // TODO(burdon): Reconcile with system service.
176
154
  getDiagnostics(): Diagnostics {
177
155
  this.refresh();
178
156
 
@@ -183,7 +161,6 @@ export class TraceProcessor {
183
161
  entry.data,
184
162
  ]),
185
163
  ),
186
- spans: Array.from(this.spans.values()),
187
164
  logs: this.logs.filter((log) => log.level >= LogLevel.INFO),
188
165
  };
189
166
  }
@@ -199,7 +176,7 @@ export class TraceProcessor {
199
176
  } else {
200
177
  res[key] = sanitizeValue(
201
178
  value,
202
- options.depth === undefined ? 1 : options.depth ?? MAX_INFO_OBJECT_DEPTH,
179
+ options.depth === undefined ? 1 : (options.depth ?? MAX_INFO_OBJECT_DEPTH),
203
180
  this,
204
181
  );
205
182
  }
@@ -250,81 +227,23 @@ export class TraceProcessor {
250
227
  (instance[key] as BaseCounter)._tick?.(time);
251
228
  }
252
229
 
253
- let _changed = false;
254
-
255
- const oldInfo = resource.data.info;
256
230
  resource.data.info = this.getResourceInfo(instance);
257
- _changed ||= !areEqualShallow(oldInfo, resource.data.info);
258
-
259
- const oldMetrics = resource.data.metrics;
260
231
  resource.data.metrics = this.getResourceMetrics(instance);
261
- _changed ||= !areEqualShallow(oldMetrics, resource.data.metrics);
262
-
263
- // TODO(dmaretskyi): Test if works and enable.
264
- // if (changed) {
265
- this._markResourceDirty(resource.data.id);
266
- // }
267
- }
268
-
269
- for (const subscription of this.subscriptions) {
270
- subscription.flush();
271
- }
272
- }
273
-
274
- //
275
- // Implementation
276
- //
277
-
278
- /**
279
- * @internal
280
- */
281
- _flushSpan(runtimeSpan: TracingSpan): void {
282
- const span = runtimeSpan.serialize();
283
- this.spans.set(span.id, span);
284
- this.spanIdList.push(span.id);
285
- if (this.spanIdList.length > MAX_SPAN_RECORDS) {
286
- this._clearSpans();
287
- }
288
- this._markSpanDirty(span.id);
289
- this.remoteTracing.flushSpan(runtimeSpan);
290
- }
291
-
292
- private _markResourceDirty(id: number): void {
293
- for (const subscription of this.subscriptions) {
294
- subscription.dirtyResources.add(id);
295
- }
296
- }
297
-
298
- private _markSpanDirty(id: number): void {
299
- for (const subscription of this.subscriptions) {
300
- subscription.dirtySpans.add(id);
301
232
  }
302
233
  }
303
234
 
304
235
  private _clearResources(): void {
305
- // TODO(dmaretskyi): Use FinalizationRegistry to delete finalized resources first.
306
236
  while (this.resourceIdList.length > MAX_RESOURCE_RECORDS) {
307
237
  const id = this.resourceIdList.shift()!;
308
238
  this.resources.delete(id);
309
239
  }
310
240
  }
311
241
 
312
- private _clearSpans(): void {
313
- while (this.spanIdList.length > MAX_SPAN_RECORDS) {
314
- const id = this.spanIdList.shift()!;
315
- this.spans.delete(id);
316
- }
317
- }
318
-
319
242
  private _pushLog(log: LogEntry): void {
320
243
  this.logs.push(log);
321
244
  if (this.logs.length > MAX_LOG_RECORDS) {
322
245
  this.logs.shift();
323
246
  }
324
-
325
- for (const subscription of this.subscriptions) {
326
- subscription.newLogs.push(log);
327
- }
328
247
  }
329
248
 
330
249
  private _logProcessor: LogProcessor = (config, entry) => {
@@ -345,7 +264,7 @@ export class TraceProcessor {
345
264
 
346
265
  const entryToPush: LogEntry = {
347
266
  level: entry.level,
348
- message: entry.message,
267
+ message: entry.message ?? (entry.error ? (entry.error.message ?? String(entry.error)) : ''),
349
268
  context,
350
269
  timestamp: new Date(),
351
270
  meta: {
@@ -362,110 +281,6 @@ export class TraceProcessor {
362
281
  };
363
282
  }
364
283
 
365
- // TODO(burdon): Comment.
366
- export class TracingSpan {
367
- static nextId = 0;
368
-
369
- readonly id: number;
370
- readonly parentId: number | null = null;
371
- readonly methodName: string;
372
- readonly resourceId: number | null = null;
373
- readonly op: string | undefined;
374
- readonly attributes: Record<string, any>;
375
- startTs: number;
376
- endTs: number | null = null;
377
- error: SerializedError | null = null;
378
-
379
- private _showInBrowserTimeline: boolean;
380
- private readonly _ctx: Context | null = null;
381
-
382
- constructor(
383
- private _traceProcessor: TraceProcessor,
384
- params: TraceSpanParams,
385
- ) {
386
- this.id = TracingSpan.nextId++;
387
- this.methodName = params.methodName;
388
- this.resourceId = _traceProcessor.getResourceId(params.instance);
389
- this.startTs = performance.now();
390
- this._showInBrowserTimeline = params.showInBrowserTimeline;
391
- this.op = params.op;
392
- this.attributes = params.attributes ?? {};
393
-
394
- if (params.parentCtx) {
395
- this._ctx = params.parentCtx.derive({
396
- attributes: {
397
- [TRACE_SPAN_ATTRIBUTE]: this.id,
398
- },
399
- });
400
- const parentId = params.parentCtx.getAttribute(TRACE_SPAN_ATTRIBUTE);
401
- if (typeof parentId === 'number') {
402
- this.parentId = parentId;
403
- }
404
- }
405
- }
406
-
407
- get name() {
408
- const resource = this._traceProcessor.resources.get(this.resourceId!);
409
- return resource ? `${resource.sanitizedClassName}#${resource.data.instanceId}.${this.methodName}` : this.methodName;
410
- }
411
-
412
- get ctx(): Context | null {
413
- return this._ctx;
414
- }
415
-
416
- markSuccess(): void {
417
- this.endTs = performance.now();
418
- this._traceProcessor._flushSpan(this);
419
-
420
- if (this._showInBrowserTimeline) {
421
- this._markInBrowserTimeline();
422
- }
423
- }
424
-
425
- markError(err: unknown): void {
426
- this.endTs = performance.now();
427
- this.error = serializeError(err);
428
- this._traceProcessor._flushSpan(this);
429
-
430
- if (this._showInBrowserTimeline) {
431
- this._markInBrowserTimeline();
432
- }
433
- }
434
-
435
- serialize(): Span {
436
- return {
437
- id: this.id,
438
- resourceId: this.resourceId ?? undefined,
439
- methodName: this.methodName,
440
- parentId: this.parentId ?? undefined,
441
- startTs: this.startTs.toFixed(3),
442
- endTs: this.endTs?.toFixed(3) ?? undefined,
443
- error: this.error ?? undefined,
444
- };
445
- }
446
-
447
- private _markInBrowserTimeline(): void {
448
- if (typeof globalThis?.performance?.measure === 'function') {
449
- performance.measure(this.name, { start: this.startTs, end: this.endTs! });
450
- }
451
- }
452
- }
453
-
454
- // TODO(burdon): Log cause.
455
- const serializeError = (err: unknown): SerializedError => {
456
- if (err instanceof Error) {
457
- return {
458
- name: err.name,
459
- message: err.message,
460
- };
461
- }
462
-
463
- return {
464
- message: String(err),
465
- };
466
- };
467
-
468
- // TODO(burdon): Rename singleton and move out of package.
469
284
  export const TRACE_PROCESSOR: TraceProcessor = ((globalThis as any).TRACE_PROCESSOR ??= new TraceProcessor());
470
285
 
471
286
  const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor): any => {
@@ -489,7 +304,6 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
489
304
  }
490
305
 
491
306
  if (typeof value.toJSON === 'function') {
492
- // TODO(dmaretskyi): This has potential to cause infinite recursion.
493
307
  return sanitizeValue(value.toJSON(), depth, traceProcessor);
494
308
  }
495
309
 
@@ -513,7 +327,6 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
513
327
  }
514
328
  }
515
329
 
516
- // TODO(dmaretskyi): Expose trait.
517
330
  if (typeof value.truncate === 'function') {
518
331
  return value.truncate();
519
332
  }
@@ -522,28 +335,14 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
522
335
  }
523
336
  };
524
337
 
525
- const areEqualShallow = (a: any, b: any) => {
526
- for (const key in a) {
527
- if (!(key in b) || a[key] !== b[key]) {
528
- return false;
529
- }
530
- }
531
- for (const key in b) {
532
- if (!(key in a) || a[key] !== b[key]) {
533
- return false;
534
- }
535
- }
536
- return true;
537
- };
538
-
539
338
  export const sanitizeClassName = (className: string) => {
339
+ let name = className.replace(/^_+/, '');
540
340
  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);
341
+ const m = name.match(SANITIZE_REGEX);
342
+ if (m) {
343
+ name = name.slice(0, -m[1].length);
546
344
  }
345
+ return name;
547
346
  };
548
347
 
549
348
  const isSetLike = (value: any): value is Set<any> =>
@@ -0,0 +1,77 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type TraceContextData } from '@dxos/context';
6
+
7
+ /**
8
+ * Opaque span handle returned by {@link TracingBackend.startSpan}.
9
+ *
10
+ * The `spanContext` field carries W3C trace context strings that are stored
11
+ * on the DXOS {@link Context} via `TRACE_SPAN_ATTRIBUTE`. Because these are
12
+ * plain strings (not live runtime objects), they survive after the span ends
13
+ * and across serialization boundaries.
14
+ */
15
+ export type RemoteSpan = {
16
+ /** Signal that the span has ended. Must be called exactly once. */
17
+ end: (endTime?: number) => void;
18
+
19
+ /** Record an error on the span (e.g., OTEL `span.recordException` + `setStatus`). */
20
+ setError?: (err: unknown) => void;
21
+
22
+ /**
23
+ * W3C trace context identifying this span.
24
+ *
25
+ * Stored on the DXOS `Context` attribute (`TRACE_SPAN_ATTRIBUTE`) so that
26
+ * child `@trace.span()` methods can read it and pass it as
27
+ * {@link StartSpanOptions.parentContext} to create properly-parented spans.
28
+ */
29
+ spanContext?: TraceContextData;
30
+ };
31
+
32
+ /**
33
+ * Options passed to {@link TracingBackend.startSpan}.
34
+ */
35
+ export type StartSpanOptions = {
36
+ /** Human-readable span name, typically `ClassName.methodName`. */
37
+ name: string;
38
+ /** Span category (e.g., `'function'`, `'rpc'`). */
39
+ op?: string;
40
+ /** Key-value attributes attached to the span. */
41
+ attributes?: Record<string, any>;
42
+
43
+ /**
44
+ * W3C trace context of the parent span.
45
+ *
46
+ * The backend extracts the trace/span IDs from these strings to establish
47
+ * parent-child relationships. When `undefined`, the backend creates a root span.
48
+ */
49
+ parentContext?: TraceContextData;
50
+
51
+ /**
52
+ * Epoch-millisecond timestamp for the span start.
53
+ * Used by the buffering backend to preserve original timing when replaying spans.
54
+ * When `undefined`, the backend uses the current time.
55
+ */
56
+ startTime?: number;
57
+ };
58
+
59
+ /**
60
+ * Backend-agnostic tracing interface implemented by the observability package
61
+ * and registered on `TRACE_PROCESSOR.tracingBackend`.
62
+ *
63
+ * The backend receives and returns {@link TraceContextData} (W3C strings) —
64
+ * no opaque runtime objects cross the interface boundary. The OTEL backend
65
+ * performs `propagation.extract/inject` internally in {@link startSpan}.
66
+ */
67
+ export interface TracingBackend {
68
+ /**
69
+ * Create a new span.
70
+ *
71
+ * The backend should:
72
+ * 1. Extract the parent from `options.parentContext` (if present).
73
+ * 2. Create a span as a child of that parent.
74
+ * 3. Inject the new span's identity into the returned `spanContext`.
75
+ */
76
+ startSpan: (options: StartSpanOptions) => RemoteSpan;
77
+ }