autotel 2.25.2 → 2.25.4
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/README.md +152 -1
- package/dist/attributes.d.cts +2 -2
- package/dist/attributes.d.ts +2 -2
- package/dist/auto.cjs +6 -6
- package/dist/auto.js +4 -4
- package/dist/{chunk-BGVKKL2N.cjs → chunk-7FIGORWI.cjs} +46 -2
- package/dist/chunk-7FIGORWI.cjs.map +1 -0
- package/dist/{chunk-4KGC5N3J.cjs → chunk-A5ZUL2RZ.cjs} +16 -16
- package/dist/{chunk-4KGC5N3J.cjs.map → chunk-A5ZUL2RZ.cjs.map} +1 -1
- package/dist/{chunk-SR35DG5A.js → chunk-BBBWDIYQ.js} +27 -13
- package/dist/chunk-BBBWDIYQ.js.map +1 -0
- package/dist/{chunk-QUW4I2OI.js → chunk-CMUM4JQI.js} +3 -3
- package/dist/{chunk-QUW4I2OI.js.map → chunk-CMUM4JQI.js.map} +1 -1
- package/dist/{chunk-MNMLCLHH.cjs → chunk-EEJGUBWV.cjs} +5 -5
- package/dist/{chunk-MNMLCLHH.cjs.map → chunk-EEJGUBWV.cjs.map} +1 -1
- package/dist/{chunk-W4EUTSB2.cjs → chunk-HZ3FYBJG.cjs} +27 -12
- package/dist/chunk-HZ3FYBJG.cjs.map +1 -0
- package/dist/{chunk-L627YSSP.cjs → chunk-I6JPSD4R.cjs} +9 -9
- package/dist/{chunk-L627YSSP.cjs.map → chunk-I6JPSD4R.cjs.map} +1 -1
- package/dist/{chunk-JVICEM6W.cjs → chunk-ITYASFHQ.cjs} +91 -19
- package/dist/chunk-ITYASFHQ.cjs.map +1 -0
- package/dist/{chunk-XRKAL7WJ.cjs → chunk-JSNUWSBH.cjs} +6 -6
- package/dist/chunk-JSNUWSBH.cjs.map +1 -0
- package/dist/{chunk-GVLK7YUU.cjs → chunk-KZEC4CHV.cjs} +6 -4
- package/dist/chunk-KZEC4CHV.cjs.map +1 -0
- package/dist/{chunk-UKUYBUFQ.cjs → chunk-MN6PZ4AN.cjs} +7 -7
- package/dist/{chunk-UKUYBUFQ.cjs.map → chunk-MN6PZ4AN.cjs.map} +1 -1
- package/dist/{chunk-VXLEJWLY.js → chunk-MNBAXRVG.js} +89 -17
- package/dist/chunk-MNBAXRVG.js.map +1 -0
- package/dist/{chunk-6YIDHH2S.cjs → chunk-OFPZULMQ.cjs} +32 -10
- package/dist/chunk-OFPZULMQ.cjs.map +1 -0
- package/dist/{chunk-IKRHEUS7.js → chunk-OPTGXEVN.js} +370 -351
- package/dist/chunk-OPTGXEVN.js.map +1 -0
- package/dist/{chunk-77PLEJ54.js → chunk-QDREXAD7.js} +4 -4
- package/dist/{chunk-77PLEJ54.js.map → chunk-QDREXAD7.js.map} +1 -1
- package/dist/{chunk-RWOVNF3V.cjs → chunk-QQLP4M6W.cjs} +400 -381
- package/dist/chunk-QQLP4M6W.cjs.map +1 -0
- package/dist/{chunk-YTGF4L2C.js → chunk-RUD7KS4R.js} +27 -5
- package/dist/chunk-RUD7KS4R.js.map +1 -0
- package/dist/{chunk-USSL3D6L.js → chunk-S4OFEXLA.js} +6 -6
- package/dist/chunk-S4OFEXLA.js.map +1 -0
- package/dist/{chunk-XND7WBVX.js → chunk-VYA6QDNA.js} +43 -3
- package/dist/chunk-VYA6QDNA.js.map +1 -0
- package/dist/{chunk-KUSYIHW7.js → chunk-WYP6OOCT.js} +3 -3
- package/dist/{chunk-KUSYIHW7.js.map → chunk-WYP6OOCT.js.map} +1 -1
- package/dist/{chunk-4SCBD22Z.js → chunk-XB2GITM5.js} +4 -4
- package/dist/{chunk-4SCBD22Z.js.map → chunk-XB2GITM5.js.map} +1 -1
- package/dist/{chunk-X4RMFFMR.js → chunk-XDKK53OL.js} +6 -4
- package/dist/chunk-XDKK53OL.js.map +1 -0
- package/dist/correlation-id.cjs +10 -9
- package/dist/correlation-id.d.cts +4 -1
- package/dist/correlation-id.d.ts +4 -1
- package/dist/correlation-id.js +2 -1
- package/dist/decorators.cjs +8 -8
- package/dist/decorators.js +7 -7
- package/dist/event.cjs +10 -9
- package/dist/event.js +7 -6
- package/dist/functional.cjs +14 -14
- package/dist/functional.js +7 -7
- package/dist/http.cjs +2 -2
- package/dist/http.js +1 -1
- package/dist/index.cjs +85 -65
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +16 -16
- package/dist/{init-Q4uIQKbq.d.cts → init-CIzpC5kZ.d.cts} +9 -2
- package/dist/{init-ls4xSZe5.d.ts → init-C_PiC_Su.d.ts} +9 -2
- package/dist/instrumentation.cjs +12 -12
- package/dist/instrumentation.cjs.map +1 -1
- package/dist/instrumentation.js +4 -4
- package/dist/instrumentation.js.map +1 -1
- package/dist/messaging-adapters.d.cts +1 -1
- package/dist/messaging-adapters.d.ts +1 -1
- package/dist/messaging.cjs +11 -11
- package/dist/messaging.d.cts +1 -1
- package/dist/messaging.d.ts +1 -1
- package/dist/messaging.js +8 -8
- package/dist/metric-helpers.d.cts +1 -1
- package/dist/metric-helpers.d.ts +1 -1
- package/dist/sampling.cjs +26 -10
- package/dist/sampling.d.cts +49 -1
- package/dist/sampling.d.ts +49 -1
- package/dist/sampling.js +1 -1
- package/dist/semantic-helpers.cjs +12 -12
- package/dist/semantic-helpers.js +8 -8
- package/dist/tail-sampling-processor.cjs +6 -2
- package/dist/tail-sampling-processor.d.cts +2 -2
- package/dist/tail-sampling-processor.d.ts +2 -2
- package/dist/tail-sampling-processor.js +5 -1
- package/dist/trace-helpers.d.cts +1 -1
- package/dist/trace-helpers.d.ts +1 -1
- package/dist/{utils-D1trOLNm.d.ts → utils-Buel3cj0.d.ts} +1 -1
- package/dist/{utils-DuNJfXSH.d.cts → utils-CbUkl8r1.d.cts} +1 -1
- package/dist/webhook.cjs +8 -8
- package/dist/webhook.js +7 -7
- package/dist/workflow-distributed.cjs +9 -9
- package/dist/workflow-distributed.js +7 -7
- package/dist/workflow.cjs +12 -12
- package/dist/workflow.js +8 -8
- package/dist/yaml-config.cjs +5 -5
- package/dist/yaml-config.d.cts +5 -4
- package/dist/yaml-config.d.ts +5 -4
- package/dist/yaml-config.js +2 -2
- package/package.json +1 -1
- package/src/correlation-id.ts +10 -5
- package/src/env-config.test.ts +77 -0
- package/src/env-config.ts +106 -0
- package/src/functional.ts +447 -421
- package/src/index.ts +6 -0
- package/src/init.customization.test.ts +61 -0
- package/src/init.integrations.test.ts +6 -1
- package/src/init.ts +23 -13
- package/src/instrumentation.ts +1 -1
- package/src/sampling.test.ts +96 -3
- package/src/sampling.ts +90 -0
- package/src/tail-sampling-processor.test.ts +26 -22
- package/src/tail-sampling-processor.ts +8 -4
- package/src/trace-context.test.ts +73 -0
- package/src/trace-context.ts +44 -12
- package/src/yaml-config.test.ts +71 -0
- package/src/yaml-config.ts +32 -2
- package/dist/chunk-6YIDHH2S.cjs.map +0 -1
- package/dist/chunk-BGVKKL2N.cjs.map +0 -1
- package/dist/chunk-GVLK7YUU.cjs.map +0 -1
- package/dist/chunk-IKRHEUS7.js.map +0 -1
- package/dist/chunk-JVICEM6W.cjs.map +0 -1
- package/dist/chunk-RWOVNF3V.cjs.map +0 -1
- package/dist/chunk-SR35DG5A.js.map +0 -1
- package/dist/chunk-USSL3D6L.js.map +0 -1
- package/dist/chunk-VXLEJWLY.js.map +0 -1
- package/dist/chunk-W4EUTSB2.cjs.map +0 -1
- package/dist/chunk-X4RMFFMR.js.map +0 -1
- package/dist/chunk-XND7WBVX.js.map +0 -1
- package/dist/chunk-XRKAL7WJ.cjs.map +0 -1
- package/dist/chunk-YTGF4L2C.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -60,6 +60,7 @@ export {
|
|
|
60
60
|
AttributeRedactingProcessor,
|
|
61
61
|
REDACTOR_PATTERNS,
|
|
62
62
|
REDACTOR_PRESETS,
|
|
63
|
+
createAttributeRedactor,
|
|
63
64
|
createRedactedSpan,
|
|
64
65
|
type AttributeRedactorFn,
|
|
65
66
|
type AttributeRedactorPreset,
|
|
@@ -151,6 +152,7 @@ export { formatDuration } from './pretty-log-formatter';
|
|
|
151
152
|
export {
|
|
152
153
|
type Sampler,
|
|
153
154
|
type SamplingContext,
|
|
155
|
+
type SamplingPreset,
|
|
154
156
|
AlwaysSampler,
|
|
155
157
|
NeverSampler,
|
|
156
158
|
RandomSampler,
|
|
@@ -158,6 +160,10 @@ export {
|
|
|
158
160
|
UserIdSampler,
|
|
159
161
|
createLinkFromHeaders,
|
|
160
162
|
extractLinksFromBatch,
|
|
163
|
+
samplingPresets,
|
|
164
|
+
resolveSamplingPreset,
|
|
165
|
+
AUTOTEL_SAMPLING_TAIL_KEEP,
|
|
166
|
+
AUTOTEL_SAMPLING_TAIL_EVALUATED,
|
|
161
167
|
} from './sampling';
|
|
162
168
|
|
|
163
169
|
// Events API
|
|
@@ -3,6 +3,7 @@ import type { MetricReader } from '@opentelemetry/sdk-metrics';
|
|
|
3
3
|
import type { NodeSDK } from '@opentelemetry/sdk-node';
|
|
4
4
|
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
5
5
|
import { mock, mockDeep, type DeepMockProxy } from 'vitest-mock-extended';
|
|
6
|
+
import { AlwaysSampler, NeverSampler } from './sampling';
|
|
6
7
|
|
|
7
8
|
type SdkRecord = {
|
|
8
9
|
options: Record<string, unknown>;
|
|
@@ -112,6 +113,7 @@ async function loadInitWithMocks() {
|
|
|
112
113
|
return {
|
|
113
114
|
init: mod.init,
|
|
114
115
|
getConfig: mod.getConfig,
|
|
116
|
+
getDefaultSampler: mod.getDefaultSampler,
|
|
115
117
|
resolveLogsFlag: mod.resolveLogsFlag,
|
|
116
118
|
sdkInstances,
|
|
117
119
|
traceExporterOptions,
|
|
@@ -128,6 +130,8 @@ describe('init() customization', () => {
|
|
|
128
130
|
delete process.env.AUTOTEL_METRICS;
|
|
129
131
|
delete process.env.AUTOTEL_LOGS;
|
|
130
132
|
delete process.env.OTEL_LOGS_EXPORTER;
|
|
133
|
+
delete process.env.OTEL_TRACES_SAMPLER;
|
|
134
|
+
delete process.env.OTEL_TRACES_SAMPLER_ARG;
|
|
131
135
|
delete process.env.NODE_ENV;
|
|
132
136
|
});
|
|
133
137
|
|
|
@@ -227,6 +231,63 @@ describe('init() customization', () => {
|
|
|
227
231
|
});
|
|
228
232
|
});
|
|
229
233
|
|
|
234
|
+
it('resolves sampling preset shorthand to a sampler instance', async () => {
|
|
235
|
+
const { init, getDefaultSampler } = await loadInitWithMocks();
|
|
236
|
+
|
|
237
|
+
init({
|
|
238
|
+
service: 'sampling-preset-app',
|
|
239
|
+
sampling: 'development',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const sampler = getDefaultSampler();
|
|
243
|
+
expect(sampler.constructor.name).toBe('AlwaysSampler');
|
|
244
|
+
expect(sampler.shouldSample({ operationName: 'test', args: [] })).toBe(
|
|
245
|
+
true,
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('prefers explicit sampler over sampling preset shorthand', async () => {
|
|
250
|
+
const { init, getDefaultSampler } = await loadInitWithMocks();
|
|
251
|
+
const explicitSampler = new NeverSampler();
|
|
252
|
+
|
|
253
|
+
init({
|
|
254
|
+
service: 'sampling-precedence-app',
|
|
255
|
+
sampler: explicitSampler,
|
|
256
|
+
sampling: 'development',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(getDefaultSampler()).toBe(explicitSampler);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('uses OTEL_TRACES_SAMPLER when no explicit sampling config is provided', async () => {
|
|
263
|
+
process.env.OTEL_TRACES_SAMPLER = 'always_off';
|
|
264
|
+
const { init, sdkInstances } = await loadInitWithMocks();
|
|
265
|
+
|
|
266
|
+
init({
|
|
267
|
+
service: 'env-sampler-app',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const options = sdkInstances.at(-1)?.options as Record<string, unknown>;
|
|
271
|
+
expect((options.sampler as { toString(): string }).toString()).toContain(
|
|
272
|
+
'AlwaysOffSampler',
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('prefers explicit sampling config over OTEL_TRACES_SAMPLER', async () => {
|
|
277
|
+
process.env.OTEL_TRACES_SAMPLER = 'always_off';
|
|
278
|
+
const { init, sdkInstances } = await loadInitWithMocks();
|
|
279
|
+
|
|
280
|
+
init({
|
|
281
|
+
service: 'explicit-over-env-sampler-app',
|
|
282
|
+
sampling: 'development',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const options = sdkInstances.at(-1)?.options as Record<string, unknown>;
|
|
286
|
+
expect((options.sampler as { toString(): string }).toString()).toBe(
|
|
287
|
+
'AutotelSamplerAdapter',
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
230
291
|
it('supports sdkFactory overrides', async () => {
|
|
231
292
|
const { init, sdkInstances } = await loadInitWithMocks();
|
|
232
293
|
const customSdk = mockDeep<NodeSDK>();
|
|
@@ -182,13 +182,18 @@ describe('init() integrations vs instrumentations', () => {
|
|
|
182
182
|
_resetAutoInstrumentationsLoader();
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
afterEach(() => {
|
|
185
|
+
afterEach(async () => {
|
|
186
186
|
for (const mod of mockedModules) {
|
|
187
187
|
vi.doUnmock(mod);
|
|
188
188
|
}
|
|
189
189
|
vi.clearAllMocks();
|
|
190
190
|
_resetAutoInstrumentationsLoader();
|
|
191
191
|
delete process.env.AUTOTEL_METRICS;
|
|
192
|
+
// Reset global OTel state that can leak between forked test files
|
|
193
|
+
const { trace, context, propagation } = await import('@opentelemetry/api');
|
|
194
|
+
trace.disable();
|
|
195
|
+
context.disable();
|
|
196
|
+
propagation.disable();
|
|
192
197
|
});
|
|
193
198
|
|
|
194
199
|
it('excludes manual instrumentations from auto-instrumentations when autoInstrumentations: true', async () => {
|
package/src/init.ts
CHANGED
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
ATTR_SERVICE_NAME,
|
|
26
26
|
ATTR_SERVICE_VERSION,
|
|
27
27
|
} from '@opentelemetry/semantic-conventions';
|
|
28
|
-
import type { Sampler } from './sampling';
|
|
29
|
-
import {
|
|
28
|
+
import type { Sampler, SamplingPreset } from './sampling';
|
|
29
|
+
import { samplingPresets, resolveSamplingPreset } from './sampling';
|
|
30
30
|
import type { EventSubscriber } from './event-subscriber';
|
|
31
31
|
import type { Logger } from './logger';
|
|
32
32
|
import type { Attributes, Context, SpanKind, Link } from '@opentelemetry/api';
|
|
@@ -603,9 +603,17 @@ export interface AutotelConfig {
|
|
|
603
603
|
*/
|
|
604
604
|
logs?: boolean | 'auto';
|
|
605
605
|
|
|
606
|
-
/** Sampling strategy
|
|
606
|
+
/** Sampling strategy - takes precedence over `sampling` preset */
|
|
607
607
|
sampler?: Sampler;
|
|
608
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Sampling preset shorthand — resolves to a pre-configured sampler.
|
|
611
|
+
* If both `sampler` and `sampling` are provided, `sampler` takes precedence.
|
|
612
|
+
*
|
|
613
|
+
* @default 'production'
|
|
614
|
+
*/
|
|
615
|
+
sampling?: SamplingPreset;
|
|
616
|
+
|
|
609
617
|
/** Service version (default: auto-detect from package.json or '1.0.0') */
|
|
610
618
|
version?: string;
|
|
611
619
|
|
|
@@ -1592,8 +1600,17 @@ export function init(cfg: AutotelConfig): void {
|
|
|
1592
1600
|
}
|
|
1593
1601
|
}
|
|
1594
1602
|
|
|
1595
|
-
const autotelSampler =
|
|
1596
|
-
|
|
1603
|
+
const autotelSampler =
|
|
1604
|
+
mergedConfig.sampler ??
|
|
1605
|
+
(mergedConfig.sampling
|
|
1606
|
+
? resolveSamplingPreset(mergedConfig.sampling)
|
|
1607
|
+
: undefined);
|
|
1608
|
+
if (autotelSampler) {
|
|
1609
|
+
mergedConfig.sampler = autotelSampler;
|
|
1610
|
+
}
|
|
1611
|
+
const sampler: OtelSampler = autotelSampler
|
|
1612
|
+
? toOtelSampler(autotelSampler)
|
|
1613
|
+
: (envConfig.otelSampler ?? toOtelSampler(samplingPresets.production()));
|
|
1597
1614
|
|
|
1598
1615
|
const sdkOptions: Partial<NodeSDKConfiguration> = {
|
|
1599
1616
|
resource,
|
|
@@ -1921,14 +1938,7 @@ export function warnIfNotInitialized(context: string): void {
|
|
|
1921
1938
|
* Get default sampler
|
|
1922
1939
|
*/
|
|
1923
1940
|
export function getDefaultSampler(): Sampler {
|
|
1924
|
-
return (
|
|
1925
|
-
config?.sampler ||
|
|
1926
|
-
new AdaptiveSampler({
|
|
1927
|
-
baselineSampleRate: 0.1,
|
|
1928
|
-
alwaysSampleErrors: true,
|
|
1929
|
-
alwaysSampleSlow: true,
|
|
1930
|
-
})
|
|
1931
|
-
);
|
|
1941
|
+
return config?.sampler || samplingPresets.production();
|
|
1932
1942
|
}
|
|
1933
1943
|
|
|
1934
1944
|
/**
|
package/src/instrumentation.ts
CHANGED
|
@@ -249,7 +249,7 @@ export async function initInstrumentation(
|
|
|
249
249
|
headers: otlpHeaders,
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
// Enables tail sampling via sampling.tail.keep attribute
|
|
252
|
+
// Enables tail sampling via autotel.sampling.tail.keep attribute
|
|
253
253
|
const spanProcessor = new TailSamplingSpanProcessor(
|
|
254
254
|
new BatchSpanProcessor(traceExporter),
|
|
255
255
|
);
|
package/src/sampling.test.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
FeatureFlagSampler,
|
|
12
12
|
createLinkFromHeaders,
|
|
13
13
|
extractLinksFromBatch,
|
|
14
|
+
samplingPresets,
|
|
15
|
+
resolveSamplingPreset,
|
|
14
16
|
type SamplingContext,
|
|
15
17
|
} from './sampling';
|
|
16
18
|
import { type ILogger } from './logger';
|
|
@@ -64,9 +66,9 @@ describe('Sampling', () => {
|
|
|
64
66
|
);
|
|
65
67
|
|
|
66
68
|
const sampleCount = results.filter(Boolean).length;
|
|
67
|
-
// Allow
|
|
68
|
-
expect(sampleCount).toBeGreaterThan(
|
|
69
|
-
expect(sampleCount).toBeLessThan(
|
|
69
|
+
// Allow 20% margin of error — random sampling is inherently noisy
|
|
70
|
+
expect(sampleCount).toBeGreaterThan(400);
|
|
71
|
+
expect(sampleCount).toBeLessThan(600);
|
|
70
72
|
});
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -964,4 +966,95 @@ describe('Sampling', () => {
|
|
|
964
966
|
});
|
|
965
967
|
});
|
|
966
968
|
});
|
|
969
|
+
|
|
970
|
+
describe('samplingPresets', () => {
|
|
971
|
+
it('development() returns AlwaysSampler', () => {
|
|
972
|
+
const sampler = samplingPresets.development();
|
|
973
|
+
expect(sampler).toBeInstanceOf(AlwaysSampler);
|
|
974
|
+
expect(sampler.shouldSample(context)).toBe(true);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('errorsOnly() returns AdaptiveSampler that drops healthy baseline', () => {
|
|
978
|
+
const sampler = samplingPresets.errorsOnly();
|
|
979
|
+
expect(sampler).toBeInstanceOf(AdaptiveSampler);
|
|
980
|
+
sampler.shouldSample(context); // prime the WeakMap baseline decision
|
|
981
|
+
// Baseline is 0, so shouldKeepTrace for successful fast requests = false
|
|
982
|
+
expect(
|
|
983
|
+
sampler.shouldKeepTrace!(context, { success: true, duration: 50 }),
|
|
984
|
+
).toBe(false);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('errorsOnly() keeps errors', () => {
|
|
988
|
+
const sampler = samplingPresets.errorsOnly();
|
|
989
|
+
sampler.shouldSample(context); // prime the baseline decision
|
|
990
|
+
expect(
|
|
991
|
+
sampler.shouldKeepTrace!(context, {
|
|
992
|
+
success: false,
|
|
993
|
+
duration: 50,
|
|
994
|
+
error: new Error('fail'),
|
|
995
|
+
}),
|
|
996
|
+
).toBe(true);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('production() returns AdaptiveSampler with 10% baseline', () => {
|
|
1000
|
+
const sampler = samplingPresets.production();
|
|
1001
|
+
expect(sampler).toBeInstanceOf(AdaptiveSampler);
|
|
1002
|
+
expect(sampler.needsTailSampling!()).toBe(true);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('production() keeps errors', () => {
|
|
1006
|
+
const sampler = samplingPresets.production();
|
|
1007
|
+
sampler.shouldSample(context);
|
|
1008
|
+
expect(
|
|
1009
|
+
sampler.shouldKeepTrace!(context, {
|
|
1010
|
+
success: false,
|
|
1011
|
+
duration: 50,
|
|
1012
|
+
error: new Error('fail'),
|
|
1013
|
+
}),
|
|
1014
|
+
).toBe(true);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('production() accepts overrides', () => {
|
|
1018
|
+
const sampler = samplingPresets.production({ baselineSampleRate: 1.0 });
|
|
1019
|
+
sampler.shouldSample(context);
|
|
1020
|
+
// With 100% baseline, all healthy traffic is kept
|
|
1021
|
+
expect(
|
|
1022
|
+
sampler.shouldKeepTrace!(context, { success: true, duration: 50 }),
|
|
1023
|
+
).toBe(true);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('off() returns NeverSampler', () => {
|
|
1027
|
+
const sampler = samplingPresets.off();
|
|
1028
|
+
expect(sampler).toBeInstanceOf(NeverSampler);
|
|
1029
|
+
expect(sampler.shouldSample(context)).toBe(false);
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
describe('resolveSamplingPreset', () => {
|
|
1034
|
+
it('resolves development', () => {
|
|
1035
|
+
const sampler = resolveSamplingPreset('development');
|
|
1036
|
+
expect(sampler).toBeInstanceOf(AlwaysSampler);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it('resolves errors-only', () => {
|
|
1040
|
+
const sampler = resolveSamplingPreset('errors-only');
|
|
1041
|
+
expect(sampler).toBeInstanceOf(AdaptiveSampler);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it('resolves production', () => {
|
|
1045
|
+
const sampler = resolveSamplingPreset('production');
|
|
1046
|
+
expect(sampler).toBeInstanceOf(AdaptiveSampler);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it('resolves off', () => {
|
|
1050
|
+
const sampler = resolveSamplingPreset('off');
|
|
1051
|
+
expect(sampler).toBeInstanceOf(NeverSampler);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('throws on invalid preset with helpful message', () => {
|
|
1055
|
+
expect(() => resolveSamplingPreset('banana' as any)).toThrow(
|
|
1056
|
+
/Unknown sampling preset: "banana".*Valid presets: development, errors-only, production, off/,
|
|
1057
|
+
);
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
967
1060
|
});
|
package/src/sampling.ts
CHANGED
|
@@ -26,6 +26,13 @@ import type { Link, Attributes } from '@opentelemetry/api';
|
|
|
26
26
|
import { TraceFlags } from '@opentelemetry/api';
|
|
27
27
|
import { type Logger } from './logger';
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Tail sampling attribute keys (autotel-internal, not OTel semconv)
|
|
31
|
+
*/
|
|
32
|
+
export const AUTOTEL_SAMPLING_TAIL_KEEP = 'autotel.sampling.tail.keep';
|
|
33
|
+
export const AUTOTEL_SAMPLING_TAIL_EVALUATED =
|
|
34
|
+
'autotel.sampling.tail.evaluated';
|
|
35
|
+
|
|
29
36
|
/**
|
|
30
37
|
* Sampler interface - return true to trace, false to skip
|
|
31
38
|
*/
|
|
@@ -493,6 +500,89 @@ export class FeatureFlagSampler implements Sampler {
|
|
|
493
500
|
}
|
|
494
501
|
}
|
|
495
502
|
|
|
503
|
+
// ============================================================================
|
|
504
|
+
// Sampling Presets
|
|
505
|
+
// ============================================================================
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Named sampling presets for common environments.
|
|
509
|
+
* Use with `init({ sampling: 'production' })` or directly via factories.
|
|
510
|
+
*/
|
|
511
|
+
export type SamplingPreset =
|
|
512
|
+
| 'development'
|
|
513
|
+
| 'errors-only'
|
|
514
|
+
| 'production'
|
|
515
|
+
| 'off';
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Sampling preset factories.
|
|
519
|
+
*
|
|
520
|
+
* For most users, the string shorthand on `init()` is simpler:
|
|
521
|
+
* ```typescript
|
|
522
|
+
* init({ service: 'my-app', sampling: 'production' })
|
|
523
|
+
* ```
|
|
524
|
+
*
|
|
525
|
+
* Use factories when you need to customize:
|
|
526
|
+
* ```typescript
|
|
527
|
+
* init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })
|
|
528
|
+
* ```
|
|
529
|
+
*/
|
|
530
|
+
export const samplingPresets = {
|
|
531
|
+
/** Capture everything — best for local development and debugging */
|
|
532
|
+
development: () => new AlwaysSampler(),
|
|
533
|
+
|
|
534
|
+
/** Only bad outcomes — zero baseline, errors always kept */
|
|
535
|
+
errorsOnly: () =>
|
|
536
|
+
new AdaptiveSampler({
|
|
537
|
+
baselineSampleRate: 0,
|
|
538
|
+
alwaysSampleErrors: true,
|
|
539
|
+
}),
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Balanced production defaults — 10% baseline + errors + slow traces.
|
|
543
|
+
* Pass overrides to tune (uses the same option names as AdaptiveSampler).
|
|
544
|
+
*/
|
|
545
|
+
production: (overrides?: {
|
|
546
|
+
baselineSampleRate?: number;
|
|
547
|
+
slowThresholdMs?: number;
|
|
548
|
+
alwaysSampleErrors?: boolean;
|
|
549
|
+
alwaysSampleSlow?: boolean;
|
|
550
|
+
}) =>
|
|
551
|
+
new AdaptiveSampler({
|
|
552
|
+
baselineSampleRate: 0.1,
|
|
553
|
+
alwaysSampleErrors: true,
|
|
554
|
+
alwaysSampleSlow: true,
|
|
555
|
+
slowThresholdMs: 1000,
|
|
556
|
+
...overrides,
|
|
557
|
+
}),
|
|
558
|
+
|
|
559
|
+
/** Disable sampling entirely */
|
|
560
|
+
off: () => new NeverSampler(),
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Resolve a preset string to a Sampler instance.
|
|
565
|
+
* Used internally by `init()` when `sampling` string is provided.
|
|
566
|
+
*
|
|
567
|
+
* @throws Error if preset is not recognized
|
|
568
|
+
*/
|
|
569
|
+
export function resolveSamplingPreset(preset: SamplingPreset): Sampler {
|
|
570
|
+
switch (preset) {
|
|
571
|
+
case 'development':
|
|
572
|
+
return samplingPresets.development();
|
|
573
|
+
case 'errors-only':
|
|
574
|
+
return samplingPresets.errorsOnly();
|
|
575
|
+
case 'production':
|
|
576
|
+
return samplingPresets.production();
|
|
577
|
+
case 'off':
|
|
578
|
+
return samplingPresets.off();
|
|
579
|
+
default:
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
496
586
|
// ============================================================================
|
|
497
587
|
// Link Helper Functions
|
|
498
588
|
// ============================================================================
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
6
|
import { TailSamplingSpanProcessor } from './tail-sampling-processor';
|
|
7
|
+
import {
|
|
8
|
+
AUTOTEL_SAMPLING_TAIL_KEEP,
|
|
9
|
+
AUTOTEL_SAMPLING_TAIL_EVALUATED,
|
|
10
|
+
} from './sampling';
|
|
7
11
|
import type {
|
|
8
12
|
SpanProcessor,
|
|
9
13
|
ReadableSpan,
|
|
@@ -86,8 +90,8 @@ describe('TailSamplingSpanProcessor', () => {
|
|
|
86
90
|
|
|
87
91
|
it('should forward spans marked to keep (sampling.tail.keep = true)', () => {
|
|
88
92
|
const span = createMockSpan({
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
94
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: true,
|
|
91
95
|
});
|
|
92
96
|
|
|
93
97
|
tailSamplingProcessor.onEnd(span);
|
|
@@ -100,8 +104,8 @@ describe('TailSamplingSpanProcessor', () => {
|
|
|
100
104
|
describe('Span dropping', () => {
|
|
101
105
|
it('should drop spans marked to drop (sampling.tail.keep = false)', () => {
|
|
102
106
|
const span = createMockSpan({
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
108
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: false,
|
|
105
109
|
});
|
|
106
110
|
|
|
107
111
|
tailSamplingProcessor.onEnd(span);
|
|
@@ -112,13 +116,13 @@ describe('TailSamplingSpanProcessor', () => {
|
|
|
112
116
|
|
|
113
117
|
it('should drop multiple spans marked as false', () => {
|
|
114
118
|
const span1 = createMockSpan({
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
120
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: false,
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
const span2 = createMockSpan({
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
125
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: false,
|
|
122
126
|
});
|
|
123
127
|
|
|
124
128
|
tailSamplingProcessor.onEnd(span1);
|
|
@@ -131,8 +135,8 @@ describe('TailSamplingSpanProcessor', () => {
|
|
|
131
135
|
describe('Edge cases', () => {
|
|
132
136
|
it('should forward spans when only evaluated but no keep attribute', () => {
|
|
133
137
|
const span = createMockSpan({
|
|
134
|
-
|
|
135
|
-
// Missing
|
|
138
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
139
|
+
// Missing AUTOTEL_SAMPLING_TAIL_KEEP
|
|
136
140
|
});
|
|
137
141
|
|
|
138
142
|
tailSamplingProcessor.onEnd(span);
|
|
@@ -143,8 +147,8 @@ describe('TailSamplingSpanProcessor', () => {
|
|
|
143
147
|
|
|
144
148
|
it('should forward spans when only keep but not evaluated', () => {
|
|
145
149
|
const span = createMockSpan({
|
|
146
|
-
// Missing
|
|
147
|
-
|
|
150
|
+
// Missing AUTOTEL_SAMPLING_TAIL_EVALUATED
|
|
151
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: false,
|
|
148
152
|
});
|
|
149
153
|
|
|
150
154
|
tailSamplingProcessor.onEnd(span);
|
|
@@ -156,12 +160,12 @@ describe('TailSamplingSpanProcessor', () => {
|
|
|
156
160
|
it('should handle mixed spans (some kept, some dropped)', () => {
|
|
157
161
|
const keptSpan1 = createMockSpan({ foo: 'bar' });
|
|
158
162
|
const droppedSpan = createMockSpan({
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
164
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: false,
|
|
161
165
|
});
|
|
162
166
|
const keptSpan2 = createMockSpan({
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
168
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: true,
|
|
165
169
|
});
|
|
166
170
|
|
|
167
171
|
tailSamplingProcessor.onEnd(keptSpan1);
|
|
@@ -196,20 +200,20 @@ describe('TailSamplingSpanProcessor', () => {
|
|
|
196
200
|
// 3. Error request (keep)
|
|
197
201
|
|
|
198
202
|
const fastSpan = createMockSpan({
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
204
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: false, // Fast, no errors
|
|
201
205
|
duration: 50,
|
|
202
206
|
});
|
|
203
207
|
|
|
204
208
|
const slowSpan = createMockSpan({
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
210
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: true, // Slow request
|
|
207
211
|
duration: 1000,
|
|
208
212
|
});
|
|
209
213
|
|
|
210
214
|
const errorSpan = createMockSpan({
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
[AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
|
|
216
|
+
[AUTOTEL_SAMPLING_TAIL_KEEP]: true, // Had error
|
|
213
217
|
error: true,
|
|
214
218
|
});
|
|
215
219
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tail Sampling Span Processor
|
|
3
3
|
*
|
|
4
|
-
* Filters spans based on the `sampling.tail.keep` attribute set during execution.
|
|
4
|
+
* Filters spans based on the `autotel.sampling.tail.keep` attribute set during execution.
|
|
5
5
|
* This enables adaptive sampling where we decide whether to keep a span AFTER
|
|
6
6
|
* the operation completes, based on criteria like errors, duration, etc.
|
|
7
7
|
*
|
|
8
8
|
* How it works:
|
|
9
9
|
* 1. Decorator creates span optimistically (head sampling returns true)
|
|
10
10
|
* 2. Operation executes and completes
|
|
11
|
-
* 3. Decorator calls shouldKeepTrace() and sets sampling.tail.keep attribute
|
|
11
|
+
* 3. Decorator calls shouldKeepTrace() and sets autotel.sampling.tail.keep attribute
|
|
12
12
|
* 4. This processor checks the attribute and drops spans marked as false
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -18,6 +18,10 @@ import type {
|
|
|
18
18
|
} from '@opentelemetry/sdk-trace-base';
|
|
19
19
|
import type { Context } from '@opentelemetry/api';
|
|
20
20
|
import type { Span } from '@opentelemetry/sdk-trace-base';
|
|
21
|
+
import {
|
|
22
|
+
AUTOTEL_SAMPLING_TAIL_KEEP,
|
|
23
|
+
AUTOTEL_SAMPLING_TAIL_EVALUATED,
|
|
24
|
+
} from './sampling';
|
|
21
25
|
|
|
22
26
|
export class TailSamplingSpanProcessor implements SpanProcessor {
|
|
23
27
|
private wrappedProcessor: SpanProcessor;
|
|
@@ -31,8 +35,8 @@ export class TailSamplingSpanProcessor implements SpanProcessor {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
onEnd(span: ReadableSpan): void {
|
|
34
|
-
const tailEvaluated = span.attributes[
|
|
35
|
-
const shouldKeep = span.attributes[
|
|
38
|
+
const tailEvaluated = span.attributes[AUTOTEL_SAMPLING_TAIL_EVALUATED];
|
|
39
|
+
const shouldKeep = span.attributes[AUTOTEL_SAMPLING_TAIL_KEEP];
|
|
36
40
|
|
|
37
41
|
if (tailEvaluated === true && shouldKeep === false) {
|
|
38
42
|
return;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { enterOrRun } from './trace-context';
|
|
3
|
+
|
|
4
|
+
type Box<T> = { value: T };
|
|
5
|
+
|
|
6
|
+
function createFakeStorage<T>(initialValue?: T) {
|
|
7
|
+
let currentStore =
|
|
8
|
+
initialValue === undefined ? undefined : { value: initialValue };
|
|
9
|
+
const runCalls: Array<Box<T>> = [];
|
|
10
|
+
const enterWithCalls: Array<Box<T>> = [];
|
|
11
|
+
|
|
12
|
+
const storage = {
|
|
13
|
+
getStore() {
|
|
14
|
+
return currentStore;
|
|
15
|
+
},
|
|
16
|
+
run(store: Box<T>, fn: () => void) {
|
|
17
|
+
runCalls.push(store);
|
|
18
|
+
const previousStore = currentStore;
|
|
19
|
+
currentStore = store;
|
|
20
|
+
try {
|
|
21
|
+
fn();
|
|
22
|
+
} finally {
|
|
23
|
+
currentStore = previousStore;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
enterWith(store: Box<T>) {
|
|
27
|
+
enterWithCalls.push(store);
|
|
28
|
+
currentStore = store;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
enterWithCalls,
|
|
34
|
+
runCalls,
|
|
35
|
+
storage: storage as unknown as {
|
|
36
|
+
enterWith?: (store: Box<T>) => void;
|
|
37
|
+
getStore: () => Box<T> | undefined;
|
|
38
|
+
run: (store: Box<T>, fn: () => void) => void;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('enterOrRun', () => {
|
|
44
|
+
it('mutates the existing store when already inside a run scope', () => {
|
|
45
|
+
const { storage } = createFakeStorage('outer');
|
|
46
|
+
|
|
47
|
+
enterOrRun(storage as never, 'updated');
|
|
48
|
+
|
|
49
|
+
expect(storage.getStore()?.value).toBe('updated');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('falls back to run() when enterWith throws', () => {
|
|
53
|
+
const { runCalls, storage } = createFakeStorage<string>();
|
|
54
|
+
storage.enterWith = () => {
|
|
55
|
+
throw new Error('enterWith not supported');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
enterOrRun(storage as never, 'worker-value');
|
|
59
|
+
|
|
60
|
+
expect(runCalls).toHaveLength(1);
|
|
61
|
+
expect(runCalls[0]?.value).toBe('worker-value');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('prefers enterWith() when no store exists and the runtime supports it', () => {
|
|
65
|
+
const { enterWithCalls, storage } = createFakeStorage<string>();
|
|
66
|
+
|
|
67
|
+
enterOrRun(storage as never, 'node-value');
|
|
68
|
+
|
|
69
|
+
expect(enterWithCalls).toHaveLength(1);
|
|
70
|
+
expect(enterWithCalls[0]?.value).toBe('node-value');
|
|
71
|
+
expect(storage.getStore()?.value).toBe('node-value');
|
|
72
|
+
});
|
|
73
|
+
});
|