@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.
Files changed (45) hide show
  1. package/dist/lib/browser/index.mjs +330 -513
  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 +330 -513
  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 +31 -14
  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.map +1 -1
  12. package/dist/types/src/diagnostics-channel.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/metrics/base.d.ts.map +1 -1
  16. package/dist/types/src/metrics/custom-counter.d.ts.map +1 -1
  17. package/dist/types/src/metrics/map-counter.d.ts.map +1 -1
  18. package/dist/types/src/metrics/time-series-counter.d.ts.map +1 -1
  19. package/dist/types/src/metrics/time-usage-counter.d.ts.map +1 -1
  20. package/dist/types/src/metrics/unary-counter.d.ts.map +1 -1
  21. package/dist/types/src/remote/index.d.ts +0 -1
  22. package/dist/types/src/remote/index.d.ts.map +1 -1
  23. package/dist/types/src/remote/metrics.d.ts.map +1 -1
  24. package/dist/types/src/symbols.d.ts +0 -1
  25. package/dist/types/src/symbols.d.ts.map +1 -1
  26. package/dist/types/src/trace-processor.d.ts +15 -56
  27. package/dist/types/src/trace-processor.d.ts.map +1 -1
  28. package/dist/types/src/tracing-types.d.ts +67 -0
  29. package/dist/types/src/tracing-types.d.ts.map +1 -0
  30. package/dist/types/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +9 -12
  32. package/src/api.ts +235 -40
  33. package/src/buffering-backend.ts +112 -0
  34. package/src/index.ts +1 -2
  35. package/src/remote/index.ts +0 -1
  36. package/src/symbols.ts +0 -2
  37. package/src/trace-processor.ts +51 -263
  38. package/src/tracing-types.ts +77 -0
  39. package/src/tracing.test.ts +513 -4
  40. package/dist/types/src/remote/tracing.d.ts +0 -57
  41. package/dist/types/src/remote/tracing.d.ts.map +0 -1
  42. package/dist/types/src/trace-sender.d.ts +0 -9
  43. package/dist/types/src/trace-sender.d.ts.map +0 -1
  44. package/src/remote/tracing.ts +0 -175
  45. package/src/trace-sender.ts +0 -88
