@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.
Files changed (48) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +331 -393
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +331 -393
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/api.d.ts +36 -17
  9. package/dist/types/src/api.d.ts.map +1 -1
  10. package/dist/types/src/buffering-backend.d.ts +24 -0
  11. package/dist/types/src/buffering-backend.d.ts.map +1 -0
  12. package/dist/types/src/diagnostic.d.ts +2 -2
  13. package/dist/types/src/diagnostic.d.ts.map +1 -1
  14. package/dist/types/src/diagnostics-channel.d.ts.map +1 -1
  15. package/dist/types/src/index.d.ts +1 -2
  16. package/dist/types/src/index.d.ts.map +1 -1
  17. package/dist/types/src/metrics/base.d.ts.map +1 -1
  18. package/dist/types/src/metrics/custom-counter.d.ts.map +1 -1
  19. package/dist/types/src/metrics/map-counter.d.ts.map +1 -1
  20. package/dist/types/src/metrics/time-series-counter.d.ts.map +1 -1
  21. package/dist/types/src/metrics/time-usage-counter.d.ts.map +1 -1
  22. package/dist/types/src/metrics/unary-counter.d.ts.map +1 -1
  23. package/dist/types/src/remote/index.d.ts +0 -1
  24. package/dist/types/src/remote/index.d.ts.map +1 -1
  25. package/dist/types/src/remote/metrics.d.ts.map +1 -1
  26. package/dist/types/src/symbols.d.ts +0 -1
  27. package/dist/types/src/symbols.d.ts.map +1 -1
  28. package/dist/types/src/trace-processor.d.ts +16 -52
  29. package/dist/types/src/trace-processor.d.ts.map +1 -1
  30. package/dist/types/src/tracing-types.d.ts +67 -0
  31. package/dist/types/src/tracing-types.d.ts.map +1 -0
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +14 -13
  34. package/src/api.ts +237 -35
  35. package/src/buffering-backend.ts +112 -0
  36. package/src/diagnostic.ts +2 -2
  37. package/src/index.ts +1 -2
  38. package/src/remote/index.ts +0 -1
  39. package/src/symbols.ts +0 -2
  40. package/src/trace-processor.ts +58 -258
  41. package/src/tracing-types.ts +77 -0
  42. package/src/tracing.test.ts +513 -4
  43. package/dist/types/src/remote/tracing.d.ts +0 -23
  44. package/dist/types/src/remote/tracing.d.ts.map +0 -1
  45. package/dist/types/src/trace-sender.d.ts +0 -9
  46. package/dist/types/src/trace-sender.d.ts.map +0 -1
  47. package/src/remote/tracing.ts +0 -53
  48. 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
- 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
 
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
  }
@@ -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) => {
@@ -338,19 +257,20 @@ export class TraceProcessor {
338
257
  return;
339
258
  }
340
259
 
341
- const context = getContextFromEntry(entry) ?? {};
342
- for (const key of Object.keys(context)) {
343
- 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;
344
263
  }
345
264
 
265
+ const { filename, line } = entry.computedMeta;
346
266
  const entryToPush: LogEntry = {
347
267
  level: entry.level,
348
- message: entry.message ?? (entry.error ? (entry.error.message ?? String(entry.error)) : ''),
268
+ message: entry.message ?? entry.computedError ?? '',
349
269
  context,
350
- timestamp: new Date(),
270
+ timestamp: new Date(entry.timestamp),
351
271
  meta: {
352
- file: entry.meta?.F ?? '',
353
- line: entry.meta?.L ?? 0,
272
+ file: filename ?? '',
273
+ line: line ?? 0,
354
274
  resourceId: resource.data.id,
355
275
  },
356
276
  };
@@ -362,110 +282,6 @@ export class TraceProcessor {
362
282
  };
363
283
  }
364
284
 
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
285
  export const TRACE_PROCESSOR: TraceProcessor = ((globalThis as any).TRACE_PROCESSOR ??= new TraceProcessor());
470
286
 
471
287
  const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor): any => {
@@ -489,7 +305,6 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
489
305
  }
490
306
 
491
307
  if (typeof value.toJSON === 'function') {
492
- // TODO(dmaretskyi): This has potential to cause infinite recursion.
493
308
  return sanitizeValue(value.toJSON(), depth, traceProcessor);
494
309
  }
495
310
 
@@ -513,7 +328,6 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
513
328
  }
514
329
  }
515
330
 
516
- // TODO(dmaretskyi): Expose trait.
517
331
  if (typeof value.truncate === 'function') {
518
332
  return value.truncate();
519
333
  }
@@ -522,28 +336,14 @@ const sanitizeValue = (value: any, depth: number, traceProcessor: TraceProcessor
522
336
  }
523
337
  };
524
338
 
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
339
  export const sanitizeClassName = (className: string) => {
340
+ let name = className.replace(/^_+/, '');
540
341
  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);
342
+ const m = name.match(SANITIZE_REGEX);
343
+ if (m) {
344
+ name = name.slice(0, -m[1].length);
546
345
  }
346
+ return name;
547
347
  };
548
348
 
549
349
  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
+ }