autotel 2.25.2 → 2.25.3
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-QUW4I2OI.js → chunk-3ZLYWPMY.js} +3 -3
- package/dist/{chunk-QUW4I2OI.js.map → chunk-3ZLYWPMY.js.map} +1 -1
- package/dist/{chunk-BGVKKL2N.cjs → chunk-7FIGORWI.cjs} +46 -2
- package/dist/chunk-7FIGORWI.cjs.map +1 -0
- package/dist/{chunk-MNMLCLHH.cjs → chunk-AQXPGGQK.cjs} +5 -5
- package/dist/{chunk-MNMLCLHH.cjs.map → chunk-AQXPGGQK.cjs.map} +1 -1
- package/dist/{chunk-4KGC5N3J.cjs → chunk-FXKB7EPR.cjs} +13 -13
- package/dist/{chunk-4KGC5N3J.cjs.map → chunk-FXKB7EPR.cjs.map} +1 -1
- package/dist/{chunk-4SCBD22Z.js → chunk-IFKDBL65.js} +3 -3
- package/dist/{chunk-4SCBD22Z.js.map → chunk-IFKDBL65.js.map} +1 -1
- package/dist/{chunk-JVICEM6W.cjs → chunk-ITYASFHQ.cjs} +91 -19
- package/dist/chunk-ITYASFHQ.cjs.map +1 -0
- package/dist/{chunk-IKRHEUS7.js → chunk-JM63D22M.js} +10 -10
- package/dist/{chunk-IKRHEUS7.js.map → chunk-JM63D22M.js.map} +1 -1
- package/dist/{chunk-GVLK7YUU.cjs → chunk-KZEC4CHV.cjs} +6 -4
- package/dist/chunk-KZEC4CHV.cjs.map +1 -0
- 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-UKUYBUFQ.cjs → chunk-OWXXS4JB.cjs} +7 -7
- package/dist/{chunk-UKUYBUFQ.cjs.map → chunk-OWXXS4JB.cjs.map} +1 -1
- package/dist/{chunk-77PLEJ54.js → chunk-PKXD2RMI.js} +4 -4
- package/dist/{chunk-77PLEJ54.js.map → chunk-PKXD2RMI.js.map} +1 -1
- package/dist/{chunk-RWOVNF3V.cjs → chunk-RMGSBMQF.cjs} +36 -36
- package/dist/chunk-RMGSBMQF.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-KUSYIHW7.js → chunk-VVYSQXQL.js} +3 -3
- package/dist/{chunk-KUSYIHW7.js.map → chunk-VVYSQXQL.js.map} +1 -1
- package/dist/{chunk-XND7WBVX.js → chunk-VYA6QDNA.js} +43 -3
- package/dist/chunk-VYA6QDNA.js.map +1 -0
- package/dist/{chunk-L627YSSP.cjs → chunk-WOWTZ4EB.cjs} +9 -9
- package/dist/{chunk-L627YSSP.cjs.map → chunk-WOWTZ4EB.cjs.map} +1 -1
- package/dist/{chunk-X4RMFFMR.js → chunk-XDKK53OL.js} +6 -4
- package/dist/chunk-XDKK53OL.js.map +1 -0
- package/dist/decorators.cjs +5 -5
- package/dist/decorators.js +5 -5
- package/dist/event.cjs +8 -8
- package/dist/event.js +5 -5
- package/dist/functional.cjs +12 -12
- package/dist/functional.js +5 -5
- package/dist/index.cjs +71 -51
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +13 -13
- 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 +9 -9
- package/dist/messaging.d.cts +1 -1
- package/dist/messaging.d.ts +1 -1
- package/dist/messaging.js +6 -6
- 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 +10 -10
- package/dist/semantic-helpers.js +6 -6
- 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 +6 -6
- package/dist/webhook.js +5 -5
- package/dist/workflow-distributed.cjs +7 -7
- package/dist/workflow-distributed.js +5 -5
- package/dist/workflow.cjs +10 -10
- package/dist/workflow.js +6 -6
- 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/env-config.test.ts +77 -0
- package/src/env-config.ts +106 -0
- package/src/functional.ts +13 -7
- 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/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-JVICEM6W.cjs.map +0 -1
- package/dist/chunk-RWOVNF3V.cjs.map +0 -1
- package/dist/chunk-VXLEJWLY.js.map +0 -1
- package/dist/chunk-X4RMFFMR.js.map +0 -1
- package/dist/chunk-XND7WBVX.js.map +0 -1
- package/dist/chunk-YTGF4L2C.js.map +0 -1
package/src/env-config.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standard OpenTelemetry environment variables
|
|
3
3
|
*/
|
|
4
|
+
import type { Sampler as OtelSampler } from '@opentelemetry/sdk-trace-base';
|
|
5
|
+
import {
|
|
6
|
+
AlwaysOffSampler,
|
|
7
|
+
AlwaysOnSampler,
|
|
8
|
+
ParentBasedSampler,
|
|
9
|
+
TraceIdRatioBasedSampler,
|
|
10
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
11
|
+
|
|
4
12
|
export interface OtelEnvVars {
|
|
5
13
|
OTEL_SERVICE_NAME?: string;
|
|
6
14
|
OTEL_EXPORTER_OTLP_ENDPOINT?: string;
|
|
7
15
|
OTEL_EXPORTER_OTLP_HEADERS?: string;
|
|
8
16
|
OTEL_RESOURCE_ATTRIBUTES?: string;
|
|
9
17
|
OTEL_EXPORTER_OTLP_PROTOCOL?: 'http' | 'grpc';
|
|
18
|
+
OTEL_TRACES_SAMPLER?: string;
|
|
19
|
+
OTEL_TRACES_SAMPLER_ARG?: string;
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
/**
|
|
@@ -33,6 +43,7 @@ export interface EnvConfig {
|
|
|
33
43
|
protocol?: 'http' | 'grpc';
|
|
34
44
|
headers?: Record<string, string>;
|
|
35
45
|
resourceAttributes?: Record<string, string>;
|
|
46
|
+
otelSampler?: OtelSampler;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
/**
|
|
@@ -93,9 +104,99 @@ export function resolveOtelEnv(): OtelEnvVars {
|
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
|
|
107
|
+
if (process.env.OTEL_TRACES_SAMPLER) {
|
|
108
|
+
const value = process.env.OTEL_TRACES_SAMPLER.trim();
|
|
109
|
+
if (value) {
|
|
110
|
+
env.OTEL_TRACES_SAMPLER = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (process.env.OTEL_TRACES_SAMPLER_ARG) {
|
|
115
|
+
const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
|
|
116
|
+
if (value) {
|
|
117
|
+
env.OTEL_TRACES_SAMPLER_ARG = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
return env;
|
|
97
122
|
}
|
|
98
123
|
|
|
124
|
+
function parseRatioSamplerArg(
|
|
125
|
+
samplerName: string,
|
|
126
|
+
samplerArg: string | undefined,
|
|
127
|
+
): number {
|
|
128
|
+
if (samplerArg === undefined) {
|
|
129
|
+
return 1.0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ratio = Number(samplerArg);
|
|
133
|
+
if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
|
|
134
|
+
console.error(
|
|
135
|
+
`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`,
|
|
136
|
+
);
|
|
137
|
+
return 1.0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return ratio;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function warnOnUnusedSamplerArg(
|
|
144
|
+
samplerName: string,
|
|
145
|
+
samplerArg: string | undefined,
|
|
146
|
+
): void {
|
|
147
|
+
if (samplerArg !== undefined) {
|
|
148
|
+
console.error(
|
|
149
|
+
`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createSamplerFromEnv(
|
|
155
|
+
env: Pick<OtelEnvVars, 'OTEL_TRACES_SAMPLER' | 'OTEL_TRACES_SAMPLER_ARG'>,
|
|
156
|
+
): OtelSampler | undefined {
|
|
157
|
+
const samplerName = env.OTEL_TRACES_SAMPLER;
|
|
158
|
+
if (!samplerName) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
switch (samplerName) {
|
|
163
|
+
case 'always_on':
|
|
164
|
+
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
|
|
165
|
+
return new AlwaysOnSampler();
|
|
166
|
+
case 'always_off':
|
|
167
|
+
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
|
|
168
|
+
return new AlwaysOffSampler();
|
|
169
|
+
case 'traceidratio':
|
|
170
|
+
return new TraceIdRatioBasedSampler(
|
|
171
|
+
parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG),
|
|
172
|
+
);
|
|
173
|
+
case 'parentbased_always_on':
|
|
174
|
+
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
|
|
175
|
+
return new ParentBasedSampler({ root: new AlwaysOnSampler() });
|
|
176
|
+
case 'parentbased_always_off':
|
|
177
|
+
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
|
|
178
|
+
return new ParentBasedSampler({ root: new AlwaysOffSampler() });
|
|
179
|
+
case 'parentbased_traceidratio':
|
|
180
|
+
return new ParentBasedSampler({
|
|
181
|
+
root: new TraceIdRatioBasedSampler(
|
|
182
|
+
parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG),
|
|
183
|
+
),
|
|
184
|
+
});
|
|
185
|
+
case 'jaeger_remote':
|
|
186
|
+
case 'parentbased_jaeger_remote':
|
|
187
|
+
case 'xray':
|
|
188
|
+
console.error(
|
|
189
|
+
`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`,
|
|
190
|
+
);
|
|
191
|
+
return undefined;
|
|
192
|
+
default:
|
|
193
|
+
console.error(
|
|
194
|
+
`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`,
|
|
195
|
+
);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
99
200
|
/**
|
|
100
201
|
* Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
|
|
101
202
|
* Example: "service.version=1.0.0,deployment.environment=production"
|
|
@@ -191,6 +292,11 @@ export function envToConfig(env: OtelEnvVars): EnvConfig {
|
|
|
191
292
|
config.resourceAttributes = resourceAttrs;
|
|
192
293
|
}
|
|
193
294
|
|
|
295
|
+
const sampler = createSamplerFromEnv(env);
|
|
296
|
+
if (sampler) {
|
|
297
|
+
config.otelSampler = sampler;
|
|
298
|
+
}
|
|
299
|
+
|
|
194
300
|
return config;
|
|
195
301
|
}
|
|
196
302
|
|
package/src/functional.ts
CHANGED
|
@@ -42,7 +42,13 @@ import {
|
|
|
42
42
|
} from '@opentelemetry/api';
|
|
43
43
|
import { getConfig } from './config';
|
|
44
44
|
import { getConfig as getInitConfig, getSdk } from './init';
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
type Sampler,
|
|
47
|
+
type SamplingContext,
|
|
48
|
+
AlwaysSampler,
|
|
49
|
+
AUTOTEL_SAMPLING_TAIL_KEEP,
|
|
50
|
+
AUTOTEL_SAMPLING_TAIL_EVALUATED,
|
|
51
|
+
} from './sampling';
|
|
46
52
|
import { getEventQueue } from './track';
|
|
47
53
|
import type { TraceContext } from './trace-context';
|
|
48
54
|
import {
|
|
@@ -797,8 +803,8 @@ function wrapWithTracing<TArgs extends unknown[], TReturn>(
|
|
|
797
803
|
duration,
|
|
798
804
|
error,
|
|
799
805
|
});
|
|
800
|
-
span.setAttribute(
|
|
801
|
-
span.setAttribute(
|
|
806
|
+
span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
|
|
807
|
+
span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
|
|
802
808
|
}
|
|
803
809
|
};
|
|
804
810
|
|
|
@@ -1104,8 +1110,8 @@ function wrapWithTracingSync<TArgs extends unknown[], TReturn>(
|
|
|
1104
1110
|
duration,
|
|
1105
1111
|
error,
|
|
1106
1112
|
});
|
|
1107
|
-
span.setAttribute(
|
|
1108
|
-
span.setAttribute(
|
|
1113
|
+
span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
|
|
1114
|
+
span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
|
|
1109
1115
|
}
|
|
1110
1116
|
};
|
|
1111
1117
|
|
|
@@ -1352,8 +1358,8 @@ function executeImmediately<TReturn = unknown>(
|
|
|
1352
1358
|
duration,
|
|
1353
1359
|
error,
|
|
1354
1360
|
});
|
|
1355
|
-
span.setAttribute(
|
|
1356
|
-
span.setAttribute(
|
|
1361
|
+
span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
|
|
1362
|
+
span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
|
|
1357
1363
|
}
|
|
1358
1364
|
};
|
|
1359
1365
|
|
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
|
// ============================================================================
|