Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2

### :rocket: Features

* feat(sampler-composite): add ComposableAnnotatingSampler and ComposableRuleBasedSampler [#6305](https://github.com/open-telemetry/opentelemetry-js/pull/6305) @trentm

### :bug: Bug Fixes

### :books: Documentation
Expand Down
4 changes: 2 additions & 2 deletions experimental/packages/sampler-composite/src/alwaysoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
import type { ComposableSampler, SamplingIntent } from './types';
import { INVALID_THRESHOLD } from './util';

const intent: SamplingIntent = {
const intent: SamplingIntent = Object.freeze({
threshold: INVALID_THRESHOLD,
thresholdReliable: false,
};
});

class ComposableAlwaysOffSampler implements ComposableSampler {
getSamplingIntent(): SamplingIntent {
Expand Down
4 changes: 2 additions & 2 deletions experimental/packages/sampler-composite/src/alwayson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
import type { ComposableSampler, SamplingIntent } from './types';
import { MIN_THRESHOLD } from './util';

const intent: SamplingIntent = {
const intent: SamplingIntent = Object.freeze({
threshold: MIN_THRESHOLD,
thresholdReliable: true,
};
});

class ComposableAlwaysOnSampler implements ComposableSampler {
getSamplingIntent(): SamplingIntent {
Expand Down
69 changes: 69 additions & 0 deletions experimental/packages/sampler-composite/src/annotating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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, Context, Link, SpanKind } from '@opentelemetry/api';
import type { ComposableSampler, SamplingIntent } from './types';

class ComposableAnnotatingSampler implements ComposableSampler {
private readonly delegate: ComposableSampler;
private readonly attributes: Attributes;
private readonly description: string;

constructor(delegate: ComposableSampler, attributes: Attributes) {
// Shallow copy `attributes` to avoid changes to the original object (at
// least top-level fields) impacting the sampler. Freeze the object so
// we can return it in `getSamplingIntent` without copying there.
this.attributes = Object.freeze({ ...attributes });
this.delegate = delegate;
this.description = `ComposableAnnotatingSampler(${delegate}, attributes)`;
}

getSamplingIntent(
context: Context,
traceId: string,
spanName: string,
spanKind: SpanKind,
attributes: Attributes,
links: Link[]
): SamplingIntent {
const intent = this.delegate.getSamplingIntent(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
return {
threshold: intent.threshold,
thresholdReliable: intent.thresholdReliable,
attributes: intent.attributes
? { ...intent.attributes, ...this.attributes }
: this.attributes,
updateTraceState: intent.updateTraceState,
};
}

toString(): string {
return this.description;
}
}

export function createComposableAnnotatingSampler(
delegate: ComposableSampler,
attributes: Attributes
): ComposableSampler {
return new ComposableAnnotatingSampler(delegate, attributes);
}
2 changes: 2 additions & 0 deletions experimental/packages/sampler-composite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ export { createComposableAlwaysOffSampler } from './alwaysoff';
export { createComposableAlwaysOnSampler } from './alwayson';
export { createComposableTraceIDRatioBasedSampler } from './traceidratio';
export { createComposableParentThresholdSampler } from './parentthreshold';
export { createComposableAnnotatingSampler } from './annotating';
export { createComposableRuleBasedSampler } from './rulebased';
export { createCompositeSampler } from './composite';
export type { ComposableSampler, SamplingIntent } from './types';
71 changes: 71 additions & 0 deletions experimental/packages/sampler-composite/src/rulebased.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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, Context, Link, SpanKind } from '@opentelemetry/api';

import type { ComposableSampler, SamplingIntent, SamplingRule } from './types';
import { INVALID_THRESHOLD } from './util';

const NON_SAMPLING_INTENT: SamplingIntent = Object.freeze({
threshold: INVALID_THRESHOLD,
thresholdReliable: false,
});

class ComposableRuleBasedSampler implements ComposableSampler {
private readonly rules: SamplingRule[];
private readonly description: string;

constructor(rules: SamplingRule[]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the good reference but I read ComposableSampler gets 2 params:

  • the rules
  • span kind (optional)

I the span kind is provided the getSamplingIntent logic changes a bit. From the docs

For calculating the SamplingIntent, if the Span kind matches the specified kind, 
or the specified kind is not given, the sampler goes through the list in the provided 
order and calls SpanMatches on Predicates passing the same arguments as 
received. If a call returns true, the result is as returned by GetSamplingIntent called 
on the corresponding Composable - no other Predicates are evaluated. If the 
SpanKind does not match, or none of the calls to SpanMatches yield true, the result 
is obtained by calling GetSamplingIntent on ConsistentAlwaysOffSampler.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed, the specifics from the OTEPs changed in some cases when making it to the spec. https://opentelemetry.io/docs/specs/otel/trace/sdk/#composablerulebased says:

Required parameters:

rules - A list of (Predicate, ComposableSampler) pairs, where Predicate is a function that evaluates whether a rule applies

Hence no separate span kind argument. Note that the predicate (type SamplingPredicate does get the spanKind, along with other arguments for its decision).

this.rules = rules;
const ruleReprs = rules.map(
rule => `(${rule[0].name || '(anonymous)'}, ${rule[1].toString()})`
);
this.description = `ComposableRuleBasedSampler([${ruleReprs.join(', ')}])`;
}

getSamplingIntent(
context: Context,
traceId: string,
spanName: string,
spanKind: SpanKind,
attributes: Attributes,
links: Link[]
): SamplingIntent {
for (const [predicate, sampler] of this.rules) {
if (predicate(context, traceId, spanName, spanKind, attributes, links)) {
return sampler.getSamplingIntent(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
}
return NON_SAMPLING_INTENT;
}

toString(): string {
return this.description;
}
}

export function createComposableRuleBasedSampler(
rules: SamplingRule[]
): ComposableSampler {
return new ComposableRuleBasedSampler(rules);
}
8 changes: 4 additions & 4 deletions experimental/packages/sampler-composite/src/traceidratio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,18 @@ class ComposableTraceIDRatioBasedSampler implements ComposableSampler {
const thresholdStr =
threshold === MAX_THRESHOLD ? 'max' : serializeTh(threshold);
if (threshold !== MAX_THRESHOLD) {
this.intent = {
this.intent = Object.freeze({
threshold: threshold,
thresholdReliable: true,
};
});
} else {
// Same as AlwaysOff, notably the threshold is not considered reliable. The spec mentions
// returning an instance of ComposableAlwaysOffSampler in this case but it seems clearer
// if the description of the sampler matches the user's request.
this.intent = {
this.intent = Object.freeze({
threshold: INVALID_THRESHOLD,
thresholdReliable: false,
};
});
}
this.description = `ComposableTraceIDRatioBasedSampler(threshold=${thresholdStr}, ratio=${ratio})`;
}
Expand Down
5 changes: 5 additions & 0 deletions experimental/packages/sampler-composite/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ export interface ComposableSampler {
/** Returns the sampler name or short description with the configuration. */
toString(): string;
}

export type SamplingPredicate = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(same thing about the reference The docs define a SpanMatches method in the predicate. So its defined as an interface rather than a function. My guess is by having

interface SamplingPredicate {
  spanMatches: (...args: Parameters<Sampler['shouldSample']>) => boolean;
}

implementors are able to provide a predicate that holds state...

Writing that last sentence I realised this can be achieved using closures so you can ignore my comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as above. The language that made it into the spec is where Predicate is a function that evaluates whether a rule applies.

...args: Parameters<Sampler['shouldSample']>
) => boolean;
export type SamplingRule = [SamplingPredicate, ComposableSampler];
87 changes: 87 additions & 0 deletions experimental/packages/sampler-composite/test/annotating.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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 * as assert from 'assert';

import { context, SpanKind } from '@opentelemetry/api';

import {
createComposableAlwaysOnSampler,
createComposableAnnotatingSampler,
} from '../src';

describe('ComposableAnnotatingSampler', () => {
const alwaysOnSampler = createComposableAlwaysOnSampler();

it('should have a description', () => {
const sampler = createComposableAnnotatingSampler(alwaysOnSampler, {});
assert.strictEqual(
sampler.toString(),
'ComposableAnnotatingSampler(ComposableAlwaysOnSampler, attributes)'
);
});

it('should add attributes to SamplingIntent', () => {
const delegateIntent = alwaysOnSampler.getSamplingIntent(
context.active(),
'unused',
'span-name',
SpanKind.SERVER,
{},
[]
);
const sampler = createComposableAnnotatingSampler(alwaysOnSampler, {
foo: 'bar',
});
const intent = sampler.getSamplingIntent(
context.active(),
'unused',
'span-name',
SpanKind.SERVER,
{},
[]
);
assert.strictEqual(intent.threshold, delegateIntent.threshold);
assert.strictEqual(
intent.thresholdReliable,
delegateIntent.thresholdReliable
);
assert.deepStrictEqual(intent.attributes, { foo: 'bar' });
});

it('should merge attributes', () => {
const sampler = createComposableAnnotatingSampler(
createComposableAnnotatingSampler(alwaysOnSampler, {
foo: 'baz',
wuz: 'here',
}),
{ foo: 'bar', spam: 'eggs' }
);
const intent = sampler.getSamplingIntent(
context.active(),
'unused',
'span-name',
SpanKind.SERVER,
{},
[]
);
assert.deepStrictEqual(intent.attributes, {
foo: 'bar', // outer annotating sampler wins
spam: 'eggs',
wuz: 'here',
});
});
});
Loading