Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle OTEL_TRACES_SAMPLER env var #2111

Merged
merged 4 commits into from
Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions packages/opentelemetry-core/src/utils/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const ENVIRONMENT_NUMBERS_KEYS = [
'OTEL_BSP_MAX_EXPORT_BATCH_SIZE',
'OTEL_BSP_MAX_QUEUE_SIZE',
'OTEL_BSP_SCHEDULE_DELAY',
'OTEL_SAMPLING_PROBABILITY',
'OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT',
'OTEL_SPAN_EVENT_COUNT_LIMIT',
'OTEL_SPAN_LINK_COUNT_LIMIT',
Expand Down Expand Up @@ -70,6 +69,8 @@ export type ENVIRONMENT = {
OTEL_EXPORTER_ZIPKIN_ENDPOINT?: string;
OTEL_LOG_LEVEL?: DiagLogLevel;
OTEL_RESOURCE_ATTRIBUTES?: string;
OTEL_TRACES_SAMPLER_ARG?: string;
OTEL_TRACES_SAMPLER?: string;
} & ENVIRONMENT_NUMBERS &
ENVIRONMENT_LISTS;

Expand Down Expand Up @@ -100,10 +101,11 @@ export const DEFAULT_ENVIRONMENT: Required<ENVIRONMENT> = {
OTEL_NO_PATCH_MODULES: [],
OTEL_PROPAGATORS: ['tracecontext', 'baggage'],
OTEL_RESOURCE_ATTRIBUTES: '',
OTEL_SAMPLING_PROBABILITY: 1,
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: 128,
OTEL_SPAN_EVENT_COUNT_LIMIT: 128,
OTEL_SPAN_LINK_COUNT_LIMIT: 128,
OTEL_TRACES_SAMPLER_ARG: '',
obecny marked this conversation as resolved.
Show resolved Hide resolved
OTEL_TRACES_SAMPLER: 'parentbased_always_on',
};

/**
Expand Down Expand Up @@ -196,10 +198,6 @@ export function parseEnvironment(values: RAW_ENVIRONMENT): ENVIRONMENT {
const key = env as keyof ENVIRONMENT;

switch (key) {
case 'OTEL_SAMPLING_PROBABILITY':
parseNumber(key, environment, values, 0, 1);
break;

case 'OTEL_LOG_LEVEL':
setLogLevelFromEnv(key, environment, values);
break;
Expand Down
24 changes: 6 additions & 18 deletions packages/opentelemetry-core/test/utils/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,15 @@ describe('environment', () => {
OTEL_LOG_LEVEL: 'ERROR',
OTEL_NO_PATCH_MODULES: 'a,b,c',
OTEL_RESOURCE_ATTRIBUTES: '<attrs>',
OTEL_SAMPLING_PROBABILITY: '0.5',
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: 10,
OTEL_SPAN_EVENT_COUNT_LIMIT: 20,
OTEL_SPAN_LINK_COUNT_LIMIT: 30,
OTEL_TRACES_SAMPLER: 'always_on',
OTEL_TRACES_SAMPLER_ARG: '0.5',
});
const env = getEnv();
assert.deepStrictEqual(env.OTEL_NO_PATCH_MODULES, ['a', 'b', 'c']);
assert.strictEqual(env.OTEL_LOG_LEVEL, DiagLogLevel.ERROR);
assert.strictEqual(env.OTEL_SAMPLING_PROBABILITY, 0.5);
assert.strictEqual(env.OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 10);
assert.strictEqual(env.OTEL_SPAN_EVENT_COUNT_LIMIT, 20);
assert.strictEqual(env.OTEL_SPAN_LINK_COUNT_LIMIT, 30);
Expand All @@ -117,20 +117,8 @@ describe('environment', () => {
assert.strictEqual(env.OTEL_RESOURCE_ATTRIBUTES, '<attrs>');
assert.strictEqual(env.OTEL_BSP_MAX_EXPORT_BATCH_SIZE, 40);
assert.strictEqual(env.OTEL_BSP_SCHEDULE_DELAY, 50);
});

it('should match invalid values to closest valid equivalent', () => {
mockEnvironment({
OTEL_SAMPLING_PROBABILITY: '-0.1',
});
const minEnv = getEnv();
assert.strictEqual(minEnv.OTEL_SAMPLING_PROBABILITY, 0);

mockEnvironment({
OTEL_SAMPLING_PROBABILITY: '1.1',
});
const maxEnv = getEnv();
assert.strictEqual(maxEnv.OTEL_SAMPLING_PROBABILITY, 1);
assert.strictEqual(env.OTEL_TRACES_SAMPLER, 'always_on');
assert.strictEqual(env.OTEL_TRACES_SAMPLER_ARG, '0.5');
});

it('should parse OTEL_LOG_LEVEL despite casing', () => {
Expand Down Expand Up @@ -158,12 +146,12 @@ describe('environment', () => {
it('should remove a mock environment', () => {
mockEnvironment({
OTEL_LOG_LEVEL: 'DEBUG',
OTEL_SAMPLING_PROBABILITY: 0.5,
OTEL_TRACES_SAMPLER: 'always_off',
});
removeMockEnvironment();
const env = getEnv();
assert.strictEqual(env.OTEL_LOG_LEVEL, DiagLogLevel.INFO);
assert.strictEqual(env.OTEL_SAMPLING_PROBABILITY, 1);
assert.strictEqual(env.OTEL_TRACES_SAMPLER, 'parentbased_always_on');
});
});
});
3 changes: 1 addition & 2 deletions packages/opentelemetry-tracing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ span.end();

Tracing configuration is a merge of user supplied configuration with both the default
configuration as specified in [config.ts](./src/config.ts) and an
environmentally configurable (via `OTEL_SAMPLING_PROBABILITY`) probability
sampler delegate of a [ParentBased](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#parentbased) sampler.
environmentally configurable sampling (via `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG`).

## Example

Expand Down
78 changes: 76 additions & 2 deletions packages/opentelemetry-tracing/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@
* limitations under the License.
*/

import { AlwaysOnSampler, getEnv } from '@opentelemetry/core';
import { diag, Sampler } from '@opentelemetry/api';
import {
AlwaysOffSampler,
AlwaysOnSampler,
getEnv,
ParentBasedSampler,
TraceIdRatioBasedSampler,
} from '@opentelemetry/core';
import { ENVIRONMENT } from '@opentelemetry/core/src/utils/environment';

const env = getEnv();

/**
* Default configuration. For fields with primitive values, any user-provided
Expand All @@ -23,10 +33,74 @@ import { AlwaysOnSampler, getEnv } from '@opentelemetry/core';
* used to extend the default value.
*/
export const DEFAULT_CONFIG = {
sampler: new AlwaysOnSampler(),
sampler: buildSamplerFromEnv(env),
traceParams: {
numberOfAttributesPerSpan: getEnv().OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
numberOfLinksPerSpan: getEnv().OTEL_SPAN_LINK_COUNT_LIMIT,
numberOfEventsPerSpan: getEnv().OTEL_SPAN_EVENT_COUNT_LIMIT,
},
};

/**
* Based on environment, builds a sampler, complies with specification.
* @param env optional, by default uses getEnv(), but allows passing a value to reuse parsed environment
*/
export function buildSamplerFromEnv(
env: Required<ENVIRONMENT> = getEnv()
): Sampler {
switch (env.OTEL_TRACES_SAMPLER) {
obecny marked this conversation as resolved.
Show resolved Hide resolved
case 'always_on':
return new AlwaysOnSampler();
case 'always_off':
return new AlwaysOffSampler();
case 'parentbased_always_on':
return new ParentBasedSampler({
root: new AlwaysOnSampler(),
});
case 'parentbased_always_off':
return new ParentBasedSampler({
root: new AlwaysOffSampler(),
});
case 'traceidratio':
return new TraceIdRatioBasedSampler(getSamplerProbabilityFromEnv(env));
case 'parentbased_traceidratio':
return new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(getSamplerProbabilityFromEnv(env)),
});
default:
diag.error(
`OTEL_TRACES_SAMPLER value "${env.OTEL_TRACES_SAMPLER} invalid, defaulting to always_on".`
);
return new AlwaysOnSampler();
}
}

