Skip to content

Commit be720b4

Browse files
authored
feat: implement parent based sampler (#1577)
1 parent 4b5b023 commit be720b4

File tree

7 files changed

+214
-90
lines changed

7 files changed

+214
-90
lines changed

packages/opentelemetry-core/README.md

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,29 +108,72 @@ const tracerProvider = new NodeTracerProvider({
108108
});
109109
```
110110

111-
#### Probability
111+
#### TraceIdRatioBased
112112

113-
Samples a configurable percentage of traces, and additionally samples any trace that was sampled upstream.
113+
Samples some percentage of traces, calculated deterministically using the trace ID.
114+
Any trace that would be sampled at a given percentage will also be sampled at any higher percentage.
115+
116+
The `TraceIDRatioSampler` may be used with the `ParentBasedSampler` to respect the sampled flag of an incoming trace.
114117

115118
```js
116119
const { NodeTracerProvider } = require("@opentelemetry/node");
117120
const { TraceIdRatioBasedSampler } = require("@opentelemetry/core");
118121

119122
const tracerProvider = new NodeTracerProvider({
120-
sampler: new TraceIdRatioBasedSampler(0.5)
123+
// See details of ParentBasedSampler below
124+
sampler: new ParentBasedSampler({
125+
// Trace ID Ratio Sampler accepts a positional argument
126+
// which represents the percentage of traces which should
127+
// be sampled.
128+
root: new TraceIdRatioBasedSampler(0.5)
129+
});
121130
});
122131
```
123132

124-
#### ParentOrElse
133+
#### ParentBasedSampler
134+
135+
- This is a composite sampler. `ParentBased` helps distinguished between the
136+
following cases:
137+
- No parent (root span).
138+
- Remote parent with `sampled` flag `true`
139+
- Remote parent with `sampled` flag `false`
140+
- Local parent with `sampled` flag `true`
141+
- Local parent with `sampled` flag `false`
142+
143+
Required parameters:
144+
145+
- `root(Sampler)` - Sampler called for spans with no parent (root spans)
146+
147+
Optional parameters:
148+
149+
- `remoteParentSampled(Sampler)` (default: `AlwaysOn`)
150+
- `remoteParentNotSampled(Sampler)` (default: `AlwaysOff`)
151+
- `localParentSampled(Sampler)` (default: `AlwaysOn`)
152+
- `localParentNotSampled(Sampler)` (default: `AlwaysOff`)
125153

126-
A composite sampler that either respects the parent span's sampling decision or delegates to `delegateSampler` for root spans.
154+
|Parent| parent.isRemote() | parent.isSampled()| Invoke sampler|
155+
|--|--|--|--|
156+
|absent| n/a | n/a |`root()`|
157+
|present|true|true|`remoteParentSampled()`|
158+
|present|true|false|`remoteParentNotSampled()`|
159+
|present|false|true|`localParentSampled()`|
160+
|present|false|false|`localParentNotSampled()`|
127161

128162
```js
129163
const { NodeTracerProvider } = require("@opentelemetry/node");
130-
const { ParentOrElseSampler, AlwaysOffSampler } = require("@opentelemetry/core");
164+
const { ParentBasedSampler, AlwaysOffSampler, TraceIdRatioBasedSampler } = require("@opentelemetry/core");
131165

132166
const tracerProvider = new NodeTracerProvider({
133-
sampler: new ParentOrElseSampler(new AlwaysOffSampler())
167+
sampler: new ParentBasedSampler({
168+
// By default, the ParentBasedSampler will respect the parent span's sampling
169+
// decision. This is configurable by providing a different sampler to use
170+
// based on the situation. See configuration details above.
171+
//
172+
// This will delegate the sampling decision of all root traces (no parent)
173+
// to the TraceIdRatioBasedSampler.
174+
// See details of TraceIdRatioBasedSampler above.
175+
root: new TraceIdRatioBasedSampler(0.5)
176+
})
134177
});
135178
```
136179

packages/opentelemetry-core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export * from './platform';
3535
export * from './trace/NoRecordingSpan';
3636
export * from './trace/sampler/AlwaysOffSampler';
3737
export * from './trace/sampler/AlwaysOnSampler';
38-
export * from './trace/sampler/ParentOrElseSampler';
38+
export * from './trace/sampler/ParentBasedSampler';
3939
export * from './trace/sampler/TraceIdRatioBasedSampler';
4040
export * from './trace/TraceState';
4141
export * from './trace/IdGenerator';
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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+
import {
18+
Attributes,
19+
Link,
20+
Sampler,
21+
SamplingResult,
22+
SpanContext,
23+
SpanKind,
24+
TraceFlags,
25+
} from '@opentelemetry/api';
26+
import { AlwaysOffSampler } from './AlwaysOffSampler';
27+
import { AlwaysOnSampler } from './AlwaysOnSampler';
28+
import { globalErrorHandler } from '../../common/global-error-handler';
29+
30+
/**
31+
* A composite sampler that either respects the parent span's sampling decision
32+
* or delegates to `delegateSampler` for root spans.
33+
*/
34+
export class ParentBasedSampler implements Sampler {
35+
private _root: Sampler;
36+
private _remoteParentSampled: Sampler;
37+
private _remoteParentNotSampled: Sampler;
38+
private _localParentSampled: Sampler;
39+
private _localParentNotSampled: Sampler;
40+
41+
constructor(config: ParentBasedSamplerConfig) {
42+
this._root = config.root;
43+
44+
if (!this._root) {
45+
globalErrorHandler(
46+
new Error('ParentBasedSampler must have a root sampler configured')
47+
);
48+
this._root = new AlwaysOnSampler();
49+
}
50+
51+
this._remoteParentSampled =
52+
config.remoteParentSampled ?? new AlwaysOnSampler();
53+
this._remoteParentNotSampled =
54+
config.remoteParentNotSampled ?? new AlwaysOffSampler();
55+
this._localParentSampled =
56+
config.localParentSampled ?? new AlwaysOnSampler();
57+
this._localParentNotSampled =
58+
config.localParentNotSampled ?? new AlwaysOffSampler();
59+
}
60+
61+
shouldSample(
62+
parentContext: SpanContext | undefined,
63+
traceId: string,
64+
spanName: string,
65+
spanKind: SpanKind,
66+
attributes: Attributes,
67+
links: Link[]
68+
): SamplingResult {
69+
if (!parentContext) {
70+
return this._root.shouldSample(
71+
parentContext,
72+
traceId,
73+
spanName,
74+
spanKind,
75+
attributes,
76+
links
77+
);
78+
}
79+
80+
if (parentContext.isRemote) {
81+
if (parentContext.traceFlags & TraceFlags.SAMPLED) {
82+
return this._remoteParentSampled.shouldSample(
83+
parentContext,
84+
traceId,
85+
spanName,
86+
spanKind,
87+
attributes,
88+
links
89+
);
90+
}
91+
return this._remoteParentNotSampled.shouldSample(
92+
parentContext,
93+
traceId,
94+
spanName,
95+
spanKind,
96+
attributes,
97+
links
98+
);
99+
}
100+
101+
if (parentContext.traceFlags & TraceFlags.SAMPLED) {
102+
return this._localParentSampled.shouldSample(
103+
parentContext,
104+
traceId,
105+
spanName,
106+
spanKind,
107+
attributes,
108+
links
109+
);
110+
}
111+
112+
return this._localParentNotSampled.shouldSample(
113+
parentContext,
114+
traceId,
115+
spanName,
116+
spanKind,
117+
attributes,
118+
links
119+
);
120+
}
121+
122+
toString(): string {
123+
return `ParentBased{root=${this._root.toString()}, remoteParentSampled=${this._remoteParentSampled.toString()}, remoteParentNotSampled=${this._remoteParentNotSampled.toString()}, localParentSampled=${this._localParentSampled.toString()}, localParentNotSampled=${this._localParentNotSampled.toString()}}`;
124+
}
125+
}
126+
127+
interface ParentBasedSamplerConfig {
128+
/** Sampler called for spans with no parent */
129+
root: Sampler;
130+
/** Sampler called for spans with a remote parent which was sampled. Default AlwaysOn */
131+
remoteParentSampled?: Sampler;
132+
/** Sampler called for spans with a remote parent which was not sampled. Default AlwaysOff */
133+
remoteParentNotSampled?: Sampler;
134+
/** Sampler called for spans with a local parent which was sampled. Default AlwaysOn */
135+
localParentSampled?: Sampler;
136+
/** Sampler called for spans with a local parent which was not sampled. Default AlwaysOff */
137+
localParentNotSampled?: Sampler;
138+
}

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

Lines changed: 0 additions & 65 deletions
This file was deleted.

packages/opentelemetry-core/test/trace/ParentOrElseSampler.test.ts renamed to packages/opentelemetry-core/test/trace/ParentBasedSampler.test.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import * as assert from 'assert';
1717
import * as api from '@opentelemetry/api';
1818
import { AlwaysOnSampler } from '../../src/trace/sampler/AlwaysOnSampler';
19-
import { ParentOrElseSampler } from '../../src/trace/sampler/ParentOrElseSampler';
19+
import { ParentBasedSampler } from '../../src/trace/sampler/ParentBasedSampler';
2020
import { TraceFlags, SpanKind } from '@opentelemetry/api';
2121
import { AlwaysOffSampler } from '../../src/trace/sampler/AlwaysOffSampler';
2222
import { TraceIdRatioBasedSampler } from '../../src';
@@ -25,23 +25,31 @@ const traceId = 'd4cda95b652f4a1592b449d5929fda1b';
2525
const spanId = '6e0c63257de34c92';
2626
const spanName = 'foobar';
2727

28-
describe('ParentOrElseSampler', () => {
28+
describe('ParentBasedSampler', () => {
2929
it('should reflect sampler name with delegate sampler', () => {
30-
let sampler = new ParentOrElseSampler(new AlwaysOnSampler());
31-
assert.strictEqual(sampler.toString(), 'ParentOrElse{AlwaysOnSampler}');
30+
let sampler = new ParentBasedSampler({ root: new AlwaysOnSampler() });
31+
assert.strictEqual(
32+
sampler.toString(),
33+
'ParentBased{root=AlwaysOnSampler, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}'
34+
);
3235

33-
sampler = new ParentOrElseSampler(new AlwaysOnSampler());
34-
assert.strictEqual(sampler.toString(), 'ParentOrElse{AlwaysOnSampler}');
36+
sampler = new ParentBasedSampler({ root: new AlwaysOffSampler() });
37+
assert.strictEqual(
38+
sampler.toString(),
39+
'ParentBased{root=AlwaysOffSampler, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}'
40+
);
3541

36-
sampler = new ParentOrElseSampler(new TraceIdRatioBasedSampler(0.5));
42+
sampler = new ParentBasedSampler({
43+
root: new TraceIdRatioBasedSampler(0.5),
44+
});
3745
assert.strictEqual(
3846
sampler.toString(),
39-
'ParentOrElse{TraceIdRatioBased{0.5}}'
47+
'ParentBased{root=TraceIdRatioBased{0.5}, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}'
4048
);
4149
});
4250

4351
it('should return api.SamplingDecision.NOT_RECORD for not sampled parent while composited with AlwaysOnSampler', () => {
44-
const sampler = new ParentOrElseSampler(new AlwaysOnSampler());
52+
const sampler = new ParentBasedSampler({ root: new AlwaysOnSampler() });
4553

4654
const spanContext = {
4755
traceId,
@@ -64,7 +72,7 @@ describe('ParentOrElseSampler', () => {
6472
});
6573

6674
it('should return api.SamplingDecision.RECORD_AND_SAMPLED while composited with AlwaysOnSampler', () => {
67-
const sampler = new ParentOrElseSampler(new AlwaysOnSampler());
75+
const sampler = new ParentBasedSampler({ root: new AlwaysOnSampler() });
6876

6977
assert.deepStrictEqual(
7078
sampler.shouldSample(
@@ -82,7 +90,7 @@ describe('ParentOrElseSampler', () => {
8290
});
8391

8492
it('should return api.SamplingDecision.RECORD_AND_SAMPLED for sampled parent while composited with AlwaysOffSampler', () => {
85-
const sampler = new ParentOrElseSampler(new AlwaysOffSampler());
93+
const sampler = new ParentBasedSampler({ root: new AlwaysOffSampler() });
8694

8795
const spanContext = {
8896
traceId,
@@ -105,7 +113,7 @@ describe('ParentOrElseSampler', () => {
105113
});
106114

107115
it('should return api.SamplingDecision.RECORD_AND_SAMPLED while composited with AlwaysOffSampler', () => {
108-
const sampler = new ParentOrElseSampler(new AlwaysOffSampler());
116+
const sampler = new ParentBasedSampler({ root: new AlwaysOffSampler() });
109117

110118
assert.deepStrictEqual(
111119
sampler.shouldSample(

packages/opentelemetry-tracing/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ span.end();
4848
Tracing configuration is a merge of user supplied configuration with both the default
4949
configuration as specified in [config.ts](./src/config.ts) and an
5050
environmentally configurable (via `OTEL_SAMPLING_PROBABILITY`) probability
51-
sampler delegate of a [ParentOrElse](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#parentorelse) sampler.
51+
sampler delegate of a [ParentBased](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#parentbased) sampler.
5252

5353
## Example
5454

0 commit comments

Comments
 (0)