@@ -2,26 +2,23 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { unrefTimeout } from '@dxos/async';
6
- import { Context } from '@dxos/context';
7
- import { LogLevel, type LogProcessor, getContextFromEntry, log } from '@dxos/log';
5
+ import { LogLevel, type LogProcessor, 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
 
@@ -31,17 +28,6 @@ export type TraceResourceConstructorProps = {
31
28
  annotation?: symbol;
32
29
  };
33
30
 
34
- export type TraceSpanProps = {
35
- instance: any;
36
- // TODO(wittjosiah): Rename to `name`.
37
- methodName: string;
38
- parentCtx: Context | null;
39
- showInBrowserTimeline: boolean;
40
- showInRemoteTracing?: boolean;
41
- op?: string;
42
- attributes?: Record<string, any>;
43
- };
44
-
45
31
  export class ResourceEntry {
46
32
  /**
47
33
  * Sometimes bundlers mangle class names: WebFile -> WebFile2.
@@ -63,39 +49,57 @@ export class ResourceEntry {
63
49
  }
64
50
  }
65
51
 
66
- export type TraceSubscription = {
67
- flush: () => void;
68
-
69
- dirtyResources: Set<number>;
70
- dirtySpans: Set<number>;
71
- newLogs: LogEntry[];
72
- };
73
-
74
52
  const MAX_RESOURCE_RECORDS = 2_000;
75
- const MAX_SPAN_RECORDS = 1_000;
76
53
  const MAX_LOG_RECORDS = 1_000;
77
54
 
78
- const REFRESH_INTERVAL = 1_000;
79
-
80
55
  const MAX_INFO_OBJECT_DEPTH = 8;
81
56
 
82
- const IS_CLOUDFLARE_WORKERS = !!globalThis?.navigator?.userAgent?.includes('Cloudflare-Workers');
83
-
84
57
  export class TraceProcessor {
85
58
  public readonly diagnostics = new DiagnosticsManager();
86
59
  public readonly diagnosticsChannel = new DiagnosticsChannel();
87
60
  public readonly remoteMetrics = new RemoteMetrics();
88
- public readonly remoteTracing = new RemoteTracing();
89
61
 
90
- 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
+ }
91
98
 
92
99
  readonly resources = new Map<number, ResourceEntry>();
93
100
  readonly resourceInstanceIndex = new WeakMap<any, ResourceEntry>();
94
101
  readonly resourceIdList: number[] = [];
95
102
 
96
- readonly spans = new Map<number, Span>();
97
- readonly spanIdList: number[] = [];
98
-
99
103
  readonly logs: LogEntry[] = [];
100
104
 
101
105
  private _instanceTag: string | null = null;
@@ -103,11 +107,6 @@ export class TraceProcessor {
103
107
  constructor() {
104
108
  log.addProcessor(this._logProcessor.bind(this));
105
109
 
106
- if (!IS_CLOUDFLARE_WORKERS) {
107
- const refreshInterval = setInterval(this.refresh.bind(this), REFRESH_INTERVAL);
108
- unrefTimeout(refreshInterval);
109
- }
110
-
111
110
  if (DiagnosticsChannel.supported) {
112
111
  this.diagnosticsChannel.serve(this.diagnostics);
113
112
  }
@@ -119,14 +118,10 @@ export class TraceProcessor {
119
118
  this.diagnostics.setInstanceTag(tag);
120
119
  }
121
120
 
122
- /**
123
- * @internal
124
- */
125
- // TODO(burdon): Comment.
121
+ /** @internal */
126
122
  createTraceResource(params: TraceResourceConstructorProps): void {
127
123
  const id = this.resources.size;
128
124
 
129
- // Init metrics counters.
130
125
  const tracingContext = getTracingContext(Object.getPrototypeOf(params.instance));
131
126
  for (const key of Object.keys(tracingContext.metricsProperties)) {
132
127
  (params.instance[key] as BaseCounter)._assign(params.instance, key);
@@ -151,29 +146,11 @@ export class TraceProcessor {
151
146
  if (this.resourceIdList.length > MAX_RESOURCE_RECORDS) {
152
147
  this._clearResources();
153
148
  }
154
-
155
- this._markResourceDirty(id);
156
- }
157
-
158
- createTraceSender(): TraceSender {
159
- return new TraceSender(this);
160
- }
161
-
162
- traceSpan(params: TraceSpanProps): TracingSpan {
163
- const span = new TracingSpan(this, params);
164
- this._flushSpan(span);
165
- return span;
166
149
  }
167
150
 
168
151
  // TODO(burdon): Not implemented.
169
152
  addLink(parent: any, child: any, opts: AddLinkOptions): void {}
170
153
 
171
- //
172
- // Getters
173
- //
174
-
175
- // TODO(burdon): Define type.
176
- // TODO(burdon): Reconcile with system service.
177
154
  getDiagnostics(): Diagnostics {
178
155
  this.refresh();
179
156
 
@@ -184,7 +161,6 @@ export class TraceProcessor {
184
161
  entry.data,
185
162
  ]),
186
163
  ),
187
- spans: Array.from(this.spans.values()),
188
164
  logs: this.logs.filter((log) => log.level >= LogLevel.INFO),
189
165
  };
190
166
  }
@@ -251,81 +227,23 @@ export class TraceProcessor {
251
227
  (instance[key] as BaseCounter)._tick?.(time);
252
228
  }
253
229
 
254
- let _changed = false;
255
-
256
- const oldInfo = resource.data.info;
257
230
  resource.data.info = this.getResourceInfo(instance);
258
- _changed ||= !areEqualShallow(oldInfo, resource.data.info);
259
-
260
- const oldMetrics = resource.data.metrics;
261
231
  resource.data.metrics = this.getResourceMetrics(instance);
262
- _changed ||= !areEqualShallow(oldMetrics, resource.data.metrics);
263
-
264
- // TODO(dmaretskyi): Test if works and enable.
265
- // if (changed) {
266
- this._markResourceDirty(resource.data.id);
267
- // }
268
- }
269
-
270
- for (const subscription of this.subscriptions) {
271
- subscription.flush();
272
- }
273
- }
274
-
275
- //
276
- // Implementation
277
- //
278
-
279
- /**
280
- * @internal
281
- */
282
- _flushSpan(runtimeSpan: TracingSpan): void {
283
- const span = runtimeSpan.serialize();
284
- this.spans.set(span.id, span);
285
- this.spanIdList.push(span.id);
286
- if (this.spanIdList.length > MAX_SPAN_RECORDS) {
287
- this._clearSpans();
288
- }
289
- this._markSpanDirty(span.id);
290
- this.remoteTracing.flushSpan(runtimeSpan);
291
- }
292
-
293
- private _markResourceDirty(id: number): void {
294
- for (const subscription of this.subscriptions) {
295
- subscription.dirtyResources.add(id);
296
- }
297
- }
298
-
299
- private _markSpanDirty(id: number): void {
300
- for (const subscription of this.subscriptions) {
301
- subscription.dirtySpans.add(id);
302
232
  }
303
233
  }
