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
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autotel-edge
|
|
3
|
+
*
|
|
4
|
+
* Vendor-agnostic OpenTelemetry for edge runtimes
|
|
5
|
+
* Foundation for Cloudflare Workers, Vercel Edge, Netlify Edge, Deno Deploy
|
|
6
|
+
*
|
|
7
|
+
* Bundle size: ~20KB minified (~8KB gzipped)
|
|
8
|
+
*
|
|
9
|
+
* @example Quick Start
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { trace, init } from 'autotel-edge'
|
|
12
|
+
*
|
|
13
|
+
* init({
|
|
14
|
+
* service: { name: 'my-edge-function' },
|
|
15
|
+
* exporter: { url: process.env.OTEL_ENDPOINT }
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* export const handler = trace(async (request: Request) => {
|
|
19
|
+
* return new Response('Hello World')
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Core exports
|
|
25
|
+
export { SpanImpl } from './core/span';
|
|
26
|
+
export { WorkerTracer, withNextSpan } from './core/tracer';
|
|
27
|
+
export { OTLPExporter } from './core/exporter';
|
|
28
|
+
export { AsyncLocalStorageContextManager } from './core/context';
|
|
29
|
+
export { WorkerTracerProvider } from './core/provider';
|
|
30
|
+
export { Buffer } from './core/buffer';
|
|
31
|
+
export {
|
|
32
|
+
parseConfig,
|
|
33
|
+
createInitialiser,
|
|
34
|
+
getActiveConfig,
|
|
35
|
+
setConfig,
|
|
36
|
+
type Initialiser,
|
|
37
|
+
} from './core/config';
|
|
38
|
+
|
|
39
|
+
// Functional API (PRIMARY - zero-boilerplate tracing)
|
|
40
|
+
export {
|
|
41
|
+
trace,
|
|
42
|
+
withTracing,
|
|
43
|
+
instrument as instrumentFunctions,
|
|
44
|
+
span,
|
|
45
|
+
type traceOptions,
|
|
46
|
+
type TraceContext,
|
|
47
|
+
type InstrumentOptions,
|
|
48
|
+
} from './functional';
|
|
49
|
+
|
|
50
|
+
// Types
|
|
51
|
+
export type {
|
|
52
|
+
Trigger,
|
|
53
|
+
EdgeConfig,
|
|
54
|
+
ResolvedEdgeConfig,
|
|
55
|
+
ServiceConfig,
|
|
56
|
+
OTLPExporterConfig,
|
|
57
|
+
ExporterConfig,
|
|
58
|
+
SamplingConfig,
|
|
59
|
+
InstrumentationOptions,
|
|
60
|
+
ResolveConfigFn,
|
|
61
|
+
ConfigurationOption,
|
|
62
|
+
PostProcessorFn,
|
|
63
|
+
TailSampleFn,
|
|
64
|
+
LocalTrace,
|
|
65
|
+
TraceFlushableSpanProcessor,
|
|
66
|
+
InitialSpanInfo,
|
|
67
|
+
HandlerInstrumentation,
|
|
68
|
+
EdgeSubscriber,
|
|
69
|
+
ReadableSpan,
|
|
70
|
+
} from './types';
|
|
71
|
+
|
|
72
|
+
// Re-export OpenTelemetry APIs for convenience
|
|
73
|
+
export { context, propagation } from '@opentelemetry/api';
|
|
74
|
+
|
|
75
|
+
// Re-export common OpenTelemetry types
|
|
76
|
+
export type {
|
|
77
|
+
Span,
|
|
78
|
+
SpanContext,
|
|
79
|
+
Tracer,
|
|
80
|
+
Context,
|
|
81
|
+
} from '@opentelemetry/api';
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency logger for edge runtimes with dynamic log level control
|
|
3
|
+
* Entry point: autotel-edge/logger
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
createEdgeLogger,
|
|
8
|
+
getEdgeTraceContext,
|
|
9
|
+
runWithLogLevel,
|
|
10
|
+
getActiveLogLevel,
|
|
11
|
+
type EdgeLogger,
|
|
12
|
+
type LogLevel,
|
|
13
|
+
} from './api/logger';
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createAdaptiveTailSampler,
|
|
4
|
+
createRandomTailSampler,
|
|
5
|
+
createErrorOnlyTailSampler,
|
|
6
|
+
createSlowOnlyTailSampler,
|
|
7
|
+
combineTailSamplers,
|
|
8
|
+
SamplingPresets,
|
|
9
|
+
type LocalTrace,
|
|
10
|
+
} from './index';
|
|
11
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
12
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
13
|
+
|
|
14
|
+
// Helper to create a mock span
|
|
15
|
+
function createMockSpan(overrides: Partial<ReadableSpan> = {}): ReadableSpan {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
return {
|
|
18
|
+
name: 'test-span',
|
|
19
|
+
spanContext: () => ({
|
|
20
|
+
traceId: 'test-trace-id',
|
|
21
|
+
spanId: 'test-span-id',
|
|
22
|
+
traceFlags: 1,
|
|
23
|
+
}),
|
|
24
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
25
|
+
endTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
26
|
+
status: { code: SpanStatusCode.UNSET },
|
|
27
|
+
attributes: {},
|
|
28
|
+
links: [],
|
|
29
|
+
events: [],
|
|
30
|
+
duration: [0, 0],
|
|
31
|
+
ended: true,
|
|
32
|
+
resource: {} as any,
|
|
33
|
+
instrumentationLibrary: { name: 'test', version: '1.0.0' },
|
|
34
|
+
droppedAttributesCount: 0,
|
|
35
|
+
droppedEventsCount: 0,
|
|
36
|
+
droppedLinksCount: 0,
|
|
37
|
+
kind: 0,
|
|
38
|
+
parentSpanId: undefined,
|
|
39
|
+
...overrides,
|
|
40
|
+
} as ReadableSpan;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper to create a LocalTrace
|
|
44
|
+
function createLocalTrace(
|
|
45
|
+
traceId: string,
|
|
46
|
+
spanOverrides: Partial<ReadableSpan> = {},
|
|
47
|
+
): LocalTrace {
|
|
48
|
+
return {
|
|
49
|
+
traceId,
|
|
50
|
+
localRootSpan: createMockSpan(spanOverrides),
|
|
51
|
+
spans: [createMockSpan(spanOverrides)],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('Sampling Strategies', () => {
|
|
56
|
+
describe('createAdaptiveTailSampler', () => {
|
|
57
|
+
it('should sample based on baseline rate for normal requests', () => {
|
|
58
|
+
const sampler = createAdaptiveTailSampler({ baselineSampleRate: 0.5 });
|
|
59
|
+
|
|
60
|
+
// Create multiple traces and check sampling distribution
|
|
61
|
+
const traces = Array.from({ length: 100 }, (_, i) =>
|
|
62
|
+
createLocalTrace(`trace-${i}`, { status: { code: SpanStatusCode.UNSET } }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const sampled = traces.filter((trace) => sampler(trace));
|
|
66
|
+
|
|
67
|
+
// Should be roughly 50% sampled (allow some variance)
|
|
68
|
+
expect(sampled.length).toBeGreaterThan(30);
|
|
69
|
+
expect(sampled.length).toBeLessThan(70);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should always sample errors when alwaysSampleErrors is true', () => {
|
|
73
|
+
const sampler = createAdaptiveTailSampler({
|
|
74
|
+
baselineSampleRate: 0,
|
|
75
|
+
alwaysSampleErrors: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const errorTrace = createLocalTrace('error-trace', {
|
|
79
|
+
status: { code: SpanStatusCode.ERROR, message: 'Test error' },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(sampler(errorTrace)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should not sample errors when alwaysSampleErrors is false', () => {
|
|
86
|
+
const sampler = createAdaptiveTailSampler({
|
|
87
|
+
baselineSampleRate: 0,
|
|
88
|
+
alwaysSampleErrors: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const errorTrace = createLocalTrace('error-trace', {
|
|
92
|
+
status: { code: SpanStatusCode.ERROR, message: 'Test error' },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(sampler(errorTrace)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should always sample slow requests when alwaysSampleSlow is true', () => {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const sampler = createAdaptiveTailSampler({
|
|
101
|
+
baselineSampleRate: 0,
|
|
102
|
+
slowThresholdMs: 1000,
|
|
103
|
+
alwaysSampleSlow: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const slowTrace = createLocalTrace('slow-trace', {
|
|
107
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
108
|
+
endTime: [Math.floor((now + 2000) / 1000), ((now + 2000) % 1000) * 1_000_000],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(sampler(slowTrace)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should not sample slow requests when alwaysSampleSlow is false', () => {
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const sampler = createAdaptiveTailSampler({
|
|
117
|
+
baselineSampleRate: 0,
|
|
118
|
+
slowThresholdMs: 1000,
|
|
119
|
+
alwaysSampleSlow: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const slowTrace = createLocalTrace('slow-trace', {
|
|
123
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
124
|
+
endTime: [Math.floor((now + 2000) / 1000), ((now + 2000) % 1000) * 1_000_000],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(sampler(slowTrace)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should maintain consistent baseline decision for same trace', () => {
|
|
131
|
+
const sampler = createAdaptiveTailSampler({ baselineSampleRate: 0.5 });
|
|
132
|
+
|
|
133
|
+
const trace1 = createLocalTrace('consistent-trace');
|
|
134
|
+
const decision1 = sampler(trace1);
|
|
135
|
+
|
|
136
|
+
// Create new trace with same ID
|
|
137
|
+
const trace2 = createLocalTrace('consistent-trace');
|
|
138
|
+
const decision2 = sampler(trace2);
|
|
139
|
+
|
|
140
|
+
expect(decision1).toBe(decision2);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('createRandomTailSampler', () => {
|
|
145
|
+
it('should sample at specified rate', () => {
|
|
146
|
+
const sampler = createRandomTailSampler(0.5);
|
|
147
|
+
|
|
148
|
+
const traces = Array.from({ length: 100 }, (_, i) =>
|
|
149
|
+
createLocalTrace(`trace-${i}`),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const sampled = traces.filter((trace) => sampler(trace));
|
|
153
|
+
|
|
154
|
+
// Should be roughly 50% sampled
|
|
155
|
+
expect(sampled.length).toBeGreaterThan(30);
|
|
156
|
+
expect(sampled.length).toBeLessThan(70);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should never sample when rate is 0', () => {
|
|
160
|
+
const sampler = createRandomTailSampler(0);
|
|
161
|
+
|
|
162
|
+
const traces = Array.from({ length: 100 }, (_, i) =>
|
|
163
|
+
createLocalTrace(`trace-${i}`),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const sampled = traces.filter((trace) => sampler(trace));
|
|
167
|
+
|
|
168
|
+
expect(sampled.length).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should always sample when rate is 1', () => {
|
|
172
|
+
const sampler = createRandomTailSampler(1);
|
|
173
|
+
|
|
174
|
+
const traces = Array.from({ length: 100 }, (_, i) =>
|
|
175
|
+
createLocalTrace(`trace-${i}`),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const sampled = traces.filter((trace) => sampler(trace));
|
|
179
|
+
|
|
180
|
+
expect(sampled.length).toBe(100);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('createErrorOnlyTailSampler', () => {
|
|
185
|
+
it('should only sample errors', () => {
|
|
186
|
+
const sampler = createErrorOnlyTailSampler();
|
|
187
|
+
|
|
188
|
+
const errorTrace = createLocalTrace('error-trace', {
|
|
189
|
+
status: { code: SpanStatusCode.ERROR },
|
|
190
|
+
});
|
|
191
|
+
const okTrace = createLocalTrace('ok-trace', {
|
|
192
|
+
status: { code: SpanStatusCode.OK },
|
|
193
|
+
});
|
|
194
|
+
const unsetTrace = createLocalTrace('unset-trace', {
|
|
195
|
+
status: { code: SpanStatusCode.UNSET },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(sampler(errorTrace)).toBe(true);
|
|
199
|
+
expect(sampler(okTrace)).toBe(false);
|
|
200
|
+
expect(sampler(unsetTrace)).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('createSlowOnlyTailSampler', () => {
|
|
205
|
+
it('should only sample slow requests', () => {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
const sampler = createSlowOnlyTailSampler(1000);
|
|
208
|
+
|
|
209
|
+
const fastTrace = createLocalTrace('fast-trace', {
|
|
210
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
211
|
+
endTime: [Math.floor((now + 500) / 1000), ((now + 500) % 1000) * 1_000_000],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const slowTrace = createLocalTrace('slow-trace', {
|
|
215
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
216
|
+
endTime: [Math.floor((now + 2000) / 1000), ((now + 2000) % 1000) * 1_000_000],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(sampler(fastTrace)).toBe(false);
|
|
220
|
+
expect(sampler(slowTrace)).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('combineTailSamplers', () => {
|
|
225
|
+
it('should sample if any sampler returns true (OR logic)', () => {
|
|
226
|
+
const errorSampler = createErrorOnlyTailSampler();
|
|
227
|
+
const slowSampler = createSlowOnlyTailSampler(1000);
|
|
228
|
+
const combined = combineTailSamplers(errorSampler, slowSampler);
|
|
229
|
+
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
|
|
232
|
+
// Error but fast - should sample
|
|
233
|
+
const errorTrace = createLocalTrace('error-trace', {
|
|
234
|
+
status: { code: SpanStatusCode.ERROR },
|
|
235
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
236
|
+
endTime: [Math.floor((now + 500) / 1000), ((now + 500) % 1000) * 1_000_000],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// OK but slow - should sample
|
|
240
|
+
const slowTrace = createLocalTrace('slow-trace', {
|
|
241
|
+
status: { code: SpanStatusCode.OK },
|
|
242
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
243
|
+
endTime: [Math.floor((now + 2000) / 1000), ((now + 2000) % 1000) * 1_000_000],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// OK and fast - should not sample
|
|
247
|
+
const normalTrace = createLocalTrace('normal-trace', {
|
|
248
|
+
status: { code: SpanStatusCode.OK },
|
|
249
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
250
|
+
endTime: [Math.floor((now + 500) / 1000), ((now + 500) % 1000) * 1_000_000],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(combined(errorTrace)).toBe(true);
|
|
254
|
+
expect(combined(slowTrace)).toBe(true);
|
|
255
|
+
expect(combined(normalTrace)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('SamplingPresets', () => {
|
|
260
|
+
it('should have production preset', () => {
|
|
261
|
+
const sampler = SamplingPresets.production();
|
|
262
|
+
expect(typeof sampler).toBe('function');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should have highTraffic preset', () => {
|
|
266
|
+
const sampler = SamplingPresets.highTraffic();
|
|
267
|
+
expect(typeof sampler).toBe('function');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should have debugging preset', () => {
|
|
271
|
+
const sampler = SamplingPresets.debugging();
|
|
272
|
+
expect(typeof sampler).toBe('function');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should have development preset', () => {
|
|
276
|
+
const sampler = SamplingPresets.development();
|
|
277
|
+
expect(typeof sampler).toBe('function');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('production preset should capture errors and slow requests', () => {
|
|
281
|
+
const sampler = SamplingPresets.production();
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
|
|
284
|
+
const errorTrace = createLocalTrace('error-trace', {
|
|
285
|
+
status: { code: SpanStatusCode.ERROR },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const slowTrace = createLocalTrace('slow-trace', {
|
|
289
|
+
startTime: [Math.floor(now / 1000), (now % 1000) * 1_000_000],
|
|
290
|
+
endTime: [Math.floor((now + 2000) / 1000), ((now + 2000) % 1000) * 1_000_000],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(sampler(errorTrace)).toBe(true);
|
|
294
|
+
expect(sampler(slowTrace)).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sampling strategies for autotel-edge
|
|
3
|
+
*
|
|
4
|
+
* Provides intelligent sampling to reduce telemetry costs while capturing critical data.
|
|
5
|
+
*
|
|
6
|
+
* Key strategies:
|
|
7
|
+
* - Always trace errors and slow requests (critical for debugging)
|
|
8
|
+
* - Adaptive sampling based on load
|
|
9
|
+
* - Baseline random sampling for normal traffic
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createAdaptiveTailSampler } from 'autotel-edge/sampling'
|
|
14
|
+
*
|
|
15
|
+
* export default instrument(handler, {
|
|
16
|
+
* sampling: {
|
|
17
|
+
* tailSampler: createAdaptiveTailSampler({
|
|
18
|
+
* baselineSampleRate: 0.1, // 10% of normal requests
|
|
19
|
+
* slowThresholdMs: 1000, // Requests > 1s are "slow"
|
|
20
|
+
* })
|
|
21
|
+
* }
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { TailSampleFn, LocalTrace } from '../types';
|
|
27
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
28
|
+
|
|
29
|
+
export interface AdaptiveSamplerOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Baseline sample rate for normal (successful, fast) requests
|
|
32
|
+
* @default 0.1 (10%)
|
|
33
|
+
*/
|
|
34
|
+
baselineSampleRate?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Threshold in milliseconds for "slow" requests
|
|
38
|
+
* Requests taking longer than this will always be trace
|
|
39
|
+
* @default 1000ms
|
|
40
|
+
*/
|
|
41
|
+
slowThresholdMs?: number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Always trace error spans
|
|
45
|
+
* @default true
|
|
46
|
+
*/
|
|
47
|
+
alwaysSampleErrors?: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Always trace slow spans
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
alwaysSampleSlow?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create an adaptive tail sampler
|
|
58
|
+
*
|
|
59
|
+
* This sampler ensures you never miss critical issues while keeping costs down:
|
|
60
|
+
* - Always traces errors (status code = ERROR)
|
|
61
|
+
* - Always traces slow requests (duration >= slowThresholdMs)
|
|
62
|
+
* - Uses baseline sample rate for successful fast requests
|
|
63
|
+
*
|
|
64
|
+
* **Recommended for production use.**
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const tailSampler = createAdaptiveTailSampler({
|
|
69
|
+
* baselineSampleRate: 0.1, // 10% of normal requests
|
|
70
|
+
* slowThresholdMs: 1000, // Requests > 1s are "slow"
|
|
71
|
+
* alwaysSampleErrors: true, // Always trace errors
|
|
72
|
+
* alwaysSampleSlow: true // Always trace slow requests
|
|
73
|
+
* })
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function createAdaptiveTailSampler(
|
|
77
|
+
options: AdaptiveSamplerOptions = {},
|
|
78
|
+
): TailSampleFn {
|
|
79
|
+
const baselineSampleRate = options.baselineSampleRate ?? 0.1;
|
|
80
|
+
const slowThresholdMs = options.slowThresholdMs ?? 1000;
|
|
81
|
+
const alwaysSampleErrors = options.alwaysSampleErrors ?? true;
|
|
82
|
+
const alwaysSampleSlow = options.alwaysSampleSlow ?? true;
|
|
83
|
+
|
|
84
|
+
if (baselineSampleRate < 0 || baselineSampleRate > 1) {
|
|
85
|
+
throw new Error('Baseline sample rate must be between 0 and 1');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Store baseline decisions using trace ID
|
|
89
|
+
const baselineDecisions = new Map<string, boolean>();
|
|
90
|
+
|
|
91
|
+
return (traceInfo: LocalTrace): boolean => {
|
|
92
|
+
const { traceId, localRootSpan } = traceInfo;
|
|
93
|
+
|
|
94
|
+
// Get or create baseline decision for this trace
|
|
95
|
+
if (!baselineDecisions.has(traceId)) {
|
|
96
|
+
baselineDecisions.set(traceId, Math.random() < baselineSampleRate);
|
|
97
|
+
}
|
|
98
|
+
const baselineDecision = baselineDecisions.get(traceId)!;
|
|
99
|
+
|
|
100
|
+
// Always keep errors (SpanStatusCode.ERROR = 2)
|
|
101
|
+
if (alwaysSampleErrors && localRootSpan.status.code === 2) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Always keep slow requests
|
|
106
|
+
if (alwaysSampleSlow) {
|
|
107
|
+
const duration = getDurationMs(localRootSpan);
|
|
108
|
+
if (duration >= slowThresholdMs) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Otherwise, use baseline decision
|
|
114
|
+
return baselineDecision;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create a simple random tail sampler
|
|
120
|
+
*
|
|
121
|
+
* Samples a fixed percentage of all traces regardless of outcome.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const tailSampler = createRandomTailSampler(0.1) // 10% of all requests
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function createRandomTailSampler(sampleRate: number): TailSampleFn {
|
|
129
|
+
if (sampleRate < 0 || sampleRate > 1) {
|
|
130
|
+
throw new Error('Sample rate must be between 0 and 1');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const decisions = new Map<string, boolean>();
|
|
134
|
+
|
|
135
|
+
return (traceInfo: LocalTrace): boolean => {
|
|
136
|
+
const { traceId } = traceInfo;
|
|
137
|
+
|
|
138
|
+
if (!decisions.has(traceId)) {
|
|
139
|
+
decisions.set(traceId, Math.random() < sampleRate);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return decisions.get(traceId)!;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a tail sampler that keeps all errors
|
|
148
|
+
*
|
|
149
|
+
* Useful for debugging - captures all failures while dropping successful requests.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const tailSampler = createErrorOnlyTailSampler()
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function createErrorOnlyTailSampler(): TailSampleFn {
|
|
157
|
+
return (traceInfo: LocalTrace): boolean => {
|
|
158
|
+
// SpanStatusCode.ERROR = 2
|
|
159
|
+
return traceInfo.localRootSpan.status.code === 2;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a tail sampler that keeps slow requests
|
|
165
|
+
*
|
|
166
|
+
* Useful for performance debugging - captures slow requests while dropping fast ones.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const tailSampler = createSlowOnlyTailSampler(1000) // Keep requests > 1s
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function createSlowOnlyTailSampler(thresholdMs: number): TailSampleFn {
|
|
174
|
+
return (traceInfo: LocalTrace): boolean => {
|
|
175
|
+
const duration = getDurationMs(traceInfo.localRootSpan);
|
|
176
|
+
return duration >= thresholdMs;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Combine multiple tail samplers with OR logic
|
|
182
|
+
*
|
|
183
|
+
* Keeps a trace if ANY sampler returns true.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const tailSampler = combineTailSamplers(
|
|
188
|
+
* createErrorOnlyTailSampler(),
|
|
189
|
+
* createSlowOnlyTailSampler(1000),
|
|
190
|
+
* createRandomTailSampler(0.01) // 1% baseline
|
|
191
|
+
* )
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function combineTailSamplers(...samplers: TailSampleFn[]): TailSampleFn {
|
|
195
|
+
if (samplers.length === 0) {
|
|
196
|
+
throw new Error('combineTailSamplers requires at least one sampler');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (traceInfo: LocalTrace): boolean => {
|
|
200
|
+
return samplers.some((sampler) => sampler(traceInfo));
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a tail sampler based on custom predicate
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* // Keep traces with specific attributes
|
|
210
|
+
* const tailSampler = createCustomTailSampler((trace) => {
|
|
211
|
+
* const attrs = trace.localRootSpan.attributes
|
|
212
|
+
* return attrs['user.id'] === 'vip_123'
|
|
213
|
+
* })
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export function createCustomTailSampler(
|
|
217
|
+
predicate: (traceInfo: LocalTrace) => boolean,
|
|
218
|
+
): TailSampleFn {
|
|
219
|
+
return predicate;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Helper: Get span duration in milliseconds
|
|
224
|
+
*/
|
|
225
|
+
function getDurationMs(span: ReadableSpan): number {
|
|
226
|
+
const start = span.startTime[0] * 1000 + span.startTime[1] / 1_000_000;
|
|
227
|
+
const end = span.endTime[0] * 1000 + span.endTime[1] / 1_000_000;
|
|
228
|
+
return end - start;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Common presets for quick setup
|
|
233
|
+
*/
|
|
234
|
+
export const SamplingPresets = {
|
|
235
|
+
/**
|
|
236
|
+
* Production: 10% baseline, all errors, all slow (>1s)
|
|
237
|
+
* Recommended for most production workloads
|
|
238
|
+
*/
|
|
239
|
+
production: (): TailSampleFn =>
|
|
240
|
+
createAdaptiveTailSampler({
|
|
241
|
+
baselineSampleRate: 0.1,
|
|
242
|
+
slowThresholdMs: 1000,
|
|
243
|
+
}),
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* High-traffic: 1% baseline, all errors, all slow (>2s)
|
|
247
|
+
* For high-volume services where cost is a concern
|
|
248
|
+
*/
|
|
249
|
+
highTraffic: (): TailSampleFn =>
|
|
250
|
+
createAdaptiveTailSampler({
|
|
251
|
+
baselineSampleRate: 0.01,
|
|
252
|
+
slowThresholdMs: 2000,
|
|
253
|
+
}),
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Debugging: All errors, all slow (>500ms), 50% baseline
|
|
257
|
+
* For active debugging sessions
|
|
258
|
+
*/
|
|
259
|
+
debugging: (): TailSampleFn =>
|
|
260
|
+
createAdaptiveTailSampler({
|
|
261
|
+
baselineSampleRate: 0.5,
|
|
262
|
+
slowThresholdMs: 500,
|
|
263
|
+
}),
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Development: 100% sampling
|
|
267
|
+
* For local development and testing
|
|
268
|
+
*/
|
|
269
|
+
development: (): TailSampleFn => () => true,
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Errors only: Capture all failures, drop all successes
|
|
273
|
+
* For error-focused monitoring
|
|
274
|
+
*/
|
|
275
|
+
errorsOnly: (): TailSampleFn => createErrorOnlyTailSampler(),
|
|
276
|
+
};
|
package/src/sampling.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing utilities for autotel-edge
|
|
3
|
+
*
|
|
4
|
+
* This module provides testing helpers for edge runtime environments.
|
|
5
|
+
* Currently minimal - can be extended with utilities from autotel package as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Empty export to make this a valid module
|
|
9
|
+
export {};
|