@ohos-ports/dxos-tracing 0.11.1-beta.0

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/LICENSE +105 -0
  2. package/README.md +3 -0
  3. package/dist/lib/index.mjs +509 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/types/src/api.d.ts +47 -0
  6. package/dist/types/src/api.d.ts.map +1 -0
  7. package/dist/types/src/buffering-backend.d.ts +24 -0
  8. package/dist/types/src/buffering-backend.d.ts.map +1 -0
  9. package/dist/types/src/diagnostic.d.ts +38 -0
  10. package/dist/types/src/diagnostic.d.ts.map +1 -0
  11. package/dist/types/src/diagnostics-channel.d.ts +34 -0
  12. package/dist/types/src/diagnostics-channel.d.ts.map +1 -0
  13. package/dist/types/src/index.d.ts +7 -0
  14. package/dist/types/src/index.d.ts.map +1 -0
  15. package/dist/types/src/remote/index.d.ts +2 -0
  16. package/dist/types/src/remote/index.d.ts.map +1 -0
  17. package/dist/types/src/remote/metrics.d.ts +37 -0
  18. package/dist/types/src/remote/metrics.d.ts.map +1 -0
  19. package/dist/types/src/trace-processor.d.ts +29 -0
  20. package/dist/types/src/trace-processor.d.ts.map +1 -0
  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/src/tracing.test.d.ts +2 -0
  24. package/dist/types/src/tracing.test.d.ts.map +1 -0
  25. package/dist/types/src/util.d.ts +2 -0
  26. package/dist/types/src/util.d.ts.map +1 -0
  27. package/dist/types/tsconfig.tsbuildinfo +1 -0
  28. package/package.json +42 -0
  29. package/src/api.ts +204 -0
  30. package/src/buffering-backend.ts +112 -0
  31. package/src/diagnostic.ts +103 -0
  32. package/src/diagnostics-channel.ts +174 -0
  33. package/src/index.ts +25 -0
  34. package/src/remote/index.ts +5 -0
  35. package/src/remote/metrics.ts +57 -0
  36. package/src/trace-processor.ts +82 -0
  37. package/src/tracing-types.ts +77 -0
  38. package/src/tracing.test.ts +296 -0
  39. package/src/util.ts +6 -0
