Skip to content

Commit 14af9c0

Browse files
committed
feat(sdk-trace-base): add AlwaysRecordSampler
1 parent 167185a commit 14af9c0

File tree

3 files changed

+206
-0
lines changed

3 files changed

+206
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
1616

1717
* feat(sdk-trace-base): implement on ending in span processor [#6024](https://github.com/open-telemetry/opentelemetry-js/pull/6024) @majanjua-amzn
1818
* note: this feature is experimental and subject to change
19+
* feat(sdk-trace-base): add AlwaysRecordSampler [#6168](https://github.com/open-telemetry/opentelemetry-js/pull/6168) @majanjua-amzn
1920

2021
### :bug: Bug Fixes
2122

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright The 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+
// Includes work from:
18+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
19+
// SPDX-License-Identifier: Apache-2.0
20+
21+
import { Context, Link, Attributes, SpanKind } from '@opentelemetry/api';
22+
import { Sampler, SamplingDecision, SamplingResult } from '../Sampler';
23+
24+
/**
25+
* This sampler will return the sampling result of the provided {@link #rootSampler}, unless the
26+
* sampling result contains the sampling decision {@link SamplingDecision.NOT_RECORD}, in which case, a
27+
* new sampling result will be returned that is functionally equivalent to the original, except that
28+
* it contains the sampling decision {@link SamplingDecision.RECORD}. This ensures that all
29+
* spans are recorded, with no change to sampling.
30+
*
31+
* <p>The intended use case of this sampler is to provide a means of sending all spans to a
32+
* processor without having an impact on the sampling rate. This may be desirable if a user wishes
33+
* to count or otherwise measure all spans produced in a service, without incurring the cost of 100%
34+
* sampling.
35+
*/
36+
export class AlwaysRecordSampler implements Sampler {
37+
private rootSampler: Sampler;
38+
39+
public static create(rootSampler: Sampler): AlwaysRecordSampler {
40+
return new AlwaysRecordSampler(rootSampler);
41+
}
42+
43+
private constructor(rootSampler: Sampler) {
44+
if (rootSampler == null) {
45+
throw new Error('rootSampler is null/undefined. It must be provided');
46+
}
47+
this.rootSampler = rootSampler;
48+
}
49+
50+
shouldSample(
51+
context: Context,
52+
traceId: string,
53+
spanName: string,
54+
spanKind: SpanKind,
55+
attributes: Attributes,
56+
links: Link[]
57+
): SamplingResult {
58+
const rootSamplerSamplingResult: SamplingResult =
59+
this.rootSampler.shouldSample(
60+
context,
61+
traceId,
62+
spanName,
63+
spanKind,
64+
attributes,
65+
links
66+
);
67+
if (rootSamplerSamplingResult.decision === SamplingDecision.NOT_RECORD) {
68+
return this.wrapResultWithRecordOnlyResult(rootSamplerSamplingResult);
69+
}
70+
return rootSamplerSamplingResult;
71+
}
72+
73+
toString(): string {
74+
return `AlwaysRecordSampler{${this.rootSampler.toString()}}`;
75+
}
76+
77+
wrapResultWithRecordOnlyResult(result: SamplingResult) {
78+
const wrappedResult: SamplingResult = {
79+
decision: SamplingDecision.RECORD,
80+
attributes: result.attributes,
81+
traceState: result.traceState,
82+
};
83+
return wrappedResult;
84+
}
85+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright The 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+
// Includes work from:
18+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
19+
// SPDX-License-Identifier: Apache-2.0
20+
21+
import * as assert from 'assert';
22+
import {
23+
Attributes,
24+
SpanKind,
25+
TraceState,
26+
context,
27+
createTraceState,
28+
} from '@opentelemetry/api';
29+
import {
30+
RandomIdGenerator,
31+
Sampler,
32+
SamplingDecision,
33+
SamplingResult,
34+
} from '../../../src';
35+
import { AlwaysOffSampler } from '../../../src/sampler/AlwaysOffSampler';
36+
import { AlwaysRecordSampler } from '../../../src/sampler/AlwaysRecordSampler';
37+
38+
let mockedSampler: Sampler;
39+
let sampler: AlwaysRecordSampler;
40+
41+
describe('AlwaysRecordSamplerTest', () => {
42+
beforeEach(() => {
43+
mockedSampler = new AlwaysOffSampler();
44+
sampler = AlwaysRecordSampler.create(mockedSampler);
45+
});
46+
47+
it('testGetDescription', () => {
48+
mockedSampler.toString = () => 'mockDescription';
49+
assert.strictEqual(
50+
sampler.toString(),
51+
'AlwaysRecordSampler{mockDescription}'
52+
);
53+
});
54+
55+
it('testRecordAndSampleSamplingDecision', () => {
56+
validateShouldSample(
57+
SamplingDecision.RECORD_AND_SAMPLED,
58+
SamplingDecision.RECORD_AND_SAMPLED
59+
);
60+
});
61+
62+
it('testRecordOnlySamplingDecision', () => {
63+
validateShouldSample(SamplingDecision.RECORD, SamplingDecision.RECORD);
64+
});
65+
66+
it('testDropSamplingDecision', () => {
67+
validateShouldSample(SamplingDecision.NOT_RECORD, SamplingDecision.RECORD);
68+
});
69+
70+
it('testCreateAlwaysRecordSamplerThrows', () => {
71+
assert.throws(() => AlwaysRecordSampler.create(null as unknown as Sampler));
72+
assert.throws(() =>
73+
AlwaysRecordSampler.create(undefined as unknown as Sampler)
74+
);
75+
});
76+
});
77+
78+
function validateShouldSample(
79+
rootDecision: SamplingDecision,
80+
expectedDecision: SamplingDecision
81+
): void {
82+
const rootResult: SamplingResult = buildRootSamplingResult(rootDecision);
83+
mockedSampler.shouldSample = () => {
84+
return rootResult;
85+
};
86+
87+
const actualResult: SamplingResult = sampler.shouldSample(
88+
context.active(),
89+
new RandomIdGenerator().generateTraceId(),
90+
'spanName',
91+
SpanKind.CLIENT,
92+
{},
93+
[]
94+
);
95+
96+
if (rootDecision === expectedDecision) {
97+
assert.strictEqual(actualResult, rootResult);
98+
assert.strictEqual(actualResult.decision, rootDecision);
99+
} else {
100+
assert.notEqual(actualResult, rootResult);
101+
assert.strictEqual(actualResult.decision, expectedDecision);
102+
}
103+
104+
assert.strictEqual(actualResult.attributes, rootResult.attributes);
105+
assert.strictEqual(actualResult.traceState, rootResult.traceState);
106+
}
107+
108+
function buildRootSamplingResult(
109+
samplingDecision: SamplingDecision
110+
): SamplingResult {
111+
const samplingAttr: Attributes = { key: SamplingDecision[samplingDecision] };
112+
const samplingTraceState: TraceState = createTraceState();
113+
samplingTraceState.set('key', SamplingDecision[samplingDecision]);
114+
const samplingResult: SamplingResult = {
115+
decision: samplingDecision,
116+
attributes: samplingAttr,
117+
traceState: samplingTraceState,
118+
};
119+
return samplingResult;
120+
}

0 commit comments

Comments
 (0)