Skip to content

Commit e37f96a

Browse files
authored
feat: spec compliant sampling result support (#1058)
1 parent 65b5ba2 commit e37f96a

File tree

7 files changed

+218
-36
lines changed

7 files changed

+218
-36
lines changed

packages/opentelemetry-api/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export * from './trace/NoopSpan';
3939
export * from './trace/NoopTracer';
4040
export * from './trace/NoopTracerProvider';
4141
export * from './trace/Sampler';
42+
export * from './trace/SamplingResult';
4243
export * from './trace/span_context';
4344
export * from './trace/span_kind';
4445
export * from './trace/span';

packages/opentelemetry-api/src/trace/Sampler.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616

1717
import { SpanContext } from './span_context';
18+
import { SpanKind } from './span_kind';
19+
import { Attributes } from './attributes';
20+
import { Link } from './link';
21+
import { SamplingResult } from './SamplingResult';
1822

1923
/**
2024
* This interface represent a sampler. Sampling is a mechanism to control the
@@ -25,12 +29,26 @@ export interface Sampler {
2529
/**
2630
* Checks whether span needs to be created and tracked.
2731
*
28-
* TODO: Consider to add required arguments https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sampling-api.md#shouldsample
29-
* @param [parentContext] Parent span context. Typically taken from the wire.
32+
* @param parentContext Parent span context. Typically taken from the wire.
3033
* Can be null.
31-
* @returns whether span should be sampled or not.
34+
* @param traceId of the span to be created. It can be different from the
35+
* traceId in the {@link SpanContext}. Typically in situations when the
36+
* span to be created starts a new trace.
37+
* @param spanName of the span to be created.
38+
* @param spanKind of the span to be created.
39+
* @param attributes Initial set of Attributes for the Span being constructed.
40+
* @param links Collection of links that will be associated with the Span to
41+
* be created. Typically useful for batch operations.
42+
* @returns a {@link SamplingResult}.
3243
*/
33-
shouldSample(parentContext?: SpanContext): boolean;
44+
shouldSample(
45+
parentContext: SpanContext | undefined,
46+
traceId: string,
47+
spanName: string,
48+
spanKind: SpanKind,
49+
attributes: Attributes,
50+
links: Link[]
51+
): SamplingResult;
3452

3553
/** Returns the sampler name or short description with the configuration. */
3654
toString(): string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*!
2+
* Copyright 2020, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Attributes } from './attributes';
18+
19+
/**
20+
* A sampling decision that determines how a {@link Span} will be recorded
21+
* and collected.
22+
*/
23+
export enum SamplingDecision {
24+
/**
25+
* `Span.isRecording() === false`, span will not be recorded and all events
26+
* and attributes will be dropped.
27+
*/
28+
NOT_RECORD,
29+
/**
30+
* `Span.isRecording() === true`, but `Sampled` flag in {@link TraceFlags}
31+
* MUST NOT be set.
32+
*/
33+
RECORD,
34+
/**
35+
* `Span.isRecording() === true` AND `Sampled` flag in {@link TraceFlags}
36+
* MUST be set.
37+
*/
38+
RECORD_AND_SAMPLED,
39+
}
40+
41+
/**
42+
* A sampling result contains a decision for a {@link Span} and additional
43+
* attributes the sampler would like to added to the Span.
44+
*/
45+
export interface SamplingResult {
46+
/**
47+
* A sampling decision, refer to {@link SamplingDecision} for details.
48+
*/
49+
decision: SamplingDecision;
50+
/**
51+
* The list of attributes returned by SamplingResult MUST be immutable.
52+
* Caller may call {@link Sampler}.shouldSample any number of times and
53+
* can safely cache the returned value.
54+
*/
55+
attributes?: Readonly<Attributes>;
56+
}

packages/opentelemetry-core/src/trace/sampler/ProbabilitySampler.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Sampler, SpanContext, TraceFlags } from '@opentelemetry/api';
17+
import {
18+
Sampler,
19+
SpanContext,
20+
TraceFlags,
21+
SamplingDecision,
22+
} from '@opentelemetry/api';
1823