@@ -0,0 +1,82 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import type { AddLinkOptions } from './api';
6
+ import { BUFFERED_PREFIX, BufferingTracingBackend } from './buffering-backend';
7
+ import { DiagnosticsManager } from './diagnostic';
8
+ import { DiagnosticsChannel } from './diagnostics-channel';
9
+ import { RemoteMetrics } from './remote/metrics';
10
+ import type { RemoteSpan, StartSpanOptions, TracingBackend } from './tracing-types';
11
+
12
+ export class TraceProcessor {
13
+ public readonly diagnostics = new DiagnosticsManager();
14
+ public readonly diagnosticsChannel = new DiagnosticsChannel();
15
+ public readonly remoteMetrics = new RemoteMetrics();
16
+
17
+ readonly #bufferingBackend = new BufferingTracingBackend();
18
+ #activeBackend: TracingBackend = this.#bufferingBackend;
19
+
20
+ /**
21
+ * Tracing backend. Initially a buffering backend that records spans;
22
+ * once the observability package sets a real backend, the buffer is drained
23
+ * and a thin translating wrapper is installed that resolves stale buffered
24
+ * parent IDs still held by in-flight {@link Context} objects.
25
+ *
26
+ * The wrapper only allocates when a `buffered-*` parent is actually encountered;
27
+ * the common path is a single `startsWith` check and direct passthrough.
28
+ */
29
+ get tracingBackend(): TracingBackend {
30
+ return this.#activeBackend;
31
+ }
32
+
33
+ set tracingBackend(backend: TracingBackend | undefined) {
34
+ if (!backend || backend === this.#bufferingBackend) {
35
+ this.#bufferingBackend.clear();
36
+ this.#activeBackend = this.#bufferingBackend;
37
+ return;
38
+ }
39
+ const idMap = this.#bufferingBackend.drain(backend);
40
+ this.#activeBackend = {
41
+ startSpan: (options: StartSpanOptions): RemoteSpan => {
42
+ const parent = options.parentContext;
43
+ if (parent?.traceparent.startsWith(BUFFERED_PREFIX)) {
44
+ const translated = idMap.get(parent.traceparent);
45
+ if (translated) {
46
+ return backend.startSpan({ ...options, parentContext: translated });
47
+ }
48
+ }
49
+ return backend.startSpan(options);
50
+ },
51
+ };
52
+ }
53
+
54
+ private _instanceTag: string | null = null;
55
+
56
+ constructor() {
57
+ if (DiagnosticsChannel.supported) {
58
+ this.diagnosticsChannel.serve(this.diagnostics);
59
+ }
60
+ this.diagnosticsChannel.unref();
61
+ }
62
+
63
+ setInstanceTag(tag: string): void {
64
+ this._instanceTag = tag;
65
+ this.diagnostics.setInstanceTag(tag);
66
+ }
67
+
68
+ // TODO(burdon): Not implemented.
69
+ addLink(parent: any, child: any, opts: AddLinkOptions): void {}
70
+ }
71
+
72
+ export const TRACE_PROCESSOR: TraceProcessor = ((globalThis as any).TRACE_PROCESSOR ??= new TraceProcessor());
73
+
74
+ export const sanitizeClassName = (className: string) => {
75
+ let name = className.replace(/^_+/, '');
76
+ const SANITIZE_REGEX = /[^_](\d+)$/;
77
+ const m = name.match(SANITIZE_REGEX);
78
+ if (m) {
79
+ name = name.slice(0, -m[1].length);
80
+ }
81
+ return name;
82
+ };
@@ -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
+ }
@@ -0,0 +1,296 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { afterEach, beforeEach, describe, test } from 'vitest';
6
+
7
+ import { Context, TRACE_SPAN_ATTRIBUTE, type TraceContextData } from '@dxos/context';
8
+
9
+ import { trace } from './api';
10
+ import { TRACE_PROCESSOR } from './trace-processor';
11
+ import type { RemoteSpan, StartSpanOptions, TracingBackend } from './tracing-types';
12
+
13
+ type SpanRecord = {
14
+ options: StartSpanOptions;
15
+ ended: boolean;
16
+ endTime?: number;
17
+ error?: unknown;
18
+ spanContext: TraceContextData;
19
+ };
20
+
21
+ let spanCounter = 0;
22
+
23
+ const createMockBackend = (): { backend: TracingBackend; spans: SpanRecord[] } => {
24
+ const spans: SpanRecord[] = [];
25
+
26
+ const backend: TracingBackend = {
27
+ startSpan: (options: StartSpanOptions): RemoteSpan => {
28
+ const record: SpanRecord = {
29
+ options,
30
+ ended: false,
31
+ spanContext: {
32
+ traceparent: `00-aaaa0000aaaa0000aaaa0000aaaa0000-${String(++spanCounter).padStart(16, '0')}-01`,
33
+ },
34
+ };
35
+ spans.push(record);
36
+ return {
37
+ end: (endTime?: number) => {
38
+ record.ended = true;
39
+ record.endTime = endTime;
40
+ },
41
+ setError: (err: unknown) => {
42
+ record.error = err;
43
+ },
44
+ spanContext: record.spanContext,
45
+ };
46
+ },
47
+ };
48
+
49
+ return { backend, spans };
50
+ };
51
+
52
+ //
53
+ // Manual span tests
54
+ //
55
+
56
+ describe('manual spans', () => {
57
+ let savedBackend: typeof TRACE_PROCESSOR.tracingBackend;
58
+
59
+ beforeEach(() => {
60
+ savedBackend = TRACE_PROCESSOR.tracingBackend;
61
+ spanCounter = 0;
62
+ });
63
+
64
+ afterEach(() => {
65
+ TRACE_PROCESSOR.tracingBackend = savedBackend;
66
+ });
67
+
68
+ test('spanStart nests under the parent context and spanEnd ends the span', ({ expect }) => {
69
+ const { backend, spans } = createMockBackend();
70
+ TRACE_PROCESSOR.tracingBackend = backend;
71
+
72
+ const parentTrace: TraceContextData = {
73
+ traceparent: '00-bbbb0000bbbb0000bbbb0000bbbb0000-cccc0000cccc0000-01',
74
+ };
75
+ const parentCtx = new Context({ attributes: { [TRACE_SPAN_ATTRIBUTE]: parentTrace } });
76
+
77
+ const childCtx = trace.spanStart({ id: 'op-1', instance: {}, methodName: 'work', parentCtx });
78
+
79
+ const span = spans.find((record) => record.options.name.endsWith('.work'));
80
+ expect(span).toBeDefined();
81
+ expect(span!.options.parentContext?.traceparent).toBe(parentTrace.traceparent);
82
+
83
+ // The returned ctx carries the new span's trace context so downstream spans nest under it.
84
+ expect(childCtx?.getAttribute(TRACE_SPAN_ATTRIBUTE)?.traceparent).toBe(span!.spanContext.traceparent);
85
+
86
+ expect(span!.ended).toBe(false);
87
+ trace.spanEnd('op-1');
88
+ expect(span!.ended).toBe(true);
89
+ });
90
+
91
+ test('spanStart with showInRemoteTracing:false creates no remote span and returns parentCtx unchanged', ({
92
+ expect,
93
+ }) => {
94
+ const { backend, spans } = createMockBackend();
95
+ TRACE_PROCESSOR.tracingBackend = backend;
96
+
97
+ const parentCtx = new Context();
98
+ const result = trace.spanStart({
99
+ id: 'op-2',
100
+ instance: {},
101
+ methodName: 'work',
102
+ parentCtx,
103
+ showInRemoteTracing: false,
104
+ });
105
+
106
+ expect(result).toBe(parentCtx);
107
+ expect(spans.find((record) => record.options.name.endsWith('.work'))).toBeUndefined();
108
+ });
109
+
110
+ test('duplicate spanStart id returns parentCtx without starting a second span', ({ expect }) => {
111
+ const { backend, spans } = createMockBackend();
112
+ TRACE_PROCESSOR.tracingBackend = backend;
113
+
114
+ const parentCtx = new Context();
115
+ trace.spanStart({ id: 'op-3', instance: {}, methodName: 'work', parentCtx });
116
+ const secondResult = trace.spanStart({ id: 'op-3', instance: {}, methodName: 'work', parentCtx });
117
+
118
+ expect(secondResult).toBe(parentCtx);
119
+ expect(spans.filter((record) => record.options.name.endsWith('.work'))).toHaveLength(1);
120
+
121
+ trace.spanEnd('op-3');
122
+ });
123
+ });
124
+
125
+ //
126
+ // Buffering backend tests
127
+ //
128
+
129
+ describe('buffering backend', () => {
130
+ let savedBackend: typeof TRACE_PROCESSOR.tracingBackend;
131
+
132
+ beforeEach(() => {
133
+ savedBackend = TRACE_PROCESSOR.tracingBackend;
134
+ TRACE_PROCESSOR.tracingBackend = undefined;
135
+ spanCounter = 0;
136
+ });
137
+
138
+ afterEach(() => {
139
+ TRACE_PROCESSOR.tracingBackend = savedBackend;
140
+ });
141
+
142
+ test('buffered spans are replayed into real backend on drain', async ({ expect }) => {
143
+ const parentCtx = new Context();
144
+
145
+ class Svc {
146
+ @trace.span()
147
+ async work(ctx: Context) {}
148
+ }
149
+
150
+ const svc = new Svc();
151
+ await svc.work(parentCtx);
152
+
153
+ const { backend, spans } = createMockBackend();
154
+ TRACE_PROCESSOR.tracingBackend = backend;
155
+
156
+ expect(spans.length).toBeGreaterThanOrEqual(1);
157
+ const workSpan = spans.find((span) => span.options.name === 'Svc.work');
158
+ expect(workSpan).toBeDefined();
159
+ expect(workSpan!.ended).toBe(true);
160
+ });
161
+
162
+ test('parent-child hierarchy is preserved across drain', ({ expect }) => {
163
+ class Svc {
164
+ @trace.span()
165
+ async parent(ctx: Context) {
166
+ await this.child(ctx);
167
+ }
168
+
169
+ @trace.span()
170
+ async child(ctx: Context) {}
171
+ }
172
+
173
+ const svc = new Svc();
174
+ void svc.parent(new Context());
175
+
176
+ const { backend, spans } = createMockBackend();
177
+ TRACE_PROCESSOR.tracingBackend = backend;
178
+
179
+ const parentSpan = spans.find((span) => span.options.name === 'Svc.parent');
180
+ const childSpan = spans.find((span) => span.options.name === 'Svc.child');
181
+ expect(parentSpan).toBeDefined();
182
+ expect(childSpan).toBeDefined();
183
+ expect(childSpan!.options.parentContext?.traceparent).toBe(parentSpan!.spanContext.traceparent);
184
+ });
185
+
186
+ test('stale buffered parent IDs on in-flight contexts are translated post-drain', async ({ expect }) => {
187
+ let capturedCtx: Context | undefined;
188
+
189
+ class Svc {
190
+ @trace.span()
191
+ async setup(ctx: Context) {
192
+ capturedCtx = ctx;
193
+ }
194
+
195
+ @trace.span()
196
+ async laterWork(ctx: Context) {}
197
+ }
198
+
199
+ const svc = new Svc();
200
+ await svc.setup(new Context());
201
+ expect(capturedCtx).toBeDefined();
202
+
203
+ // Now register the real backend.
204
+ const { backend, spans } = createMockBackend();
205
+ TRACE_PROCESSOR.tracingBackend = backend;
206
+
207
+ // The capturedCtx carries a buffered-* traceparent from the setup span.
208
+ // Calling laterWork with it should translate the stale buffered ID.
209
+ await svc.laterWork(capturedCtx!);
210
+
211
+ const setupSpan = spans.find((span) => span.options.name === 'Svc.setup');
212
+ const laterSpan = spans.find((span) => span.options.name === 'Svc.laterWork');
213
+ expect(setupSpan).toBeDefined();
214
+ expect(laterSpan).toBeDefined();
215
+ expect(laterSpan!.options.parentContext?.traceparent).toBe(setupSpan!.spanContext.traceparent);
216
+ });
217
+
218
+ test('errors and end() are replayed on drain', async ({ expect }) => {
219
+ const testError = new Error('boom');
220
+
221
+ class Svc {
222
+ @trace.span()
223
+ async failingWork(ctx: Context) {
224
+ throw testError;
225
+ }
226
+ }
227
+
228
+ const svc = new Svc();
229
+ await svc.failingWork(new Context()).catch(() => {});
230
+
231
+ const { backend, spans } = createMockBackend();
232
+ TRACE_PROCESSOR.tracingBackend = backend;
233
+
234
+ const failSpan = spans.find((span) => span.options.name === 'Svc.failingWork');
235
+ expect(failSpan).toBeDefined();
236
+ expect(failSpan!.error).toBe(testError);
237
+ expect(failSpan!.ended).toBe(true);
238
+ });
239
+
240
+ test('still-open spans forward end() to real backend after drain', async ({ expect }) => {
241
+ let resolveWork: () => void;
242
+ const workPromise = new Promise<void>((resolve) => {
243
+ resolveWork = resolve;
244
+ });
245
+
246
+ class Svc {
247
+ @trace.span()
248
+ async longWork(ctx: Context) {
249
+ await workPromise;
250
+ }
251
+ }
252
+
253
+ const svc = new Svc();
254
+ const done = svc.longWork(new Context());
255
+
256
+ const { backend, spans } = createMockBackend();
257
+ TRACE_PROCESSOR.tracingBackend = backend;
258
+
259
+ const longSpan = spans.find((span) => span.options.name === 'Svc.longWork');
260
+ expect(longSpan).toBeDefined();
261
+ expect(longSpan!.ended).toBe(false);
262
+
263
+ resolveWork!();
264
+ await done;
265
+
266
+ expect(longSpan!.ended).toBe(true);
267
+ });
268
+
269
+ test('replayed spans preserve original start and end timestamps', async ({ expect }) => {
270
+ const beforeStart = Date.now();
271
+
272
+ class Svc {
273
+ @trace.span()
274
+ async work(ctx: Context) {}
275
+ }
276
+
277
+ const svc = new Svc();
278
+ await svc.work(new Context());
279
+
280
+ const afterEnd = Date.now();
281
+
282
+ const { backend, spans } = createMockBackend();
283
+ TRACE_PROCESSOR.tracingBackend = backend;
284
+
285
+ const workSpan = spans.find((span) => span.options.name === 'Svc.work');
286
+ expect(workSpan).toBeDefined();
287
+
288
+ expect(workSpan!.options.startTime).toBeTypeOf('number');
289
+ expect(workSpan!.options.startTime).toBeGreaterThanOrEqual(beforeStart);
290
+ expect(workSpan!.options.startTime).toBeLessThanOrEqual(afterEnd);
291
+
292
+ expect(workSpan!.endTime).toBeTypeOf('number');
293
+ expect(workSpan!.endTime).toBeGreaterThanOrEqual(workSpan!.options.startTime!);
294
+ expect(workSpan!.endTime).toBeLessThanOrEqual(afterEnd);
295
+ });
296
+ });
package/src/util.ts ADDED
@@ -0,0 +1,6 @@
1
+ // TODO(dmaretskyi): Use UUID.
2
+ //
3
+ // Copyright 2024 DXOS.org
4
+ //
5
+
6
+ export const createId = () => Math.random().toString(36).slice(2);