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,388 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { parseConfig, createInitialiser, getActiveConfig, setConfig } from './config';
|
|
3
|
+
import type { EdgeConfig } from '../types';
|
|
4
|
+
import { context as api_context } from '@opentelemetry/api';
|
|
5
|
+
|
|
6
|
+
describe('Config System', () => {
|
|
7
|
+
describe('parseConfig()', () => {
|
|
8
|
+
it('should parse minimal config (only service.name)', () => {
|
|
9
|
+
const config: EdgeConfig = {
|
|
10
|
+
service: { name: 'test-service' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const parsed = parseConfig(config);
|
|
14
|
+
|
|
15
|
+
expect(parsed.service.name).toBe('test-service');
|
|
16
|
+
expect(parsed.service.version).toBeUndefined();
|
|
17
|
+
expect(parsed.service.namespace).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should parse full service config', () => {
|
|
21
|
+
const config: EdgeConfig = {
|
|
22
|
+
service: {
|
|
23
|
+
name: 'test-service',
|
|
24
|
+
version: '1.2.3',
|
|
25
|
+
namespace: 'production',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const parsed = parseConfig(config);
|
|
30
|
+
|
|
31
|
+
expect(parsed.service.name).toBe('test-service');
|
|
32
|
+
expect(parsed.service.version).toBe('1.2.3');
|
|
33
|
+
expect(parsed.service.namespace).toBe('production');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should parse exporter config (URL + headers)', () => {
|
|
37
|
+
const config: EdgeConfig = {
|
|
38
|
+
service: { name: 'test-service' },
|
|
39
|
+
exporter: {
|
|
40
|
+
url: 'https://api.honeycomb.io/v1/traces',
|
|
41
|
+
headers: { 'x-api-key': 'test-key' },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const parsed = parseConfig(config);
|
|
46
|
+
|
|
47
|
+
expect(parsed.spanProcessors).toHaveLength(1);
|
|
48
|
+
expect(parsed.spanProcessors[0]).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should create SpanProcessor from exporter', () => {
|
|
52
|
+
const config: EdgeConfig = {
|
|
53
|
+
service: { name: 'test-service' },
|
|
54
|
+
exporter: {
|
|
55
|
+
url: 'http://localhost:4318/v1/traces',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const parsed = parseConfig(config);
|
|
60
|
+
|
|
61
|
+
expect(parsed.spanProcessors).toBeDefined();
|
|
62
|
+
expect(Array.isArray(parsed.spanProcessors)).toBe(true);
|
|
63
|
+
expect(parsed.spanProcessors.length).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should accept custom SpanProcessor array', () => {
|
|
67
|
+
const mockSpanProcessor = {
|
|
68
|
+
onStart: () => {},
|
|
69
|
+
onEnd: () => {},
|
|
70
|
+
forceFlush: async () => {},
|
|
71
|
+
shutdown: async () => {},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const config: EdgeConfig = {
|
|
75
|
+
service: { name: 'test-service' },
|
|
76
|
+
spanProcessors: [mockSpanProcessor as any],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const parsed = parseConfig(config);
|
|
80
|
+
|
|
81
|
+
expect(parsed.spanProcessors).toHaveLength(1);
|
|
82
|
+
expect(parsed.spanProcessors[0]).toBe(mockSpanProcessor);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should use AlwaysOnSampler as default head sampler', () => {
|
|
86
|
+
const config: EdgeConfig = {
|
|
87
|
+
service: { name: 'test-service' },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const parsed = parseConfig(config);
|
|
91
|
+
|
|
92
|
+
expect(parsed.sampling.headSampler).toBeDefined();
|
|
93
|
+
// Default is ParentBasedSampler with AlwaysOnSampler root
|
|
94
|
+
expect(parsed.sampling.headSampler.toString()).toContain('ParentBased');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should create ParentRatioSampler from ratio config', () => {
|
|
98
|
+
const config: EdgeConfig = {
|
|
99
|
+
service: { name: 'test-service' },
|
|
100
|
+
sampling: {
|
|
101
|
+
headSampler: {
|
|
102
|
+
ratio: 0.5,
|
|
103
|
+
acceptRemote: true,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const parsed = parseConfig(config);
|
|
109
|
+
|
|
110
|
+
expect(parsed.sampling.headSampler).toBeDefined();
|
|
111
|
+
// Should be ParentBasedSampler wrapping ratio sampler
|
|
112
|
+
expect(parsed.sampling.headSampler.toString()).toContain('ParentBased');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should use default tail sampler (keep sampled or errors)', () => {
|
|
116
|
+
const config: EdgeConfig = {
|
|
117
|
+
service: { name: 'test-service' },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const parsed = parseConfig(config);
|
|
121
|
+
|
|
122
|
+
expect(parsed.sampling.tailSampler).toBeDefined();
|
|
123
|
+
expect(typeof parsed.sampling.tailSampler).toBe('function');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should accept custom tail sampler', () => {
|
|
127
|
+
const customTailSampler = () => true;
|
|
128
|
+
|
|
129
|
+
const config: EdgeConfig = {
|
|
130
|
+
service: { name: 'test-service' },
|
|
131
|
+
sampling: {
|
|
132
|
+
tailSampler: customTailSampler,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const parsed = parseConfig(config);
|
|
137
|
+
|
|
138
|
+
expect(parsed.sampling.tailSampler).toBe(customTailSampler);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should use W3CTraceContextPropagator as default', () => {
|
|
142
|
+
const config: EdgeConfig = {
|
|
143
|
+
service: { name: 'test-service' },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const parsed = parseConfig(config);
|
|
147
|
+
|
|
148
|
+
expect(parsed.propagator).toBeDefined();
|
|
149
|
+
expect(parsed.propagator.constructor.name).toBe('W3CTraceContextPropagator');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should accept custom propagator', () => {
|
|
153
|
+
const mockPropagator = {
|
|
154
|
+
inject: () => {},
|
|
155
|
+
extract: () => ({} as any),
|
|
156
|
+
fields: () => [],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const config: EdgeConfig = {
|
|
160
|
+
service: { name: 'test-service' },
|
|
161
|
+
propagator: mockPropagator as any,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const parsed = parseConfig(config);
|
|
165
|
+
|
|
166
|
+
expect(parsed.propagator).toBe(mockPropagator);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should enable global fetch instrumentation by default', () => {
|
|
170
|
+
const config: EdgeConfig = {
|
|
171
|
+
service: { name: 'test-service' },
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const parsed = parseConfig(config);
|
|
175
|
+
|
|
176
|
+
expect(parsed.instrumentation.instrumentGlobalFetch).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should disable global cache instrumentation by default', () => {
|
|
180
|
+
const config: EdgeConfig = {
|
|
181
|
+
service: { name: 'test-service' },
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const parsed = parseConfig(config);
|
|
185
|
+
|
|
186
|
+
expect(parsed.instrumentation.instrumentGlobalCache).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should allow enabling cache instrumentation', () => {
|
|
190
|
+
const config: EdgeConfig = {
|
|
191
|
+
service: { name: 'test-service' },
|
|
192
|
+
instrumentation: {
|
|
193
|
+
instrumentGlobalCache: true,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const parsed = parseConfig(config);
|
|
198
|
+
|
|
199
|
+
expect(parsed.instrumentation.instrumentGlobalCache).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should use default fetch.includeTraceContext = true', () => {
|
|
203
|
+
const config: EdgeConfig = {
|
|
204
|
+
service: { name: 'test-service' },
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const parsed = parseConfig(config);
|
|
208
|
+
|
|
209
|
+
expect(parsed.fetch.includeTraceContext).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should accept custom fetch.includeTraceContext function', () => {
|
|
213
|
+
const customFn = (request: Request) => request.url.includes('internal');
|
|
214
|
+
|
|
215
|
+
const config: EdgeConfig = {
|
|
216
|
+
service: { name: 'test-service' },
|
|
217
|
+
fetch: {
|
|
218
|
+
includeTraceContext: customFn,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const parsed = parseConfig(config);
|
|
223
|
+
|
|
224
|
+
expect(parsed.fetch.includeTraceContext).toBe(customFn);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('createInitialiser()', () => {
|
|
229
|
+
it('should create initialiser from static config', () => {
|
|
230
|
+
const config: EdgeConfig = {
|
|
231
|
+
service: { name: 'test-service' },
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const initialiser = createInitialiser(config);
|
|
235
|
+
|
|
236
|
+
expect(typeof initialiser).toBe('function');
|
|
237
|
+
|
|
238
|
+
const resolved = initialiser({}, { request: null as any });
|
|
239
|
+
|
|
240
|
+
expect(resolved.service.name).toBe('test-service');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should create initialiser from config function', () => {
|
|
244
|
+
const configFn = (env: { SERVICE_NAME: string }) => ({
|
|
245
|
+
service: { name: env.SERVICE_NAME },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const initialiser = createInitialiser(configFn);
|
|
249
|
+
|
|
250
|
+
expect(typeof initialiser).toBe('function');
|
|
251
|
+
|
|
252
|
+
const resolved = initialiser({ SERVICE_NAME: 'dynamic-service' }, { request: null as any });
|
|
253
|
+
|
|
254
|
+
expect(resolved.service.name).toBe('dynamic-service');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should pass env and trigger to config function', () => {
|
|
258
|
+
const configFn = vi.fn((env: any, trigger: any) => ({
|
|
259
|
+
service: { name: 'test' },
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
const initialiser = createInitialiser(configFn);
|
|
263
|
+
|
|
264
|
+
const mockEnv = { API_KEY: 'test-key' };
|
|
265
|
+
const mockTrigger = { request: new Request('http://example.com') };
|
|
266
|
+
|
|
267
|
+
initialiser(mockEnv, mockTrigger);
|
|
268
|
+
|
|
269
|
+
expect(configFn).toHaveBeenCalledWith(mockEnv, mockTrigger);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('getActiveConfig() / setConfig()', () => {
|
|
274
|
+
beforeEach(() => {
|
|
275
|
+
// Reset active config before each test
|
|
276
|
+
setConfig(null as any);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should store and retrieve active config', () => {
|
|
280
|
+
const config: EdgeConfig = {
|
|
281
|
+
service: { name: 'test-service' },
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const parsed = parseConfig(config);
|
|
285
|
+
const ctx = setConfig(parsed);
|
|
286
|
+
|
|
287
|
+
// Use api_context.with() to activate the context
|
|
288
|
+
api_context.with(ctx, () => {
|
|
289
|
+
const active = getActiveConfig();
|
|
290
|
+
expect(active).toBe(parsed);
|
|
291
|
+
expect(active?.service.name).toBe('test-service');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should return null when no active config', () => {
|
|
296
|
+
const ctx = setConfig(null as any);
|
|
297
|
+
|
|
298
|
+
api_context.with(ctx, () => {
|
|
299
|
+
const active = getActiveConfig();
|
|
300
|
+
expect(active).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should allow updating active config', () => {
|
|
305
|
+
const config1 = parseConfig({
|
|
306
|
+
service: { name: 'service-1' },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const ctx1 = setConfig(config1);
|
|
310
|
+
api_context.with(ctx1, () => {
|
|
311
|
+
expect(getActiveConfig()?.service.name).toBe('service-1');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const config2 = parseConfig({
|
|
315
|
+
service: { name: 'service-2' },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const ctx2 = setConfig(config2);
|
|
319
|
+
api_context.with(ctx2, () => {
|
|
320
|
+
expect(getActiveConfig()?.service.name).toBe('service-2');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('Config context isolation', () => {
|
|
326
|
+
it('should isolate config per context using setConfig() return value', () => {
|
|
327
|
+
const config1 = parseConfig({
|
|
328
|
+
service: { name: 'service-1' },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const config2 = parseConfig({
|
|
332
|
+
service: { name: 'service-2' },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Create separate contexts
|
|
336
|
+
const context1 = setConfig(config1);
|
|
337
|
+
const context2 = setConfig(config2);
|
|
338
|
+
|
|
339
|
+
// Verify contexts are different
|
|
340
|
+
expect(context1).not.toBe(context2);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should use OpenTelemetry context for storage', () => {
|
|
344
|
+
const config = parseConfig({
|
|
345
|
+
service: { name: 'test-service' },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const context = setConfig(config);
|
|
349
|
+
|
|
350
|
+
// setConfig should return a context object
|
|
351
|
+
expect(context).toBeDefined();
|
|
352
|
+
expect(typeof context).toBe('object');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should not have race conditions with context-based storage', () => {
|
|
356
|
+
// This test verifies the fix for the config race condition bug
|
|
357
|
+
// where module-level state caused request B to overwrite request A's config
|
|
358
|
+
|
|
359
|
+
const configA = parseConfig({
|
|
360
|
+
service: { name: 'request-a' },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const configB = parseConfig({
|
|
364
|
+
service: { name: 'request-b' },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Simulate setting configs for different requests
|
|
368
|
+
const contextA = setConfig(configA);
|
|
369
|
+
const contextB = setConfig(configB);
|
|
370
|
+
|
|
371
|
+
// Both contexts should exist independently
|
|
372
|
+
expect(contextA).not.toBe(contextB);
|
|
373
|
+
|
|
374
|
+
// Each context has its own config that doesn't interfere with the other
|
|
375
|
+
api_context.with(contextA, () => {
|
|
376
|
+
const activeConfig = getActiveConfig();
|
|
377
|
+
expect(activeConfig?.service.name).toBe('request-a');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
api_context.with(contextB, () => {
|
|
381
|
+
const activeConfig = getActiveConfig();
|
|
382
|
+
expect(activeConfig?.service.name).toBe('request-b');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// This demonstrates that configs are properly isolated per context
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration system for autotel-edge
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { W3CTraceContextPropagator } from '@opentelemetry/core';
|
|
6
|
+
import { ParentBasedSampler, AlwaysOnSampler } from '@opentelemetry/sdk-trace-base';
|
|
7
|
+
import { context as api_context, createContextKey, type Context } from '@opentelemetry/api';
|
|
8
|
+
import type {
|
|
9
|
+
EdgeConfig,
|
|
10
|
+
ResolvedEdgeConfig,
|
|
11
|
+
ConfigurationOption,
|
|
12
|
+
Trigger,
|
|
13
|
+
ParentRatioSamplingConfig,
|
|
14
|
+
} from '../types';
|
|
15
|
+
import { isSpanProcessorConfig } from '../types';
|
|
16
|
+
import { OTLPExporter } from './exporter';
|
|
17
|
+
import { TailSamplingSpanProcessor } from './spanprocessor';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type for config initialization function
|
|
21
|
+
*/
|
|
22
|
+
export type Initialiser = (env: any, trigger: Trigger) => ResolvedEdgeConfig;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Context key for storing config (isolates config per-request)
|
|
26
|
+
*/
|
|
27
|
+
const CONFIG_KEY = createContextKey('autotel-edge-config');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the currently active config from context
|
|
31
|
+
*
|
|
32
|
+
* This reads the config from the active context, ensuring each request
|
|
33
|
+
* has its own isolated config even when multiple requests are in-flight.
|
|
34
|
+
*/
|
|
35
|
+
export function getActiveConfig(): ResolvedEdgeConfig | null {
|
|
36
|
+
const value = api_context.active().getValue(CONFIG_KEY) as
|
|
37
|
+
| ResolvedEdgeConfig
|
|
38
|
+
| null
|
|
39
|
+
| undefined;
|
|
40
|
+
return value ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set the active config in context
|
|
45
|
+
*
|
|
46
|
+
* Returns a new context with the config stored. This context should be
|
|
47
|
+
* used with api_context.with() to ensure the config is isolated per-request.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const config = parseConfig({ service: { name: 'my-service' } });
|
|
52
|
+
* const context = setConfig(config);
|
|
53
|
+
*
|
|
54
|
+
* api_context.with(context, () => {
|
|
55
|
+
* // Config is available here via getActiveConfig()
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function setConfig(config: ResolvedEdgeConfig): Context {
|
|
60
|
+
return api_context.active().setValue(CONFIG_KEY, config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse and validate configuration
|
|
65
|
+
*/
|
|
66
|
+
export function parseConfig(config: EdgeConfig): ResolvedEdgeConfig {
|
|
67
|
+
// Parse head sampler
|
|
68
|
+
const headSampler =
|
|
69
|
+
config.sampling?.headSampler ??
|
|
70
|
+
new ParentBasedSampler({
|
|
71
|
+
root: new AlwaysOnSampler(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const parsedHeadSampler =
|
|
75
|
+
typeof headSampler === 'object' && 'ratio' in headSampler
|
|
76
|
+
? createParentRatioSampler(headSampler)
|
|
77
|
+
: headSampler;
|
|
78
|
+
|
|
79
|
+
// Parse tail sampler (default: keep sampled or error traces)
|
|
80
|
+
const tailSampler =
|
|
81
|
+
config.sampling?.tailSampler ??
|
|
82
|
+
((traceInfo) => {
|
|
83
|
+
const localRootSpan = traceInfo.localRootSpan;
|
|
84
|
+
const ctx = localRootSpan.spanContext();
|
|
85
|
+
// Keep if sampled or if root span has error
|
|
86
|
+
return (ctx.traceFlags & 1) === 1 || localRootSpan.status.code === 2; // SAMPLED flag | ERROR status
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Parse exporter - use TailSamplingSpanProcessor when tail sampler is present
|
|
90
|
+
const spanProcessors = isSpanProcessorConfig(config)
|
|
91
|
+
? Array.isArray(config.spanProcessors)
|
|
92
|
+
? config.spanProcessors
|
|
93
|
+
: [config.spanProcessors]
|
|
94
|
+
: [
|
|
95
|
+
// Use TailSamplingSpanProcessor to enable tail sampling
|
|
96
|
+
new TailSamplingSpanProcessor(
|
|
97
|
+
typeof config.exporter === 'object' && 'url' in config.exporter
|
|
98
|
+
? new OTLPExporter(config.exporter)
|
|
99
|
+
: config.exporter,
|
|
100
|
+
config.postProcessor,
|
|
101
|
+
tailSampler, // Wire up the tail sampler!
|
|
102
|
+
),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// Build resolved config
|
|
106
|
+
const resolved: ResolvedEdgeConfig = {
|
|
107
|
+
service: config.service,
|
|
108
|
+
handlers: {
|
|
109
|
+
fetch: config.handlers?.fetch ?? {},
|
|
110
|
+
},
|
|
111
|
+
fetch: {
|
|
112
|
+
includeTraceContext: config.fetch?.includeTraceContext ?? true,
|
|
113
|
+
},
|
|
114
|
+
postProcessor: config.postProcessor ?? ((spans) => spans),
|
|
115
|
+
sampling: {
|
|
116
|
+
headSampler: parsedHeadSampler,
|
|
117
|
+
tailSampler,
|
|
118
|
+
},
|
|
119
|
+
spanProcessors,
|
|
120
|
+
propagator: config.propagator ?? new W3CTraceContextPropagator(),
|
|
121
|
+
instrumentation: {
|
|
122
|
+
instrumentGlobalFetch: config.instrumentation?.instrumentGlobalFetch ?? true,
|
|
123
|
+
instrumentGlobalCache: config.instrumentation?.instrumentGlobalCache ?? false,
|
|
124
|
+
disabled: config.instrumentation?.disabled ?? false,
|
|
125
|
+
},
|
|
126
|
+
subscribers: config.subscribers ?? [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return resolved;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a parent-based ratio sampler
|
|
134
|
+
*/
|
|
135
|
+
function createParentRatioSampler(config: ParentRatioSamplingConfig) {
|
|
136
|
+
const { ratio, acceptRemote = true } = config;
|
|
137
|
+
|
|
138
|
+
// Simple ratio sampler
|
|
139
|
+
const ratioSampler = {
|
|
140
|
+
shouldSample: () => ({
|
|
141
|
+
decision: Math.random() < ratio ? 1 : 0, // RECORD_AND_SAMPLED : NOT_RECORD
|
|
142
|
+
attributes: {},
|
|
143
|
+
}),
|
|
144
|
+
toString: () => `ParentRatioSampler{ratio=${ratio}}`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (acceptRemote) {
|
|
148
|
+
return new ParentBasedSampler({ root: ratioSampler as any });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return ratioSampler;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a config initializer function
|
|
156
|
+
*/
|
|
157
|
+
export function createInitialiser(config: ConfigurationOption): Initialiser {
|
|
158
|
+
if (typeof config === 'function') {
|
|
159
|
+
return (env, trigger) => {
|
|
160
|
+
const conf = parseConfig(config(env, trigger));
|
|
161
|
+
return conf;
|
|
162
|
+
};
|
|
163
|
+
} else {
|
|
164
|
+
const parsed = parseConfig(config);
|
|
165
|
+
return () => parsed;
|
|
166
|
+
}
|
|
167
|
+
}
|