autotel-edge 3.0.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.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/chunk-F32WSLNX.js +309 -0
- package/dist/chunk-F32WSLNX.js.map +1 -0
- package/dist/events.d.ts +86 -0
- package/dist/events.js +157 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +326 -0
- package/dist/index.js +921 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +89 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/sampling.d.ts +166 -0
- package/dist/sampling.js +108 -0
- package/dist/sampling.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-Dj85cPUj.d.ts +182 -0
- package/package.json +88 -0
- package/src/api/logger.test.ts +367 -0
- package/src/api/logger.ts +197 -0
- package/src/compose.ts +243 -0
- package/src/core/buffer.ts +16 -0
- package/src/core/config.test.ts +388 -0
- package/src/core/config.ts +167 -0
- package/src/core/context.ts +224 -0
- package/src/core/exporter.ts +99 -0
- package/src/core/provider.ts +45 -0
- package/src/core/span.ts +222 -0
- package/src/core/spanprocessor.test.ts +521 -0
- package/src/core/spanprocessor.ts +232 -0
- package/src/core/trace-context.ts +66 -0
- package/src/core/tracer.test.ts +123 -0
- package/src/core/tracer.ts +216 -0
- package/src/events/index.test.ts +242 -0
- package/src/events/index.ts +338 -0
- package/src/events.ts +6 -0
- package/src/functional.test.ts +702 -0
- package/src/functional.ts +846 -0
- package/src/index.ts +81 -0
- package/src/logger.ts +13 -0
- package/src/sampling/index.test.ts +297 -0
- package/src/sampling/index.ts +276 -0
- package/src/sampling.ts +6 -0
- package/src/testing/index.ts +9 -0
- package/src/testing.ts +6 -0
- package/src/types.ts +267 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { SpanProcessorWithFlush, TailSamplingSpanProcessor } from './spanprocessor';
|
|
3
|
+
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
4
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
5
|
+
|
|
6
|
+
describe('SpanProcessorWithFlush', () => {
|
|
7
|
+
let mockExporter: SpanExporter;
|
|
8
|
+
let processor: SpanProcessorWithFlush;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockExporter = {
|
|
12
|
+
export: vi.fn((spans, callback) => {
|
|
13
|
+
callback({ code: 0 }); // SUCCESS
|
|
14
|
+
}),
|
|
15
|
+
shutdown: vi.fn(async () => {}),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
processor = new SpanProcessorWithFlush(mockExporter);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('onEnd()', () => {
|
|
22
|
+
it('should buffer spans by trace ID', () => {
|
|
23
|
+
const span1 = createMockSpan('trace-1', 'span-1');
|
|
24
|
+
const span2 = createMockSpan('trace-1', 'span-2');
|
|
25
|
+
const span3 = createMockSpan('trace-2', 'span-3');
|
|
26
|
+
|
|
27
|
+
processor.onEnd(span1);
|
|
28
|
+
processor.onEnd(span2);
|
|
29
|
+
processor.onEnd(span3);
|
|
30
|
+
|
|
31
|
+
// Spans are buffered, not exported yet
|
|
32
|
+
expect(mockExporter.export).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('forceFlush()', () => {
|
|
37
|
+
it('should flush specific trace by ID', async () => {
|
|
38
|
+
const span1 = createMockSpan('trace-1', 'span-1');
|
|
39
|
+
const span2 = createMockSpan('trace-1', 'span-2');
|
|
40
|
+
const span3 = createMockSpan('trace-2', 'span-3');
|
|
41
|
+
|
|
42
|
+
processor.onEnd(span1);
|
|
43
|
+
processor.onEnd(span2);
|
|
44
|
+
processor.onEnd(span3);
|
|
45
|
+
|
|
46
|
+
await processor.forceFlush('trace-1');
|
|
47
|
+
|
|
48
|
+
expect(mockExporter.export).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(mockExporter.export).toHaveBeenCalledWith(
|
|
50
|
+
expect.arrayContaining([span1, span2]),
|
|
51
|
+
expect.any(Function)
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should flush all traces when no ID provided', async () => {
|
|
56
|
+
const span1 = createMockSpan('trace-1', 'span-1');
|
|
57
|
+
const span2 = createMockSpan('trace-2', 'span-2');
|
|
58
|
+
|
|
59
|
+
processor.onEnd(span1);
|
|
60
|
+
processor.onEnd(span2);
|
|
61
|
+
|
|
62
|
+
await processor.forceFlush();
|
|
63
|
+
|
|
64
|
+
expect(mockExporter.export).toHaveBeenCalledTimes(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should apply post-processor before export', async () => {
|
|
68
|
+
const postProcessor = vi.fn((spans) => {
|
|
69
|
+
// Add custom attribute to all spans
|
|
70
|
+
return spans.map((span) => ({
|
|
71
|
+
...span,
|
|
72
|
+
attributes: { ...span.attributes, 'custom.tag': 'test' },
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
processor = new SpanProcessorWithFlush(mockExporter, postProcessor);
|
|
77
|
+
|
|
78
|
+
const span1 = createMockSpan('trace-1', 'span-1');
|
|
79
|
+
processor.onEnd(span1);
|
|
80
|
+
|
|
81
|
+
await processor.forceFlush('trace-1');
|
|
82
|
+
|
|
83
|
+
expect(postProcessor).toHaveBeenCalledWith([span1]);
|
|
84
|
+
expect(mockExporter.export).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('TailSamplingSpanProcessor', () => {
|
|
90
|
+
let mockExporter: SpanExporter;
|
|
91
|
+
let processor: TailSamplingSpanProcessor;
|
|
92
|
+
let exportedSpans: ReadableSpan[] = [];
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
exportedSpans = [];
|
|
96
|
+
|
|
97
|
+
mockExporter = {
|
|
98
|
+
export: vi.fn((spans, callback) => {
|
|
99
|
+
exportedSpans.push(...spans);
|
|
100
|
+
callback({ code: 0 }); // SUCCESS
|
|
101
|
+
}),
|
|
102
|
+
shutdown: vi.fn(async () => {}),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('Span buffering', () => {
|
|
107
|
+
it('should buffer all spans until root span ends', async () => {
|
|
108
|
+
// Custom tail sampler that always keeps traces
|
|
109
|
+
const tailSampler = vi.fn(() => true);
|
|
110
|
+
|
|
111
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
112
|
+
|
|
113
|
+
// Create a trace: root -> child1 -> child2
|
|
114
|
+
const child2 = createMockSpan('trace-1', 'span-3', 'span-2');
|
|
115
|
+
const child1 = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
116
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
117
|
+
|
|
118
|
+
// End spans in order: child2, child1, root
|
|
119
|
+
processor.onEnd(child2);
|
|
120
|
+
processor.onEnd(child1);
|
|
121
|
+
|
|
122
|
+
// No spans should be exported yet
|
|
123
|
+
expect(mockExporter.export).not.toHaveBeenCalled();
|
|
124
|
+
|
|
125
|
+
// End root span - should trigger export of all buffered spans
|
|
126
|
+
processor.onEnd(root);
|
|
127
|
+
|
|
128
|
+
// Wait for async flush
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
130
|
+
|
|
131
|
+
// Now all spans should be exported
|
|
132
|
+
expect(mockExporter.export).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(exportedSpans).toHaveLength(3);
|
|
134
|
+
expect(exportedSpans).toContain(child2);
|
|
135
|
+
expect(exportedSpans).toContain(child1);
|
|
136
|
+
expect(exportedSpans).toContain(root);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should only make tail sampling decision when root span ends', async () => {
|
|
140
|
+
const tailSampler = vi.fn(() => true);
|
|
141
|
+
|
|
142
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
143
|
+
|
|
144
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
145
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
146
|
+
|
|
147
|
+
processor.onEnd(child);
|
|
148
|
+
|
|
149
|
+
// Tail sampler should NOT be called yet
|
|
150
|
+
expect(tailSampler).not.toHaveBeenCalled();
|
|
151
|
+
|
|
152
|
+
processor.onEnd(root);
|
|
153
|
+
|
|
154
|
+
// Wait for async flush
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
156
|
+
|
|
157
|
+
// Tail sampler should be called exactly once when root ends
|
|
158
|
+
expect(tailSampler).toHaveBeenCalledTimes(1);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('Tail sampling decisions', () => {
|
|
163
|
+
it('should export all buffered spans when tail sampler returns true', async () => {
|
|
164
|
+
const tailSampler = vi.fn(() => true);
|
|
165
|
+
|
|
166
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
167
|
+
|
|
168
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
169
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
170
|
+
|
|
171
|
+
processor.onEnd(child);
|
|
172
|
+
processor.onEnd(root);
|
|
173
|
+
|
|
174
|
+
// Wait for async flush
|
|
175
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
176
|
+
|
|
177
|
+
expect(exportedSpans).toHaveLength(2);
|
|
178
|
+
expect(exportedSpans).toContain(child);
|
|
179
|
+
expect(exportedSpans).toContain(root);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should drop all buffered spans when tail sampler returns false', async () => {
|
|
183
|
+
const tailSampler = vi.fn(() => false);
|
|
184
|
+
|
|
185
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
186
|
+
|
|
187
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
188
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
189
|
+
|
|
190
|
+
processor.onEnd(child);
|
|
191
|
+
processor.onEnd(root);
|
|
192
|
+
|
|
193
|
+
// No spans should be exported
|
|
194
|
+
expect(mockExporter.export).not.toHaveBeenCalled();
|
|
195
|
+
expect(exportedSpans).toHaveLength(0);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should keep trace when root span has error (default behavior)', async () => {
|
|
199
|
+
// Default tail sampler: keep if sampled or error
|
|
200
|
+
const defaultTailSampler = (traceInfo: any) => {
|
|
201
|
+
const localRootSpan = traceInfo.localRootSpan;
|
|
202
|
+
const ctx = localRootSpan.spanContext();
|
|
203
|
+
return (ctx.traceFlags & 1) === 1 || localRootSpan.status.code === 2; // SAMPLED | ERROR
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, defaultTailSampler);
|
|
207
|
+
|
|
208
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
209
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
210
|
+
|
|
211
|
+
// Set root span status to ERROR
|
|
212
|
+
root.status = { code: SpanStatusCode.ERROR };
|
|
213
|
+
|
|
214
|
+
processor.onEnd(child);
|
|
215
|
+
processor.onEnd(root);
|
|
216
|
+
|
|
217
|
+
// Wait for async flush
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
219
|
+
|
|
220
|
+
// Trace should be kept because root has error
|
|
221
|
+
expect(exportedSpans).toHaveLength(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should keep trace when error in child span affects root decision', async () => {
|
|
225
|
+
// Tail sampler that checks if any span in trace has error
|
|
226
|
+
const errorAwareSampler = (traceInfo: any) => {
|
|
227
|
+
return traceInfo.spans.some((span: any) => span.status.code === SpanStatusCode.ERROR);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, errorAwareSampler);
|
|
231
|
+
|
|
232
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
233
|
+
child.status = { code: SpanStatusCode.ERROR }; // Child has error
|
|
234
|
+
|
|
235
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
236
|
+
root.status = { code: SpanStatusCode.OK }; // Root is OK
|
|
237
|
+
|
|
238
|
+
processor.onEnd(child);
|
|
239
|
+
processor.onEnd(root);
|
|
240
|
+
|
|
241
|
+
// Wait for async flush
|
|
242
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
243
|
+
|
|
244
|
+
// Trace should be kept because child has error
|
|
245
|
+
expect(exportedSpans).toHaveLength(2);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('Trace cleanup', () => {
|
|
250
|
+
it('should clean up trace after decision', async () => {
|
|
251
|
+
const tailSampler = vi.fn(() => true);
|
|
252
|
+
|
|
253
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
254
|
+
|
|
255
|
+
const root1 = createMockSpan('trace-1', 'span-1');
|
|
256
|
+
const root2 = createMockSpan('trace-2', 'span-2');
|
|
257
|
+
|
|
258
|
+
processor.onEnd(root1);
|
|
259
|
+
processor.onEnd(root2);
|
|
260
|
+
|
|
261
|
+
// Both traces should have been auto-flushed when root spans ended
|
|
262
|
+
// Wait a tick for async flush to complete
|
|
263
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
264
|
+
|
|
265
|
+
// Both root spans should have been exported
|
|
266
|
+
expect(exportedSpans).toHaveLength(2);
|
|
267
|
+
expect(exportedSpans).toContain(root1);
|
|
268
|
+
expect(exportedSpans).toContain(root2);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('Without tail sampler', () => {
|
|
273
|
+
it('should export all spans when no tail sampler provided', async () => {
|
|
274
|
+
processor = new TailSamplingSpanProcessor(mockExporter);
|
|
275
|
+
|
|
276
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
277
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
278
|
+
|
|
279
|
+
processor.onEnd(child);
|
|
280
|
+
processor.onEnd(root);
|
|
281
|
+
|
|
282
|
+
// Wait for async flush
|
|
283
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
284
|
+
|
|
285
|
+
// All spans should be exported
|
|
286
|
+
expect(exportedSpans).toHaveLength(2);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('Complex trace scenarios', () => {
|
|
291
|
+
it('should handle multiple traces in parallel', async () => {
|
|
292
|
+
const tailSampler = vi.fn((traceInfo) => {
|
|
293
|
+
// Keep trace-1, drop trace-2
|
|
294
|
+
return traceInfo.traceId === 'trace-1';
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
298
|
+
|
|
299
|
+
// Trace 1
|
|
300
|
+
const trace1_child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
301
|
+
const trace1_root = createMockSpan('trace-1', 'span-1');
|
|
302
|
+
|
|
303
|
+
// Trace 2
|
|
304
|
+
const trace2_child = createMockSpan('trace-2', 'span-4', 'span-3');
|
|
305
|
+
const trace2_root = createMockSpan('trace-2', 'span-3');
|
|
306
|
+
|
|
307
|
+
// Interleave span endings
|
|
308
|
+
processor.onEnd(trace1_child);
|
|
309
|
+
processor.onEnd(trace2_child);
|
|
310
|
+
processor.onEnd(trace1_root);
|
|
311
|
+
processor.onEnd(trace2_root);
|
|
312
|
+
|
|
313
|
+
// Wait for async flush
|
|
314
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
315
|
+
|
|
316
|
+
// Only trace-1 spans should be exported
|
|
317
|
+
expect(exportedSpans).toHaveLength(2);
|
|
318
|
+
expect(exportedSpans).toContain(trace1_child);
|
|
319
|
+
expect(exportedSpans).toContain(trace1_root);
|
|
320
|
+
expect(exportedSpans).not.toContain(trace2_child);
|
|
321
|
+
expect(exportedSpans).not.toContain(trace2_root);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should handle deeply nested spans', async () => {
|
|
325
|
+
const tailSampler = vi.fn(() => true);
|
|
326
|
+
|
|
327
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
328
|
+
|
|
329
|
+
// Create a deep trace: root -> child1 -> child2 -> child3
|
|
330
|
+
const child3 = createMockSpan('trace-1', 'span-4', 'span-3');
|
|
331
|
+
const child2 = createMockSpan('trace-1', 'span-3', 'span-2');
|
|
332
|
+
const child1 = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
333
|
+
const root = createMockSpan('trace-1', 'span-1');
|
|
334
|
+
|
|
335
|
+
processor.onEnd(child3);
|
|
336
|
+
processor.onEnd(child2);
|
|
337
|
+
processor.onEnd(child1);
|
|
338
|
+
processor.onEnd(root);
|
|
339
|
+
|
|
340
|
+
// Wait for async flush
|
|
341
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
342
|
+
|
|
343
|
+
// All spans should be exported
|
|
344
|
+
expect(exportedSpans).toHaveLength(4);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should handle distributed traces (local root with remote parent)', async () => {
|
|
348
|
+
const tailSampler = vi.fn(() => true);
|
|
349
|
+
|
|
350
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
351
|
+
|
|
352
|
+
// Create a distributed trace:
|
|
353
|
+
// - remote-root (not in this trace, represented by parentSpanId 'remote-1')
|
|
354
|
+
// -> local-root (has parentSpanId but is first span we see)
|
|
355
|
+
// -> child1
|
|
356
|
+
// -> child2
|
|
357
|
+
|
|
358
|
+
const child2 = createMockSpan('trace-1', 'span-3', 'span-2');
|
|
359
|
+
const child1 = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
360
|
+
// Local root has a parentSpanId (from remote) but is our local root
|
|
361
|
+
const localRoot = createMockSpan('trace-1', 'span-1', 'remote-1');
|
|
362
|
+
|
|
363
|
+
// End spans in order
|
|
364
|
+
processor.onEnd(child2);
|
|
365
|
+
processor.onEnd(child1);
|
|
366
|
+
processor.onEnd(localRoot);
|
|
367
|
+
|
|
368
|
+
// Distributed traces don't auto-flush (local root has parentSpanId)
|
|
369
|
+
// Explicitly flush to trigger tail sampling decision (simulates instrument.ts behavior)
|
|
370
|
+
await processor.forceFlush('trace-1');
|
|
371
|
+
|
|
372
|
+
// Wait for async flush
|
|
373
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
374
|
+
|
|
375
|
+
// ALL spans should be exported (including distributed trace entry point)
|
|
376
|
+
expect(exportedSpans).toHaveLength(3);
|
|
377
|
+
expect(exportedSpans).toContain(child2);
|
|
378
|
+
expect(exportedSpans).toContain(child1);
|
|
379
|
+
expect(exportedSpans).toContain(localRoot);
|
|
380
|
+
expect(tailSampler).toHaveBeenCalledTimes(1);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should NOT leak traces when distributed trace root ends', async () => {
|
|
384
|
+
const tailSampler = vi.fn(() => false); // Drop all traces
|
|
385
|
+
|
|
386
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
387
|
+
|
|
388
|
+
// Distributed trace
|
|
389
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
390
|
+
const localRoot = createMockSpan('trace-1', 'span-1', 'remote-1');
|
|
391
|
+
|
|
392
|
+
processor.onEnd(child);
|
|
393
|
+
processor.onEnd(localRoot);
|
|
394
|
+
|
|
395
|
+
// Explicitly flush (simulates instrument.ts behavior)
|
|
396
|
+
await processor.forceFlush('trace-1');
|
|
397
|
+
|
|
398
|
+
// No spans should be exported (tail sampler returned false)
|
|
399
|
+
expect(exportedSpans).toHaveLength(0);
|
|
400
|
+
expect(tailSampler).toHaveBeenCalledTimes(1);
|
|
401
|
+
|
|
402
|
+
// Trace should be cleaned up (not leaked)
|
|
403
|
+
// Start a new trace with same traceId to verify cleanup
|
|
404
|
+
const anotherSpan = createMockSpan('trace-1', 'span-3');
|
|
405
|
+
processor.onEnd(anotherSpan);
|
|
406
|
+
|
|
407
|
+
// Explicitly flush the new trace
|
|
408
|
+
await processor.forceFlush('trace-1');
|
|
409
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
410
|
+
|
|
411
|
+
// Tail sampler should be called again for the new trace
|
|
412
|
+
expect(tailSampler).toHaveBeenCalledTimes(2);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should handle distributed trace where ALL spans have remote parent', async () => {
|
|
416
|
+
// This is the critical test: when all spans have parentSpanId,
|
|
417
|
+
// localRootSpan must be correctly identified as the handler (span with remote parent)
|
|
418
|
+
// not as a child that happened to end first
|
|
419
|
+
const tailSampler = vi.fn((traceInfo) => {
|
|
420
|
+
// Tail sampler accesses localRootSpan.spanContext()
|
|
421
|
+
// This would crash if localRootSpan is undefined
|
|
422
|
+
expect(traceInfo.localRootSpan).toBeDefined();
|
|
423
|
+
expect(traceInfo.localRootSpan.spanContext()).toBeDefined();
|
|
424
|
+
return true;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, tailSampler);
|
|
428
|
+
|
|
429
|
+
// All spans have parentSpanId (span1 has remote parent, others have local parent)
|
|
430
|
+
// Spans end in this order: child2 -> child1 -> handler
|
|
431
|
+
const span3 = createMockSpan('trace-1', 'span-3', 'span-2'); // grandchild
|
|
432
|
+
const span2 = createMockSpan('trace-1', 'span-2', 'span-1'); // child
|
|
433
|
+
const span1 = createMockSpan('trace-1', 'span-1', 'remote-parent-id'); // handler (local root)
|
|
434
|
+
|
|
435
|
+
processor.onEnd(span3); // Ends first, but has local parent (span-1)
|
|
436
|
+
processor.onEnd(span2); // Ends second, but has local parent (span-1)
|
|
437
|
+
processor.onEnd(span1); // Ends last, has remote parent → this is local root
|
|
438
|
+
|
|
439
|
+
// Explicitly flush (simulates instrument.ts behavior)
|
|
440
|
+
await processor.forceFlush('trace-1');
|
|
441
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
442
|
+
|
|
443
|
+
// Tail sampler should have been called without crashing
|
|
444
|
+
expect(tailSampler).toHaveBeenCalledTimes(1);
|
|
445
|
+
|
|
446
|
+
// localRootSpan should be span-1 (handler with remote parent), NOT span-3 (first to end)
|
|
447
|
+
const traceInfo = tailSampler.mock.calls[0][0];
|
|
448
|
+
expect(traceInfo.localRootSpan.spanContext().spanId).toBe('span-1');
|
|
449
|
+
|
|
450
|
+
// All spans should be exported
|
|
451
|
+
expect(exportedSpans).toHaveLength(3);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should check handler error status, not child status, in distributed trace', async () => {
|
|
455
|
+
// Critical scenario: handler has error, child is OK
|
|
456
|
+
// Tail sampler must check localRootSpan (handler), not first-to-end child
|
|
457
|
+
const defaultTailSampler = (traceInfo: any) => {
|
|
458
|
+
const localRootSpan = traceInfo.localRootSpan;
|
|
459
|
+
const ctx = localRootSpan.spanContext();
|
|
460
|
+
// Default: keep if sampled or error
|
|
461
|
+
return (ctx.traceFlags & 1) === 1 || localRootSpan.status.code === 2; // SAMPLED | ERROR
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
processor = new TailSamplingSpanProcessor(mockExporter, undefined, defaultTailSampler);
|
|
465
|
+
|
|
466
|
+
// Child ends first with OK status
|
|
467
|
+
const child = createMockSpan('trace-1', 'span-2', 'span-1');
|
|
468
|
+
child.status = { code: SpanStatusCode.OK }; // Child is fine
|
|
469
|
+
|
|
470
|
+
// Handler ends second with ERROR status and remote parent
|
|
471
|
+
const handler = createMockSpan('trace-1', 'span-1', 'remote-parent-id');
|
|
472
|
+
handler.status = { code: SpanStatusCode.ERROR }; // Handler has error
|
|
473
|
+
|
|
474
|
+
processor.onEnd(child); // Child ends first (OK status)
|
|
475
|
+
processor.onEnd(handler); // Handler ends second (ERROR status, remote parent)
|
|
476
|
+
|
|
477
|
+
// Explicitly flush
|
|
478
|
+
await processor.forceFlush('trace-1');
|
|
479
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
480
|
+
|
|
481
|
+
// Trace should be KEPT because handler (localRootSpan) has ERROR
|
|
482
|
+
// If we incorrectly used child as localRootSpan, trace would be dropped (OK status)
|
|
483
|
+
expect(exportedSpans).toHaveLength(2);
|
|
484
|
+
expect(exportedSpans).toContain(child);
|
|
485
|
+
expect(exportedSpans).toContain(handler);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Helper to create mock ReadableSpan
|
|
492
|
+
*/
|
|
493
|
+
function createMockSpan(
|
|
494
|
+
traceId: string,
|
|
495
|
+
spanId: string,
|
|
496
|
+
parentSpanId?: string
|
|
497
|
+
): ReadableSpan {
|
|
498
|
+
return {
|
|
499
|
+
name: `span-${spanId}`,
|
|
500
|
+
spanContext: () => ({
|
|
501
|
+
traceId,
|
|
502
|
+
spanId,
|
|
503
|
+
traceFlags: 1, // SAMPLED
|
|
504
|
+
traceState: undefined,
|
|
505
|
+
}),
|
|
506
|
+
parentSpanId,
|
|
507
|
+
startTime: [Date.now(), 0],
|
|
508
|
+
endTime: [Date.now(), 0],
|
|
509
|
+
status: { code: SpanStatusCode.OK },
|
|
510
|
+
attributes: {},
|
|
511
|
+
links: [],
|
|
512
|
+
events: [],
|
|
513
|
+
duration: [0, 0],
|
|
514
|
+
ended: true,
|
|
515
|
+
resource: {} as any,
|
|
516
|
+
instrumentationLibrary: { name: 'test', version: '1.0.0' },
|
|
517
|
+
droppedAttributesCount: 0,
|
|
518
|
+
droppedEventsCount: 0,
|
|
519
|
+
droppedLinksCount: 0,
|
|
520
|
+
} as ReadableSpan;
|
|
521
|
+
}
|