Skip to content

Commit

Permalink
feat: implement parent based sampler (#1577)
Browse files Browse the repository at this point in the history
  • Loading branch information
dyladan authored Oct 13, 2020
1 parent 4b5b023 commit be720b4
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 90 deletions.
57 changes: 50 additions & 7 deletions packages/opentelemetry-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,29 +108,72 @@ const tracerProvider = new NodeTracerProvider({
});
```

#### Probability
#### TraceIdRatioBased

Samples a configurable percentage of traces, and additionally samples any trace that was sampled upstream.
Samples some percentage of traces, calculated deterministically using the trace ID.
Any trace that would be sampled at a given percentage will also be sampled at any higher percentage.

The `TraceIDRatioSampler` may be used with the `ParentBasedSampler` to respect the sampled flag of an incoming trace.

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

const tracerProvider = new NodeTracerProvider({
sampler: new TraceIdRatioBasedSampler(0.5)
// See details of ParentBasedSampler below
sampler: new ParentBasedSampler({
// Trace ID Ratio Sampler accepts a positional argument
// which represents the percentage of traces which should
// be sampled.
root: new TraceIdRatioBasedSampler(0.5)
});
});
```

#### ParentOrElse
#### ParentBasedSampler

- This is a composite sampler. `ParentBased` helps distinguished between the
following cases:
- No parent (root span).
- Remote parent with `sampled` flag `true`
- Remote parent with `sampled` flag `false`
- Local parent with `sampled` flag `true`
- Local parent with `sampled` flag `false`

Required parameters:

- `root(Sampler)` - Sampler called for spans with no parent (root spans)

Optional parameters:

- `remoteParentSampled(Sampler)` (default: `AlwaysOn`)
- `remoteParentNotSampled(Sampler)` (default: `AlwaysOff`)
- `localParentSampled(Sampler)` (default: `AlwaysOn`)
- `localParentNotSampled(Sampler)` (default: `AlwaysOff`)

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

```js
const { NodeTracerProvider } = require("@opentelemetry/node");
const { ParentOrElseSampler, AlwaysOffSampler } = require("@opentelemetry/core");
const { ParentBasedSampler, AlwaysOffSampler, TraceIdRatioBasedSampler } = require("@opentelemetry/core");

const tracerProvider = new NodeTracerProvider({
sampler: new ParentOrElseSampler(new AlwaysOffSampler())
sampler: new ParentBasedSampler({
// By default, the ParentBasedSampler will respect the parent span's sampling
// decision. This is configurable by providing a different sampler to use
// based on the situation. See configuration details above.
//
// This will delegate the sampling decision of all root traces (no parent)
// to the TraceIdRatioBasedSampler.
// See details of TraceIdRatioBasedSampler above.
root: new TraceIdRatioBasedSampler(0.5)
})
});
```

Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export * from './platform';
export * from './trace/NoRecordingSpan';
export * from './trace/sampler/AlwaysOffSampler';
export * from './trace/sampler/AlwaysOnSampler';
export * from './trace/sampler/ParentOrElseSampler';
export * from './trace/sampler/ParentBasedSampler';
export * from './trace/sampler/TraceIdRatioBasedSampler';
export * from './trace/TraceState';
export * from './trace/IdGenerator';
Expand Down
138 changes: 138 additions & 0 deletions packages/opentelemetry-core/src/trace/sampler/ParentBasedSampler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
Attributes,
Link,
Sampler,
SamplingResult,
SpanContext,
SpanKind,
TraceFlags,
} from '@opentelemetry/api';
import { AlwaysOffSampler } from './AlwaysOffSampler';
import { AlwaysOnSampler } from './AlwaysOnSampler';
import { globalErrorHandler } from '../../common/global-error-handler';