304
234
 
305
235
  private _clearResources(): void {
306
- // TODO(dmaretskyi): Use FinalizationRegistry to delete finalized resources first.
307
236
  while (this.resourceIdList.length > MAX_RESOURCE_RECORDS) {
308
237
  const id = this.resourceIdList.shift()!;
309
238
  this.resources.delete(id);
310
239
  }
311
240
  }
312
241
 
313
- private _clearSpans(): void {
314
- while (this.spanIdList.length > MAX_SPAN_RECORDS) {
315
- const id = this.spanIdList.shift()!;
316
- this.spans.delete(id);
317
- }
318
- }
319
-
320
242
  private _pushLog(log: LogEntry): void {
321
243
  this.logs.push(log);
322
244
  if (this.logs.length > MAX_LOG_RECORDS) {
323
245
  this.logs.shift();
324
246
  }
325
-
326
- for (const subscription of this.subscriptions) {
327
- subscription.newLogs.push(log);
328
- }
329
247
  }
330
248
 
331
249
  private _logProcessor: LogProcessor = (config, entry) => {
@@ -339,19 +257,20 @@ export class TraceProcessor {
339
257
  return;
340
258
  }
341
259
 
342
- const context = getContextFromEntry(entry) ?? {};
343
- for (const key of Object.keys(context)) {
344
- context[key] = sanitizeValue(context[key], 0, this);
260
+ const context: Record<string, any> = { ...entry.computedContext };
261
+ if (entry.computedError !== undefined) {
262
+ context.error = entry.computedError;
345
263
  }
346
264
 
265
+ const { filename, line } = entry.computedMeta;
347
266
  const entryToPush: LogEntry = {
348
267
  level: entry.level,
349
- message: entry.message ?? (entry.error ? (entry.error.message ?? String(entry.error)) : ''),
268
+ message: entry.message ?? entry.computedError ?? '',
350
269
  context,
351
- timestamp: new Date(),
270
+ timestamp: new Date(entry.timestamp),
352
271
  meta: {
353
- file: entry.meta?.F ?? '',
354
- line: entry.meta?.L ?? 0,
272
+ file: filename ?? '',
273
+ line: line ?? 0,
355
274
  resourceId: resource.data.id,
356
275
  },
357
276
  };
@@ -363,121 +282,6 @@ export class TraceProcessor {
363
282
  };
364
283
  }
365
284
 
366
- // TODO(burdon): Comment.
367
- export class TracingSpan {
368
- static nextId = 0;
369
-
370
- readonly id: number;
371
- readonly parentId: number | null = null;
372
- readonly methodName: string;
373
- readonly resourceId: number | null = null;
374
- readonly op: string | undefined;
375
- readonly attributes: Record<string, any>;
376
- startTs: number;
377
- endTs: number | null = null;
378
- error: SerializedError | null = null;
379
-
380
- private _showInBrowserTimeline: boolean;
381
- private _showInRemoteTracing: boolean;
382
- private readonly _ctx: Context;
383
-
384
- constructor(
385
- private _traceProcessor: TraceProcessor,
386
- params: TraceSpanProps,
387
- ) {
388
- this.id = TracingSpan.nextId++;
389
- this.methodName = params.methodName;
390
- this.resourceId = _traceProcessor.getResourceId(params.instance);
391
- this.startTs = performance.now();
392
- this._showInBrowserTimeline = params.showInBrowserTimeline;
393
- this._showInRemoteTracing = params.showInRemoteTracing ?? true;
394
- this.op = params.op;
395
- this.attributes = params.attributes ?? {};
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();
401
- if (params.parentCtx) {
402
- const parentId = params.parentCtx.getAttribute(TRACE_SPAN_ATTRIBUTE);
403
- if (typeof parentId === 'number') {
404
- this.parentId = parentId;
405
- }
406
- }
407
- }
408
-
409
- get name() {
410
- const resource = this._traceProcessor.resources.get(this.resourceId!);
411
- return resource ? `${resource.sanitizedClassName}#${resource.data.instanceId}.${this.methodName}` : this.methodName;
412
- }
413
-
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 {
421
- return this._ctx;
422
- }
423
-
424
- get showInRemoteTracing(): boolean {
425
- return this._showInRemoteTracing;
426
- }
427
-
428
- markSuccess(): void {
429
- this.endTs = performance.now();
430
- this._traceProcessor._flushSpan(this);
431
-
432
- if (this._showInBrowserTimeline) {
433
- this._markInBrowserTimeline();
434
- }
435
- }
436
-
437
- markError(err: unknown): void {
438
- this.endTs = performance.now();
439
- this.error = serializeError(err);
440
- this._traceProcessor._flushSpan(this);
441
-
442
- if (this._showInBrowserTimeline) {
443
- this._markInBrowserTimeline();
444
- }
445
- }
446
-
447
- serialize(): Span {
448
- return {
449
- id: this.id,
450
- resourceId: this.resourceId ?? undefined,
451
- methodName: this.methodName,
452
- parentId: this.parentId ?? undefined,
453
- startTs: this.startTs.toFixed(3),
454
- endTs: this.endTs?.toFixed(3) ?? undefined,
455
- error: this.error ?? undefined,
456
- };
457
- }
458
-
459
- private _markInBrowserTimeline(): void {
460
- if (typeof globalThis?.performance?.measure === 'function') {
461
- performance.measure(this.name, { start: this.startTs, end: this.endTs! });
462
- }
463
- }
464
- }
465
-
466
- // TODO(burdon): Log cause.
467
- const serializeError = (err: unknown): SerializedError => {
468
- if (err instanceof Error) {
469
- return {
470
- name: err.name,
471
- message: err.message,
472
- };
473
- }
474
-
475
- return {
476
- message: String(err),
477
- };
478
- };
479
-
480
- // TODO(burdon): Rename singleton and move out of package.
481
285
  export const TRACE_PROCESSOR: TraceProcessor = ((globalThis as any).TRACE_PROCESSOR ??= new TraceProcessor());
482
286
 
483
287
  const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor): any => {
@@ -501,7 +305,6 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
501
305
  }
502
306
 
503
307
  if (typeof value.toJSON === 'function') {
504
- // TODO(dmaretskyi): This has potential to cause infinite recursion.
505
308
  return sanitizeValue(value.toJSON(), depth, traceProcessor);
506
309
  }
507
310
 
@@ -525,7 +328,6 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
525
328
  }
526
329
  }
527
330
 
528
- // TODO(dmaretskyi): Expose trait.
529
331
  if (typeof value.truncate === 'function') {
530
332
  return value.truncate();
531
333
  }
@@ -534,20 +336,6 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
534
336
  }
535
337
  };
536
338
 
537
- const areEqualShallow = (a: any, b: any) => {
538
- for (const key in a) {
539
- if (!(key in b) || a[key] !== b[key]) {
540
- return false;
541
- }
542
- }
543
- for (const key in b) {
544
- if (!(key in a) || a[key] !== b[key]) {
545
- return false;
546
- }
547
- }
548
- return true;
549
- };
550
-
551
339
  export const sanitizeClassName = (className: string) => {
552
340
  let name = className.replace(/^_+/, '');
553
341
  const SANITIZE_REGEX = /[^_](\d+)$/;
@@ -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
+ }