diff --git a/packages/opentelemetry-core/README.md b/packages/opentelemetry-core/README.md index bd534ec011..5f60e17947 100644 --- a/packages/opentelemetry-core/README.md +++ b/packages/opentelemetry-core/README.md @@ -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) + }) }); ``` diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index 569e4d9553..d3a84ee651 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -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'; diff --git a/packages/opentelemetry-core/src/trace/sampler/ParentBasedSampler.ts b/packages/opentelemetry-core/src/trace/sampler/ParentBasedSampler.ts new file mode 100644 index 0000000000..98224171c4 --- /dev/null +++ b/packages/opentelemetry-core/src/trace/sampler/ParentBasedSampler.ts @@ -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; +} diff --git a/packages/opentelemetry-core/src/trace/sampler/ParentOrElseSampler.ts b/packages/opentelemetry-core/src/trace/sampler/ParentOrElseSampler.ts deleted file mode 100644 index 71495a3c70..0000000000 --- a/packages/opentelemetry-core/src/trace/sampler/ParentOrElseSampler.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { - Sampler, - SpanContext, - TraceFlags, - SamplingDecision, - SpanKind, - Attributes, - Link, - SamplingResult, -} from '@opentelemetry/api'; - -/** - * A composite sampler that either respects the parent span's sampling decision - * or delegates to `delegateSampler` for root spans. - */ -export class ParentOrElseSampler implements Sampler { - constructor(private _delegateSampler: Sampler) {} - - shouldSample( - parentContext: SpanContext | undefined, - traceId: string, - spanName: string, - spanKind: SpanKind, - attributes: Attributes, - links: Link[] - ): SamplingResult { - // Respect the parent sampling decision if there is one - if (parentContext) { - return { - decision: - (TraceFlags.SAMPLED & parentContext.traceFlags) === TraceFlags.SAMPLED - ? SamplingDecision.RECORD_AND_SAMPLED - : SamplingDecision.NOT_RECORD, - }; - } - return this._delegateSampler.shouldSample( - parentContext, - traceId, - spanName, - spanKind, - attributes, - links - ); - } - - toString(): string { - return `ParentOrElse{${this._delegateSampler.toString()}}`; - } -} diff --git a/packages/opentelemetry-core/test/trace/ParentOrElseSampler.test.ts b/packages/opentelemetry-core/test/trace/ParentBasedSampler.test.ts similarity index 67% rename from packages/opentelemetry-core/test/trace/ParentOrElseSampler.test.ts rename to packages/opentelemetry-core/test/trace/ParentBasedSampler.test.ts index 915804fcd6..ef43ce70bf 100644 --- a/packages/opentelemetry-core/test/trace/ParentOrElseSampler.test.ts +++ b/packages/opentelemetry-core/test/trace/ParentBasedSampler.test.ts @@ -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'; @@ -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, @@ -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( @@ -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, @@ -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( diff --git a/packages/opentelemetry-tracing/README.md b/packages/opentelemetry-tracing/README.md index 1de732e127..c43a28810d 100644 --- a/packages/opentelemetry-tracing/README.md +++ b/packages/opentelemetry-tracing/README.md @@ -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 diff --git a/packages/opentelemetry-tracing/src/utility.ts b/packages/opentelemetry-tracing/src/utility.ts index 15f5c832f5..98fe6fac55 100644 --- a/packages/opentelemetry-tracing/src/utility.ts +++ b/packages/opentelemetry-tracing/src/utility.ts @@ -22,7 +22,7 @@ import { } from './config'; import { TracerConfig } from './types'; import { - ParentOrElseSampler, + ParentBasedSampler, TraceIdRatioBasedSampler, getEnv, } from '@opentelemetry/core'; @@ -40,9 +40,9 @@ export function mergeConfig(userConfig: TracerConfig) { // use default AlwaysOnSampler if otelSamplingProbability is 1 otelSamplingProbability !== undefined && otelSamplingProbability < 1 ? { - sampler: new ParentOrElseSampler( - new TraceIdRatioBasedSampler(otelSamplingProbability) - ), + sampler: new ParentBasedSampler({ + root: new TraceIdRatioBasedSampler(otelSamplingProbability), + }), } : {}, userConfig