/**
* A composite sampler that either respects the parent span's sampling decision
* or delegates to `delegateSampler` for root spans.
*/
export class ParentBasedSampler implements Sampler {
private _root: Sampler;
private _remoteParentSampled: Sampler;
private _remoteParentNotSampled: Sampler;
private _localParentSampled: Sampler;
private _localParentNotSampled: Sampler;

constructor(config: ParentBasedSamplerConfig) {
this._root = config.root;

if (!this._root) {
globalErrorHandler(
new Error('ParentBasedSampler must have a root sampler configured')
);
this._root = new AlwaysOnSampler();
}

this._remoteParentSampled =
config.remoteParentSampled ?? new AlwaysOnSampler();
this._remoteParentNotSampled =
config.remoteParentNotSampled ?? new AlwaysOffSampler();
this._localParentSampled =
config.localParentSampled ?? new AlwaysOnSampler();
this._localParentNotSampled =
config.localParentNotSampled ?? new AlwaysOffSampler();
}

shouldSample(
parentContext: SpanContext | undefined,
traceId: string,
spanName: string,
spanKind: SpanKind,
attributes: Attributes,
links: Link[]
): SamplingResult {
if (!parentContext) {
return this._root.shouldSample(
parentContext,
traceId,
spanName,
spanKind,
attributes,
links
);
}

if (parentContext.isRemote) {
if (parentContext.traceFlags & TraceFlags.SAMPLED) {
return this._remoteParentSampled.shouldSample(
parentContext,
traceId,
spanName,
spanKind,
attributes,
links
);
}
return this._remoteParentNotSampled.shouldSample(
parentContext,
traceId,
spanName,
spanKind,
attributes,
links
);
}

if (parentContext.traceFlags & TraceFlags.SAMPLED) {
return this._localParentSampled.shouldSample(
parentContext,
traceId,
spanName,
spanKind,
attributes,
links
);
}

return this._localParentNotSampled.shouldSample(
parentContext,
traceId,
spanName,
spanKind,
attributes,
links
);
}

toString(): string {
return `ParentBased{root=${this._root.toString()}, remoteParentSampled=${this._remoteParentSampled.toString()}, remoteParentNotSampled=${this._remoteParentNotSampled.toString()}, localParentSampled=${this._localParentSampled.toString()}, localParentNotSampled=${this._localParentNotSampled.toString()}}`;
}
}

interface ParentBasedSamplerConfig {
/** Sampler called for spans with no parent */
root: Sampler;
/** Sampler called for spans with a remote parent which was sampled. Default AlwaysOn */
remoteParentSampled?: Sampler;
/** Sampler called for spans with a remote parent which was not sampled. Default AlwaysOff */
remoteParentNotSampled?: Sampler;
/** Sampler called for spans with a local parent which was sampled. Default AlwaysOn */
localParentSampled?: Sampler;
/** Sampler called for spans with a local parent which was not sampled. Default AlwaysOff */
localParentNotSampled?: Sampler;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import * as assert from 'assert';
import * as api from '@opentelemetry/api';
import { AlwaysOnSampler } from '../../src/trace/sampler/AlwaysOnSampler';
import { ParentOrElseSampler } from '../../src/trace/sampler/ParentOrElseSampler';
import { ParentBasedSampler } from '../../src/trace/sampler/ParentBasedSampler';
import { TraceFlags, SpanKind } from '@opentelemetry/api';
import { AlwaysOffSampler } from '../../src/trace/sampler/AlwaysOffSampler';
import { TraceIdRatioBasedSampler } from '../../src';
Expand All @@ -25,23 +25,31 @@ const traceId = 'd4cda95b652f4a1592b449d5929fda1b';
const spanId = '6e0c63257de34c92';
const spanName = 'foobar';

describe('ParentOrElseSampler', () => {
describe('ParentBasedSampler', () => {
it('should reflect sampler name with delegate sampler', () => {
let sampler = new ParentOrElseSampler(new AlwaysOnSampler());
assert.strictEqual(sampler.toString(), 'ParentOrElse{AlwaysOnSampler}');
let sampler = new ParentBasedSampler({ root: new AlwaysOnSampler() });
assert.strictEqual(
sampler.toString(),
'ParentBased{root=AlwaysOnSampler, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}'
);

sampler = new ParentOrElseSampler(new AlwaysOnSampler());
assert.strictEqual(sampler.toString(), 'ParentOrElse{AlwaysOnSampler}');
sampler = new ParentBasedSampler({ root: new AlwaysOffSampler() });
assert.strictEqual(
sampler.toString(),
'ParentBased{root=AlwaysOffSampler, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}'
);

sampler = new ParentOrElseSampler(new TraceIdRatioBasedSampler(0.5));
sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.5),
});
assert.strictEqual(
sampler.toString(),
'ParentOrElse{TraceIdRatioBased{0.5}}'
'ParentBased{root=TraceIdRatioBased{0.5}, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}'
);
});

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

const spanContext = {
traceId,
Expand All @@ -64,7 +72,7 @@ describe('ParentOrElseSampler', () => {
});

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

assert.deepStrictEqual(
sampler.shouldSample(
Expand All @@ -82,7 +90,7 @@ describe('ParentOrElseSampler', () => {
});

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

const spanContext = {
traceId,
Expand All @@ -105,7 +113,7 @@ describe('ParentOrElseSampler', () => {
});

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

assert.deepStrictEqual(
sampler.shouldSample(
Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry-tracing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,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 [ParentOrElse](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#parentorelse) sampler.
sampler delegate of a [ParentBased](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#parentbased) sampler.

## Example

Expand Down
Loading

0 comments on commit be720b4

Please sign in to comment.