function getSamplerProbabilityFromEnv(
env: Required<ENVIRONMENT>
): number | undefined {
if (
env.OTEL_TRACES_SAMPLER_ARG === undefined ||
env.OTEL_TRACES_SAMPLER_ARG === ''
) {
diag.error('"OTEL_TRACES_SAMPLER_ARG is empty, defaulting to 1.');
vmarchaud marked this conversation as resolved.
Show resolved Hide resolved
return 1;
}

const probability = Number(env.OTEL_TRACES_SAMPLER_ARG);

if (isNaN(probability)) {
diag.error(
`"${env.OTEL_TRACES_SAMPLER_ARG}" was given as OTEL_TRACES_SAMPLER_ARG, but is invalid.`
);
return undefined;
vmarchaud marked this conversation as resolved.
Show resolved Hide resolved
}

if (probability < 0 || probability > 1) {
diag.error(
`OTEL_TRACES_SAMPLER_ARG value "${env.OTEL_TRACES_SAMPLER_ARG}" out of range ([0..1]).`
);
return undefined;
}

return probability;
}
20 changes: 5 additions & 15 deletions packages/opentelemetry-tracing/src/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,22 @@
* limitations under the License.
*/

import { DEFAULT_CONFIG } from './config';
import { buildSamplerFromEnv, DEFAULT_CONFIG } from './config';
import { TracerConfig } from './types';
import {
ParentBasedSampler,
TraceIdRatioBasedSampler,
getEnv,
} from '@opentelemetry/core';

