@dxos/tracing 0.8.3 → 0.8.4-main.03d5cd7b56
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/dist/lib/browser/index.mjs +416 -456
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +416 -456
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/api.d.ts +36 -17
- package/dist/types/src/api.d.ts.map +1 -1
- package/dist/types/src/buffering-backend.d.ts +24 -0
- package/dist/types/src/buffering-backend.d.ts.map +1 -0
- package/dist/types/src/diagnostic.d.ts +2 -2
- package/dist/types/src/diagnostic.d.ts.map +1 -1
- package/dist/types/src/diagnostics-channel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/metrics/base.d.ts.map +1 -1
- package/dist/types/src/metrics/custom-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/map-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/time-series-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/time-usage-counter.d.ts.map +1 -1
- package/dist/types/src/metrics/unary-counter.d.ts.map +1 -1
- package/dist/types/src/remote/index.d.ts +0 -1
- package/dist/types/src/remote/index.d.ts.map +1 -1
- package/dist/types/src/remote/metrics.d.ts.map +1 -1
- package/dist/types/src/symbols.d.ts +0 -1
- package/dist/types/src/symbols.d.ts.map +1 -1
- package/dist/types/src/trace-processor.d.ts +16 -52
- package/dist/types/src/trace-processor.d.ts.map +1 -1
- package/dist/types/src/tracing-types.d.ts +67 -0
- package/dist/types/src/tracing-types.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -14
- package/src/api.ts +237 -35
- package/src/buffering-backend.ts +112 -0
- package/src/diagnostic.ts +2 -2
- package/src/index.ts +1 -2
- package/src/remote/index.ts +0 -1
- package/src/symbols.ts +0 -2
- package/src/trace-processor.ts +59 -259
- package/src/tracing-types.ts +77 -0
- package/src/tracing.test.ts +513 -4
- package/dist/lib/node/index.cjs +0 -1111
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/types/src/remote/tracing.d.ts +0 -23
- package/dist/types/src/remote/tracing.d.ts.map +0 -1
- package/dist/types/src/trace-sender.d.ts +0 -9
- package/dist/types/src/trace-sender.d.ts.map +0 -1
- package/src/remote/tracing.ts +0 -53
- package/src/trace-sender.ts +0 -88
package/src/tracing.test.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { beforeEach, describe, expect, test } from 'vitest';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { Context } from '@dxos/context';
|
|
8
|
-
import { log } from '@dxos/log';
|
|
7
|
+
import { Context, Resource, TRACE_SPAN_ATTRIBUTE, type TraceContextData } from '@dxos/context';
|
|
9
8
|
|
|
10
9
|
import { trace } from './api';
|
|
11
10
|
import { TRACE_PROCESSOR } from './trace-processor';
|
|
11
|
+
import type { RemoteSpan, StartSpanOptions, TracingBackend } from './tracing-types';
|
|
12
12
|
|
|
13
13
|
const FeedStoreResource = Symbol.for('FeedStore');
|
|
14
14
|
|
|
@@ -62,7 +62,6 @@ describe('tracing', () => {
|
|
|
62
62
|
await feed2.close().catch(() => {});
|
|
63
63
|
|
|
64
64
|
expect([...TRACE_PROCESSOR.resources.values()].map((r) => r.instance.deref())).to.deep.eq([store, feed1, feed2]);
|
|
65
|
-
log.info('spans', { spans: TRACE_PROCESSOR.spans });
|
|
66
65
|
});
|
|
67
66
|
|
|
68
67
|
test('findByAnnotation', async () => {
|
|
@@ -70,3 +69,513 @@ describe('tracing', () => {
|
|
|
70
69
|
expect(TRACE_PROCESSOR.findResourcesByAnnotation(FeedStoreResource)[0].instance.deref()).to.eq(store);
|
|
71
70
|
});
|
|
72
71
|
});
|
|
72
|
+
|
|
73
|
+
//
|
|
74
|
+
// Lifecycle span tests
|
|
75
|
+
//
|
|
76
|
+
|
|
77
|
+
type SpanRecord = {
|
|
78
|
+
options: StartSpanOptions;
|
|
79
|
+
ended: boolean;
|
|
80
|
+
endTime?: number;
|
|
81
|
+
error?: unknown;
|
|
82
|
+
spanContext: TraceContextData;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let spanCounter = 0;
|
|
86
|
+
|
|
87
|
+
const createMockBackend = (): { backend: TracingBackend; spans: SpanRecord[] } => {
|
|
88
|
+
const spans: SpanRecord[] = [];
|
|
89
|
+
|
|
90
|
+
const backend: TracingBackend = {
|
|
91
|
+
startSpan: (options: StartSpanOptions): RemoteSpan => {
|
|
92
|
+
const record: SpanRecord = {
|
|
93
|
+
options,
|
|
94
|
+
ended: false,
|
|
95
|
+
spanContext: {
|
|
96
|
+
traceparent: `00-aaaa0000aaaa0000aaaa0000aaaa0000-${String(++spanCounter).padStart(16, '0')}-01`,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
spans.push(record);
|
|
100
|
+
return {
|
|
101
|
+
end: (endTime?: number) => {
|
|
102
|
+
record.ended = true;
|
|
103
|
+
record.endTime = endTime;
|
|
104
|
+
},
|
|
105
|
+
setError: (err: unknown) => {
|
|
106
|
+
record.error = err;
|
|
107
|
+
},
|
|
108
|
+
spanContext: record.spanContext,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return { backend, spans };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
describe('lifecycle span', () => {
|
|
117
|
+
let savedBackend: typeof TRACE_PROCESSOR.tracingBackend;
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
TRACE_PROCESSOR.resources.clear();
|
|
121
|
+
savedBackend = TRACE_PROCESSOR.tracingBackend;
|
|
122
|
+
spanCounter = 0;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterEach(() => {
|
|
126
|
+
TRACE_PROCESSOR.tracingBackend = savedBackend;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('lifecycle span is started on open and ended on close', async ({ expect }) => {
|
|
130
|
+
const { backend, spans } = createMockBackend();
|
|
131
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
132
|
+
|
|
133
|
+
@trace.resource({ lifecycle: true })
|
|
134
|
+
class TestResource extends Resource {
|
|
135
|
+
protected override async _open(_ctx: Context) {}
|
|
136
|
+
protected override async _close(_ctx: Context) {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resource = new TestResource();
|
|
140
|
+
await resource.open();
|
|
141
|
+
|
|
142
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'TestResource.lifecycle');
|
|
143
|
+
expect(lifecycleSpan).toBeDefined();
|
|
144
|
+
expect(lifecycleSpan!.options.op).toBe('lifecycle');
|
|
145
|
+
expect(lifecycleSpan!.ended).toBe(false);
|
|
146
|
+
|
|
147
|
+
await resource.close();
|
|
148
|
+
expect(lifecycleSpan!.ended).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('this._ctx carries lifecycle span trace context', async ({ expect }) => {
|
|
152
|
+
const { backend, spans } = createMockBackend();
|
|
153
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
154
|
+
|
|
155
|
+
let capturedCtxTraceData: TraceContextData | undefined;
|
|
156
|
+
|
|
157
|
+
@trace.resource({ lifecycle: true })
|
|
158
|
+
class TestResource extends Resource {
|
|
159
|
+
protected override async _open(_ctx: Context) {
|
|
160
|
+
capturedCtxTraceData = this._ctx.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const resource = new TestResource();
|
|
165
|
+
await resource.open();
|
|
166
|
+
|
|
167
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'TestResource.lifecycle');
|
|
168
|
+
expect(capturedCtxTraceData).toBeDefined();
|
|
169
|
+
expect(capturedCtxTraceData!.traceparent).toBe(lifecycleSpan!.spanContext.traceparent);
|
|
170
|
+
|
|
171
|
+
await resource.close();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('@trace.span() inside _open is child of lifecycle span', async ({ expect }) => {
|
|
175
|
+
const { backend, spans } = createMockBackend();
|
|
176
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
177
|
+
|
|
178
|
+
@trace.resource({ lifecycle: true })
|
|
179
|
+
class TestResource extends Resource {
|
|
180
|
+
@trace.span()
|
|
181
|
+
protected override async _open(_ctx: Context) {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const resource = new TestResource();
|
|
185
|
+
await resource.open();
|
|
186
|
+
|
|
187
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'TestResource.lifecycle');
|
|
188
|
+
const openSpan = spans.find((span) => span.options.name === 'TestResource._open');
|
|
189
|
+
expect(lifecycleSpan).toBeDefined();
|
|
190
|
+
expect(openSpan).toBeDefined();
|
|
191
|
+
expect(openSpan!.options.parentContext?.traceparent).toBe(lifecycleSpan!.spanContext.traceparent);
|
|
192
|
+
|
|
193
|
+
await resource.close();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('subscriptions use lifecycle context, not _open context', async ({ expect }) => {
|
|
197
|
+
const { backend, spans } = createMockBackend();
|
|
198
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
199
|
+
|
|
200
|
+
let subscriptionParentTrace: TraceContextData | undefined;
|
|
201
|
+
|
|
202
|
+
@trace.resource({ lifecycle: true })
|
|
203
|
+
class TestResource extends Resource {
|
|
204
|
+
@trace.span()
|
|
205
|
+
protected override async _open(_ctx: Context) {
|
|
206
|
+
// Simulate a subscription callback using this._ctx.
|
|
207
|
+
subscriptionParentTrace = this._ctx.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const resource = new TestResource();
|
|
212
|
+
await resource.open();
|
|
213
|
+
|
|
214
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'TestResource.lifecycle');
|
|
215
|
+
const openSpan = spans.find((span) => span.options.name === 'TestResource._open');
|
|
216
|
+
|
|
217
|
+
expect(subscriptionParentTrace!.traceparent).toBe(lifecycleSpan!.spanContext.traceparent);
|
|
218
|
+
expect(subscriptionParentTrace!.traceparent).not.toBe(openSpan!.spanContext.traceparent);
|
|
219
|
+
|
|
220
|
+
await resource.close();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('nested resource lifecycles are nested', async ({ expect }) => {
|
|
224
|
+
const { backend, spans } = createMockBackend();
|
|
225
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
226
|
+
|
|
227
|
+
@trace.resource({ lifecycle: true })
|
|
228
|
+
class ChildResource extends Resource {
|
|
229
|
+
protected override async _open(_ctx: Context) {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@trace.resource({ lifecycle: true })
|
|
233
|
+
class ParentResource extends Resource {
|
|
234
|
+
child = new ChildResource();
|
|
235
|
+
|
|
236
|
+
@trace.span()
|
|
237
|
+
protected override async _open(ctx: Context) {
|
|
238
|
+
await this.child.open(ctx);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
protected override async _close(_ctx: Context) {
|
|
242
|
+
await this.child.close();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const parent = new ParentResource();
|
|
247
|
+
await parent.open();
|
|
248
|
+
|
|
249
|
+
const parentLifecycle = spans.find((span) => span.options.name === 'ParentResource.lifecycle');
|
|
250
|
+
const parentOpen = spans.find((span) => span.options.name === 'ParentResource._open');
|
|
251
|
+
const childLifecycle = spans.find((span) => span.options.name === 'ChildResource.lifecycle');
|
|
252
|
+
|
|
253
|
+
expect(parentLifecycle).toBeDefined();
|
|
254
|
+
expect(parentOpen).toBeDefined();
|
|
255
|
+
expect(childLifecycle).toBeDefined();
|
|
256
|
+
|
|
257
|
+
// Parent._open is child of Parent.lifecycle.
|
|
258
|
+
expect(parentOpen!.options.parentContext?.traceparent).toBe(parentLifecycle!.spanContext.traceparent);
|
|
259
|
+
// Child.lifecycle is child of Parent._open (direct async call).
|
|
260
|
+
expect(childLifecycle!.options.parentContext?.traceparent).toBe(parentOpen!.spanContext.traceparent);
|
|
261
|
+
|
|
262
|
+
await parent.close();
|
|
263
|
+
expect(childLifecycle!.ended).toBe(true);
|
|
264
|
+
expect(parentLifecycle!.ended).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('lifecycle requires Resource base class', ({ expect }) => {
|
|
268
|
+
expect(() => {
|
|
269
|
+
@trace.resource({ lifecycle: true })
|
|
270
|
+
class NotAResource {
|
|
271
|
+
async open() {}
|
|
272
|
+
async close() {}
|
|
273
|
+
}
|
|
274
|
+
void NotAResource;
|
|
275
|
+
}).toThrow(/requires.*to extend Resource/);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('no lifecycle span without opt-in', async ({ expect }) => {
|
|
279
|
+
const { backend, spans } = createMockBackend();
|
|
280
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
281
|
+
|
|
282
|
+
@trace.resource()
|
|
283
|
+
class PlainResource extends Resource {
|
|
284
|
+
@trace.span()
|
|
285
|
+
protected override async _open(_ctx: Context) {}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const resource = new PlainResource();
|
|
289
|
+
await resource.open();
|
|
290
|
+
|
|
291
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'PlainResource.lifecycle');
|
|
292
|
+
expect(lifecycleSpan).toBeUndefined();
|
|
293
|
+
|
|
294
|
+
await resource.close();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('lifecycle span records error when _open throws', async ({ expect }) => {
|
|
298
|
+
const { backend, spans } = createMockBackend();
|
|
299
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
300
|
+
|
|
301
|
+
const openError = new Error('open failed');
|
|
302
|
+
|
|
303
|
+
@trace.resource({ lifecycle: true })
|
|
304
|
+
class FailingResource extends Resource {
|
|
305
|
+
protected override async _open(_ctx: Context) {
|
|
306
|
+
throw openError;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const resource = new FailingResource();
|
|
311
|
+
await expect(resource.open()).rejects.toThrow('open failed');
|
|
312
|
+
|
|
313
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'FailingResource.lifecycle');
|
|
314
|
+
expect(lifecycleSpan).toBeDefined();
|
|
315
|
+
expect(lifecycleSpan!.error).toBe(openError);
|
|
316
|
+
expect(lifecycleSpan!.ended).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('double open does not start a second lifecycle span', async ({ expect }) => {
|
|
320
|
+
const { backend, spans } = createMockBackend();
|
|
321
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
322
|
+
|
|
323
|
+
@trace.resource({ lifecycle: true })
|
|
324
|
+
class TestResource extends Resource {
|
|
325
|
+
protected override async _open(_ctx: Context) {}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const resource = new TestResource();
|
|
329
|
+
await resource.open();
|
|
330
|
+
await resource.open();
|
|
331
|
+
|
|
332
|
+
const lifecycleSpans = spans.filter((span) => span.options.name === 'TestResource.lifecycle');
|
|
333
|
+
expect(lifecycleSpans).toHaveLength(1);
|
|
334
|
+
|
|
335
|
+
await resource.close();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('lifecycle span works without tracing backend', async () => {
|
|
339
|
+
TRACE_PROCESSOR.tracingBackend = undefined;
|
|
340
|
+
|
|
341
|
+
@trace.resource({ lifecycle: true })
|
|
342
|
+
class TestResource extends Resource {
|
|
343
|
+
protected override async _open(_ctx: Context) {}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const resource = new TestResource();
|
|
347
|
+
await resource.open();
|
|
348
|
+
await resource.close();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('caller trace context is parent of lifecycle span', async ({ expect }) => {
|
|
352
|
+
const { backend, spans } = createMockBackend();
|
|
353
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
354
|
+
|
|
355
|
+
@trace.resource({ lifecycle: true })
|
|
356
|
+
class TestResource extends Resource {
|
|
357
|
+
protected override async _open(_ctx: Context) {}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const callerTraceData: TraceContextData = {
|
|
361
|
+
traceparent: '00-bbbb0000bbbb0000bbbb0000bbbb0000-cccc0000cccc0000-01',
|
|
362
|
+
};
|
|
363
|
+
const callerCtx = new Context({ attributes: { [TRACE_SPAN_ATTRIBUTE]: callerTraceData } });
|
|
364
|
+
|
|
365
|
+
const resource = new TestResource();
|
|
366
|
+
await resource.open(callerCtx);
|
|
367
|
+
|
|
368
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'TestResource.lifecycle');
|
|
369
|
+
expect(lifecycleSpan!.options.parentContext?.traceparent).toBe(callerTraceData.traceparent);
|
|
370
|
+
|
|
371
|
+
await resource.close();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('lifecycle span records error and ends when _close throws', async ({ expect }) => {
|
|
375
|
+
const { backend, spans } = createMockBackend();
|
|
376
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
377
|
+
|
|
378
|
+
const closeError = new Error('close failed');
|
|
379
|
+
|
|
380
|
+
@trace.resource({ lifecycle: true })
|
|
381
|
+
class FailingCloseResource extends Resource {
|
|
382
|
+
protected override async _open(_ctx: Context) {}
|
|
383
|
+
|
|
384
|
+
protected override async _close(_ctx: Context) {
|
|
385
|
+
throw closeError;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const resource = new FailingCloseResource();
|
|
390
|
+
await resource.open();
|
|
391
|
+
|
|
392
|
+
const lifecycleSpan = spans.find((span) => span.options.name === 'FailingCloseResource.lifecycle');
|
|
393
|
+
expect(lifecycleSpan).toBeDefined();
|
|
394
|
+
expect(lifecycleSpan!.ended).toBe(false);
|
|
395
|
+
|
|
396
|
+
await expect(resource.close()).rejects.toThrow('close failed');
|
|
397
|
+
|
|
398
|
+
expect(lifecycleSpan!.error).toBe(closeError);
|
|
399
|
+
expect(lifecycleSpan!.ended).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
//
|
|
404
|
+
// Buffering backend tests
|
|
405
|
+
//
|
|
406
|
+
|
|
407
|
+
describe('buffering backend', () => {
|
|
408
|
+
let savedBackend: typeof TRACE_PROCESSOR.tracingBackend;
|
|
409
|
+
|
|
410
|
+
beforeEach(() => {
|
|
411
|
+
TRACE_PROCESSOR.resources.clear();
|
|
412
|
+
savedBackend = TRACE_PROCESSOR.tracingBackend;
|
|
413
|
+
TRACE_PROCESSOR.tracingBackend = undefined;
|
|
414
|
+
spanCounter = 0;
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
afterEach(() => {
|
|
418
|
+
TRACE_PROCESSOR.tracingBackend = savedBackend;
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('buffered spans are replayed into real backend on drain', async ({ expect }) => {
|
|
422
|
+
const parentCtx = new Context();
|
|
423
|
+
|
|
424
|
+
@trace.resource()
|
|
425
|
+
class Svc {
|
|
426
|
+
@trace.span()
|
|
427
|
+
async work(ctx: Context) {}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const svc = new Svc();
|
|
431
|
+
await svc.work(parentCtx);
|
|
432
|
+
|
|
433
|
+
const { backend, spans } = createMockBackend();
|
|
434
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
435
|
+
|
|
436
|
+
expect(spans.length).toBeGreaterThanOrEqual(1);
|
|
437
|
+
const workSpan = spans.find((span) => span.options.name === 'Svc.work');
|
|
438
|
+
expect(workSpan).toBeDefined();
|
|
439
|
+
expect(workSpan!.ended).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('parent-child hierarchy is preserved across drain', ({ expect }) => {
|
|
443
|
+
@trace.resource()
|
|
444
|
+
class Svc {
|
|
445
|
+
@trace.span()
|
|
446
|
+
async parent(ctx: Context) {
|
|
447
|
+
await this.child(ctx);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
@trace.span()
|
|
451
|
+
async child(ctx: Context) {}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const svc = new Svc();
|
|
455
|
+
void svc.parent(new Context());
|
|
456
|
+
|
|
457
|
+
const { backend, spans } = createMockBackend();
|
|
458
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
459
|
+
|
|
460
|
+
const parentSpan = spans.find((span) => span.options.name === 'Svc.parent');
|
|
461
|
+
const childSpan = spans.find((span) => span.options.name === 'Svc.child');
|
|
462
|
+
expect(parentSpan).toBeDefined();
|
|
463
|
+
expect(childSpan).toBeDefined();
|
|
464
|
+
expect(childSpan!.options.parentContext?.traceparent).toBe(parentSpan!.spanContext.traceparent);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('stale buffered parent IDs on in-flight contexts are translated post-drain', async ({ expect }) => {
|
|
468
|
+
let capturedCtx: Context | undefined;
|
|
469
|
+
|
|
470
|
+
@trace.resource()
|
|
471
|
+
class Svc {
|
|
472
|
+
@trace.span()
|
|
473
|
+
async setup(ctx: Context) {
|
|
474
|
+
capturedCtx = ctx;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@trace.span()
|
|
478
|
+
async laterWork(ctx: Context) {}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const svc = new Svc();
|
|
482
|
+
await svc.setup(new Context());
|
|
483
|
+
expect(capturedCtx).toBeDefined();
|
|
484
|
+
|
|
485
|
+
// Now register the real backend.
|
|
486
|
+
const { backend, spans } = createMockBackend();
|
|
487
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
488
|
+
|
|
489
|
+
// The capturedCtx carries a buffered-* traceparent from the setup span.
|
|
490
|
+
// Calling laterWork with it should translate the stale buffered ID.
|
|
491
|
+
await svc.laterWork(capturedCtx!);
|
|
492
|
+
|
|
493
|
+
const setupSpan = spans.find((span) => span.options.name === 'Svc.setup');
|
|
494
|
+
const laterSpan = spans.find((span) => span.options.name === 'Svc.laterWork');
|
|
495
|
+
expect(setupSpan).toBeDefined();
|
|
496
|
+
expect(laterSpan).toBeDefined();
|
|
497
|
+
expect(laterSpan!.options.parentContext?.traceparent).toBe(setupSpan!.spanContext.traceparent);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test('errors and end() are replayed on drain', async ({ expect }) => {
|
|
501
|
+
const testError = new Error('boom');
|
|
502
|
+
|
|
503
|
+
@trace.resource()
|
|
504
|
+
class Svc {
|
|
505
|
+
@trace.span()
|
|
506
|
+
async failingWork(ctx: Context) {
|
|
507
|
+
throw testError;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const svc = new Svc();
|
|
512
|
+
await svc.failingWork(new Context()).catch(() => {});
|
|
513
|
+
|
|
514
|
+
const { backend, spans } = createMockBackend();
|
|
515
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
516
|
+
|
|
517
|
+
const failSpan = spans.find((span) => span.options.name === 'Svc.failingWork');
|
|
518
|
+
expect(failSpan).toBeDefined();
|
|
519
|
+
expect(failSpan!.error).toBe(testError);
|
|
520
|
+
expect(failSpan!.ended).toBe(true);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('still-open spans forward end() to real backend after drain', async ({ expect }) => {
|
|
524
|
+
let resolveWork: () => void;
|
|
525
|
+
const workPromise = new Promise<void>((resolve) => {
|
|
526
|
+
resolveWork = resolve;
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
@trace.resource()
|
|
530
|
+
class Svc {
|
|
531
|
+
@trace.span()
|
|
532
|
+
async longWork(ctx: Context) {
|
|
533
|
+
await workPromise;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const svc = new Svc();
|
|
538
|
+
const done = svc.longWork(new Context());
|
|
539
|
+
|
|
540
|
+
const { backend, spans } = createMockBackend();
|
|
541
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
542
|
+
|
|
543
|
+
const longSpan = spans.find((span) => span.options.name === 'Svc.longWork');
|
|
544
|
+
expect(longSpan).toBeDefined();
|
|
545
|
+
expect(longSpan!.ended).toBe(false);
|
|
546
|
+
|
|
547
|
+
resolveWork!();
|
|
548
|
+
await done;
|
|
549
|
+
|
|
550
|
+
expect(longSpan!.ended).toBe(true);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('replayed spans preserve original start and end timestamps', async ({ expect }) => {
|
|
554
|
+
const beforeStart = Date.now();
|
|
555
|
+
|
|
556
|
+
@trace.resource()
|
|
557
|
+
class Svc {
|
|
558
|
+
@trace.span()
|
|
559
|
+
async work(ctx: Context) {}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const svc = new Svc();
|
|
563
|
+
await svc.work(new Context());
|
|
564
|
+
|
|
565
|
+
const afterEnd = Date.now();
|
|
566
|
+
|
|
567
|
+
const { backend, spans } = createMockBackend();
|
|
568
|
+
TRACE_PROCESSOR.tracingBackend = backend;
|
|
569
|
+
|
|
570
|
+
const workSpan = spans.find((span) => span.options.name === 'Svc.work');
|
|
571
|
+
expect(workSpan).toBeDefined();
|
|
572
|
+
|
|
573
|
+
expect(workSpan!.options.startTime).toBeTypeOf('number');
|
|
574
|
+
expect(workSpan!.options.startTime).toBeGreaterThanOrEqual(beforeStart);
|
|
575
|
+
expect(workSpan!.options.startTime).toBeLessThanOrEqual(afterEnd);
|
|
576
|
+
|
|
577
|
+
expect(workSpan!.endTime).toBeTypeOf('number');
|
|
578
|
+
expect(workSpan!.endTime).toBeGreaterThanOrEqual(workSpan!.options.startTime!);
|
|
579
|
+
expect(workSpan!.endTime).toBeLessThanOrEqual(afterEnd);
|
|
580
|
+
});
|
|
581
|
+
});
|