1924
/** Sampler that samples a given fraction of traces. */
2025
export class ProbabilitySampler implements Sampler {
@@ -25,13 +30,19 @@ export class ProbabilitySampler implements Sampler {
2530
shouldSample(parentContext?: SpanContext) {
2631
// Respect the parent sampling decision if there is one
2732
if (parentContext && typeof parentContext.traceFlags !== 'undefined') {
28-
return (
29-
(TraceFlags.SAMPLED & parentContext.traceFlags) === TraceFlags.SAMPLED
30-
);
33+
return {
34+
decision:
35+
(TraceFlags.SAMPLED & parentContext.traceFlags) === TraceFlags.SAMPLED
36+
? SamplingDecision.RECORD_AND_SAMPLED
37+
: SamplingDecision.NOT_RECORD,
38+
};
3139
}
32-
if (this._probability >= 1.0) return true;
33-
else if (this._probability <= 0) return false;
34-
return Math.random() < this._probability;
40+
return {
41+
decision:
42+
Math.random() < this._probability
43+
? SamplingDecision.RECORD_AND_SAMPLED
44+
: SamplingDecision.NOT_RECORD,
45+
};
3546
}
3647

3748
toString(): string {

packages/opentelemetry-core/test/trace/ProbabilitySampler.test.ts

+36-13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import * as assert from 'assert';
18+
import * as api from '@opentelemetry/api';
1819
import {
1920
ProbabilitySampler,
2021
ALWAYS_SAMPLER,
@@ -24,60 +25,82 @@ import {
2425
describe('ProbabilitySampler', () => {
2526
it('should return a always sampler for 1', () => {
2627
const sampler = new ProbabilitySampler(1);
27-
assert.strictEqual(sampler.shouldSample(), true);
28+
assert.deepStrictEqual(sampler.shouldSample(), {
29+
decision: api.SamplingDecision.RECORD_AND_SAMPLED,
30+
});
2831
});
2932

3033
it('should return a always sampler for >1', () => {
3134
const sampler = new ProbabilitySampler(100);
32-
assert.strictEqual(sampler.shouldSample(), true);
35+
assert.deepStrictEqual(sampler.shouldSample(), {
36+
decision: api.SamplingDecision.RECORD_AND_SAMPLED,
37+
});
3338
assert.strictEqual(sampler.toString(), 'ProbabilitySampler{1}');
3439
});
3540

3641
it('should return a never sampler for 0', () => {
3742
const sampler = new ProbabilitySampler(0);
38-
assert.strictEqual(sampler.shouldSample(), false);
43+
assert.deepStrictEqual(sampler.shouldSample(), {
44+
decision: api.SamplingDecision.NOT_RECORD,
45+
});
3946
});
4047

4148
it('should return a never sampler for <0', () => {
4249
const sampler = new ProbabilitySampler(-1);
43-
assert.strictEqual(sampler.shouldSample(), false);
50+
assert.deepStrictEqual(sampler.shouldSample(), {
51+
decision: api.SamplingDecision.NOT_RECORD,
52+
});
4453
});
4554

4655
it('should sample according to the probability', () => {
4756
Math.random = () => 1 / 10;
4857
const sampler = new ProbabilitySampler(0.2);
49-
assert.strictEqual(sampler.shouldSample(), true);
58+
assert.deepStrictEqual(sampler.shouldSample(), {
59+
decision: api.SamplingDecision.RECORD_AND_SAMPLED,
60+
});
5061
assert.strictEqual(sampler.toString(), 'ProbabilitySampler{0.2}');
5162

5263
Math.random = () => 5 / 10;
53-
assert.strictEqual(sampler.shouldSample(), false);
64+
assert.deepStrictEqual(sampler.shouldSample(), {
65+
decision: api.SamplingDecision.NOT_RECORD,
66+
});
5467
});
5568

56-
it('should return true for ALWAYS_SAMPLER', () => {
57-
assert.strictEqual(ALWAYS_SAMPLER.shouldSample(), true);
69+
it('should return api.SamplingDecision.RECORD_AND_SAMPLED for ALWAYS_SAMPLER', () => {
70+
assert.deepStrictEqual(ALWAYS_SAMPLER.shouldSample(), {
71+
decision: api.SamplingDecision.RECORD_AND_SAMPLED,
72+
});
5873
assert.strictEqual(ALWAYS_SAMPLER.toString(), 'ProbabilitySampler{1}');
5974
});
6075

61-
it('should return false for NEVER_SAMPLER', () => {
62-
assert.strictEqual(NEVER_SAMPLER.shouldSample(), false);
76+
it('should return decision: api.SamplingDecision.NOT_RECORD for NEVER_SAMPLER', () => {
77+
assert.deepStrictEqual(NEVER_SAMPLER.shouldSample(), {
78+
decision: api.SamplingDecision.NOT_RECORD,
79+
});
6380
assert.strictEqual(NEVER_SAMPLER.toString(), 'ProbabilitySampler{0}');
6481
});
6582

6683
it('should handle NaN', () => {
6784
const sampler = new ProbabilitySampler(NaN);
68-
assert.strictEqual(sampler.shouldSample(), false);
85+
assert.deepStrictEqual(sampler.shouldSample(), {
86+
decision: api.SamplingDecision.NOT_RECORD,
87+
});
6988
assert.strictEqual(sampler.toString(), 'ProbabilitySampler{0}');
7089
});
7190

7291
it('should handle -NaN', () => {
7392
const sampler = new ProbabilitySampler(-NaN);
74-
assert.strictEqual(sampler.shouldSample(), false);
93+
assert.deepStrictEqual(sampler.shouldSample(), {
94+
decision: api.SamplingDecision.NOT_RECORD,
95+
});
7596
assert.strictEqual(sampler.toString(), 'ProbabilitySampler{0}');
7697
});
7798

7899
it('should handle undefined', () => {
79100
const sampler = new ProbabilitySampler(undefined);
80-
assert.strictEqual(sampler.shouldSample(), false);
101+
assert.deepStrictEqual(sampler.shouldSample(), {
102+
decision: api.SamplingDecision.NOT_RECORD,
103+
});
81104
assert.strictEqual(sampler.toString(), 'ProbabilitySampler{0}');
82105
});
83106
});

packages/opentelemetry-tracing/src/Tracer.ts

+22-12
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ export class Tracer implements api.Tracer {
6666
context = api.context.active()
6767
): api.Span {
6868
const parentContext = getParent(options, context);
69-
// make sampling decision
70-
const samplingDecision = this._sampler.shouldSample(parentContext);
7169
const spanId = randomSpanId();
7270
let traceId;
7371
let traceState;
@@ -79,28 +77,40 @@ export class Tracer implements api.Tracer {
7977
traceId = parentContext.traceId;
8078
traceState = parentContext.traceState;
8179
}
82-
const traceFlags = samplingDecision
83-
? api.TraceFlags.SAMPLED
84-
: api.TraceFlags.NONE;
80+
const spanKind = options.kind ?? api.SpanKind.INTERNAL;
81+
const links = options.links ?? [];
82+
const attributes = { ...this._defaultAttributes, ...options.attributes };
83+
// make sampling decision
84+
const samplingResult = this._sampler.shouldSample(
85+
parentContext,
86+
traceId,
87+
name,
88+
spanKind,
89+
attributes,
90+
links
91+
);
92+
93+
const traceFlags =
94+
samplingResult.decision === api.SamplingDecision.RECORD_AND_SAMPLED
95+
? api.TraceFlags.SAMPLED
96+
: api.TraceFlags.NONE;
8597
const spanContext = { traceId, spanId, traceFlags, traceState };
86-
if (!samplingDecision) {
87-
this.logger.debug('Sampling is off, starting no recording span');
98+
if (samplingResult.decision === api.SamplingDecision.NOT_RECORD) {
99+
this.logger.debug('Recording is off, starting no recording span');
88100
return new NoRecordingSpan(spanContext);
89101
}
90102

91103
const span = new Span(
92104
this,
93105
name,
94106
spanContext,
95-
options.kind || api.SpanKind.INTERNAL,
107+
spanKind,
96108
parentContext ? parentContext.spanId : undefined,
97-
options.links || [],
109+
links,
98110
options.startTime
99111
);
100112
// Set default attributes
101-
span.setAttributes(
102-
Object.assign({}, this._defaultAttributes, options.attributes)
103-
);
113+
span.setAttributes(Object.assign(attributes, samplingResult.attributes));
104114
return span;
105115
}
106116

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*!
2+
* Copyright 2020, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import { NoopSpan, Sampler, SamplingDecision } from '@opentelemetry/api';
19+
import { BasicTracerProvider, Tracer, Span } from '../src';
20+
import { NoopLogger, ALWAYS_SAMPLER, NEVER_SAMPLER } from '@opentelemetry/core';
21+
22+
describe('Tracer', () => {
23+
const tracerProvider = new BasicTracerProvider({
24+
logger: new NoopLogger(),
25+
});
26+
27+
class TestSampler implements Sampler {
28+
shouldSample() {
29+
return {
30+
decision: SamplingDecision.RECORD_AND_SAMPLED,
31+
attributes: {
32+
testAttribute: 'foobar',
33+
},
34+
};
35+
}
36+
}
37+
38+
it('should create a Tracer instance', () => {
39+
const tracer = new Tracer({}, tracerProvider);
40+
assert.ok(tracer instanceof Tracer);
41+
});
42+
43+
it('should respect NO_RECORD sampling result', () => {
44+
const tracer = new Tracer({ sampler: NEVER_SAMPLER }, tracerProvider);
45+
const span = tracer.startSpan('span1');
46+
assert.ok(span instanceof NoopSpan);
47+
span.end();
48+
});
49+
50+
it('should respect RECORD_AND_SAMPLE sampling result', () => {
51+
const tracer = new Tracer({ sampler: ALWAYS_SAMPLER }, tracerProvider);
52+
const span = tracer.startSpan('span2');
53+
assert.ok(!(span instanceof NoopSpan));
54+
span.end();
55+
});
56+
57+
it('should start a span with attributes in sampling result', () => {
58+
const tracer = new Tracer({ sampler: new TestSampler() }, tracerProvider);
59+
const span = tracer.startSpan('span3');
60+
assert.strictEqual((span as Span).attributes.testAttribute, 'foobar');
61+
span.end();
62+
});
63+
});

0 commit comments

Comments
 (0)