/**
* Function to merge Default configuration (as specified in './config') with
* user provided configurations.
*/
export function mergeConfig(userConfig: TracerConfig) {
const otelSamplingProbability = getEnv().OTEL_SAMPLING_PROBABILITY;
const perInstanceDefaults: Partial<TracerConfig> = {
sampler: buildSamplerFromEnv(),
};

const target = Object.assign(
{},
DEFAULT_CONFIG,
// use default AlwaysOnSampler if otelSamplingProbability is 1
otelSamplingProbability !== undefined && otelSamplingProbability < 1
? {
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(otelSamplingProbability),
}),
}
: {},
perInstanceDefaults,
userConfig
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ describe('BasicTracerProvider', () => {
});

it('should start a span with name and with invalid parent span', () => {
const tracer = new BasicTracerProvider().getTracer('default');
const tracer = new BasicTracerProvider({
sampler: new AlwaysOnSampler(),
}).getTracer('default');
const span = tracer.startSpan(
'my-span',
{},
Expand Down
111 changes: 49 additions & 62 deletions packages/opentelemetry-tracing/test/Tracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import {

describe('Tracer', () => {
const tracerProvider = new BasicTracerProvider();
const envSource = (typeof window !== 'undefined'
? window
: process.env) as any;

class TestSampler implements Sampler {
shouldSample() {
Expand All @@ -47,9 +50,8 @@ describe('Tracer', () => {
}

afterEach(() => {
if (typeof process !== 'undefined' && process.release.name === 'node') {
delete process.env.OTEL_SAMPLING_PROBABILITY;
}
delete envSource.OTEL_TRACES_SAMPLER;
delete envSource.OTEL_TRACES_SAMPLER_ARG;
});

it('should create a Tracer instance', () => {
Expand All @@ -61,13 +63,16 @@ describe('Tracer', () => {
assert.ok(tracer instanceof Tracer);
});

it('should use an AlwaysOnSampler by default', () => {
it('should use an ParentBasedSampler by default', () => {
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
assert.strictEqual(tracer['_sampler'].toString(), 'AlwaysOnSampler');
assert.strictEqual(
tracer['_sampler'].toString(),
'ParentBased{root=AlwaysOnSampler, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}'
);
});

it('should respect NO_RECORD sampling result', () => {
Expand Down Expand Up @@ -174,63 +179,45 @@ describe('Tracer', () => {
assert.strictEqual((span as Span).parentSpanId, undefined);
});

if (typeof process !== 'undefined' && process.release.name === 'node') {
it('should sample a trace when OTEL_SAMPLING_PROBABILITY is invalid', () => {
process.env.OTEL_SAMPLING_PROBABILITY = 'invalid value';
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
const span = tracer.startSpan('my-span');
const context = span.context();
assert.strictEqual(context.traceFlags, TraceFlags.SAMPLED);
span.end();
});
}

if (typeof process !== 'undefined' && process.release.name === 'node') {
it('should sample a trace when OTEL_SAMPLING_PROBABILITY is greater than 1', () => {
process.env.OTEL_SAMPLING_PROBABILITY = '2';
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
const span = tracer.startSpan('my-span');
const context = span.context();
assert.strictEqual(context.traceFlags, TraceFlags.SAMPLED);
span.end();
});
}
it('should sample a trace when OTEL_TRACES_SAMPLER_ARG is unset', () => {
envSource.OTEL_TRACES_SAMPLER = 'traceidratio';
envSource.OTEL_TRACES_SAMPLER_ARG = '';
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
const span = tracer.startSpan('my-span');
const context = span.context();
assert.strictEqual(context.traceFlags, TraceFlags.SAMPLED);
span.end();
});

if (typeof process !== 'undefined' && process.release.name === 'node') {
it('should not sample a trace when OTEL_SAMPLING_PROBABILITY is 0', () => {
process.env.OTEL_SAMPLING_PROBABILITY = '0';
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
const span = tracer.startSpan('my-span');
const context = span.context();
assert.strictEqual(context.traceFlags, TraceFlags.NONE);
span.end();
});
}
it('should not sample a trace when OTEL_TRACES_SAMPLER_ARG is out of range', () => {
envSource.OTEL_TRACES_SAMPLER = 'traceidratio';
envSource.OTEL_TRACES_SAMPLER_ARG = '2';
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
const span = tracer.startSpan('my-span');
const context = span.context();
assert.strictEqual(context.traceFlags, TraceFlags.NONE);
span.end();
});

if (typeof process !== 'undefined' && process.release.name === 'node') {
it('should not sample a trace when OTEL_SAMPLING_PROBABILITY is less than 0', () => {
process.env.OTEL_SAMPLING_PROBABILITY = '-1';
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
const span = tracer.startSpan('my-span');
const context = span.context();
assert.strictEqual(context.traceFlags, TraceFlags.NONE);
span.end();
});
}
it('should not sample a trace when OTEL_TRACES_SAMPLER_ARG is 0', () => {
envSource.OTEL_TRACES_SAMPLER = 'traceidratio';
envSource.OTEL_TRACES_SAMPLER_ARG = '0';
const tracer = new Tracer(
{ name: 'default', version: '0.0.1' },
{},
tracerProvider
);
const span = tracer.startSpan('my-span');
const context = span.context();
assert.strictEqual(context.traceFlags, TraceFlags.NONE);
span.end();
});
});
Loading