Skip to content

Commit 4fcf803

Browse files
anuraagatrentm
andauthored
feat(sampler-composite): add experimental implementation of composite sampling spec (#5839)
Co-authored-by: Trent Mick <[email protected]>
1 parent 7ff19fe commit 4fcf803

27 files changed

+1811
-0
lines changed

experimental/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
1010

1111
### :rocket: Features
1212

13+
* feat(sampler-composite): Added experimental implementations of draft composite sampling spec [#5839](https://github.com/open-telemetry/opentelemetry-js/pull/5839) @anuraaga
14+
1315
### :bug: Bug Fixes
1416

1517
* fix(instrumentation-http): respect requireParent flag when INVALID_SPAN_CONTEXT is used [#4788](https://github.com/open-telemetry/opentelemetry-js/pull/4788) @reberhardt7
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
build
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
"env": {
3+
"mocha": true,
4+
"commonjs": true,
5+
"shared-node-browser": true
6+
},
7+
...require('../../../eslint.base.js')
8+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# OpenTelemetry Composite Sampling
2+
3+
[![NPM Published Version][npm-img]][npm-url]
4+
[![Apache License][license-image]][license-image]
5+
6+
**Note: This is an experimental package under active development. New releases may include breaking changes.**
7+
8+
This package provides implementations of composite samplers that propagate sampling information across a trace.
9+
This implements the [experimental specification][probability-sampling].
10+
11+
Currently `ComposableRuleBased` and `ComposableAnnotating` are not implemented.
12+
13+
## Quick Start
14+
15+
To get started you will need to install a compatible OpenTelemetry SDK.
16+
17+
### Samplers
18+
19+
This module exports samplers that follow the general behavior of the standard SDK samplers, but ensuring
20+
it is consistent across a trace by using the tracestate header. Notably, the tracestate can be examined
21+
in exported spans to reconstruct population metrics.
22+
23+
```typescript
24+
import {
25+
createCompositeSampler,
26+
createComposableAlwaysOffSampler,
27+
createComposableAlwaysOnSampler,
28+
createComposableParentThresholdSampler,
29+
createComposableTraceIDRatioBasedSampler,
30+
} from '@opentelemetry/sampler-composite';
31+
32+
// never sample
33+
const sampler = createCompositeSampler(createComposableAlwaysOffSampler());
34+
// always sample
35+
const sampler = createCompositeSampler(createComposableAlwaysOnSampler());
36+
// follow the parent, or otherwise sample with a probability if root
37+
const sampler = createCompositeSampler(
38+
createComposableParentThresholdSampler(createComposableTraceIDRatioBasedSampler(0.3)));
39+
```
40+
41+
## Useful links
42+
43+
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
44+
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>
45+
- For help or feedback on this project, join us in [GitHub Discussions][discussions-url]
46+
47+
## License
48+
49+
Apache 2.0 - See [LICENSE][license-url] for more information.
50+
51+
[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions
52+
[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE
53+
[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat
54+
[npm-url]: https://www.npmjs.com/package/@opentelemetry/sampler-composite
55+
[npm-img]: https://badge.fury.io/js/%40opentelemetry%sampler-composite.svg
56+
57+
[probability-sampling]: https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"name": "@opentelemetry/sampler-composite",
3+
"private": false,
4+
"publishConfig": {
5+
"access": "public"
6+
},
7+
"version": "0.205.0",
8+
"description": "Composite samplers for OpenTelemetry tracing",
9+
"module": "build/esm/index.js",
10+
"esnext": "build/esnext/index.js",
11+
"types": "build/src/index.d.ts",
12+
"main": "build/src/index.js",
13+
"repository": "open-telemetry/opentelemetry-js",
14+
"scripts": {
15+
"prepublishOnly": "npm run compile",
16+
"compile": "tsc --build",
17+
"clean": "tsc --build --clean",
18+
"test": "nyc mocha 'test/**/*.test.ts'",
19+
"tdd": "npm run test -- --watch-extensions ts --watch",
20+
"lint": "eslint . --ext .ts",
21+
"lint:fix": "eslint . --ext .ts --fix",
22+
"version": "node ../../../scripts/version-update.js",
23+
"watch": "tsc --build --watch",
24+
"precompile": "lerna run version --scope @opentelemetry/sampler-composite --include-dependencies",
25+
"prewatch": "npm run precompile",
26+
"peer-api-check": "node ../../../scripts/peer-api-check.js",
27+
"align-api-deps": "node ../../../scripts/align-api-deps.js"
28+
},
29+
"keywords": [
30+
"opentelemetry",
31+
"nodejs",
32+
"sampling",
33+
"tracing"
34+
],
35+
"author": "OpenTelemetry Authors",
36+
"license": "Apache-2.0",
37+
"engines": {
38+
"node": "^18.19.0 || >=20.6.0"
39+
},
40+
"files": [
41+
"build/esm/**/*.js",
42+
"build/esm/**/*.js.map",
43+
"build/esm/**/*.d.ts",
44+
"build/esnext/**/*.js",
45+
"build/esnext/**/*.js.map",
46+
"build/esnext/**/*.d.ts",
47+
"build/src/**/*.js",
48+
"build/src/**/*.js.map",
49+
"build/src/**/*.d.ts",
50+
"LICENSE",
51+
"README.md"
52+
],
53+
"peerDependencies": {
54+
"@opentelemetry/api": "^1.3.0"
55+
},
56+
"devDependencies": {
57+
"@opentelemetry/api": "1.9.0",
58+
"@types/mocha": "10.0.10",
59+
"@types/node": "18.6.5",
60+
"lerna": "6.6.2",
61+
"mocha": "11.1.0",
62+
"nyc": "17.1.0"
63+
},
64+
"dependencies": {
65+
"@opentelemetry/core": "2.0.1",
66+
"@opentelemetry/sdk-trace-base": "2.0.1"
67+
},
68+
"homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/sampler-composite",
69+
"sideEffects": false
70+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
import type { ComposableSampler, SamplingIntent } from './types';
17+
import { INVALID_THRESHOLD } from './util';
18+
19+
const intent: SamplingIntent = {
20+
threshold: INVALID_THRESHOLD,
21+
thresholdReliable: false,
22+
};
23+
24+
class ComposableAlwaysOffSampler implements ComposableSampler {
25+
getSamplingIntent(): SamplingIntent {
26+
return intent;
27+
}
28+
29+
toString(): string {
30+
return 'ComposableAlwaysOffSampler';
31+
}
32+
}
33+
34+
const _sampler = new ComposableAlwaysOffSampler();
35+
36+
/**
37+
* Returns a composable sampler that does not sample any span.
38+
*/
39+
export function createComposableAlwaysOffSampler(): ComposableSampler {
40+
return _sampler;
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
import type { ComposableSampler, SamplingIntent } from './types';
17+
import { MIN_THRESHOLD } from './util';
18+
19+
const intent: SamplingIntent = {
20+
threshold: MIN_THRESHOLD,
21+
thresholdReliable: true,
22+
};
23+
24+
class ComposableAlwaysOnSampler implements ComposableSampler {
25+
getSamplingIntent(): SamplingIntent {
26+
return intent;
27+
}
28+
29+
toString(): string {
30+
return 'ComposableAlwaysOnSampler';
31+
}
32+
}
33+
34+
const _sampler = new ComposableAlwaysOnSampler();
35+
36+
/**
37+
* Returns a composable sampler that samples all span.
38+
*/
39+
export function createComposableAlwaysOnSampler(): ComposableSampler {
40+
return _sampler;
41+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
import {
17+
Context,
18+
SpanKind,
19+
Attributes,
20+
Link,
21+
TraceState,
22+
trace,
23+
} from '@opentelemetry/api';
24+
import { TraceState as CoreTraceState } from '@opentelemetry/core';
25+
import {
26+
Sampler,
27+
SamplingDecision,
28+
SamplingResult,
29+
} from '@opentelemetry/sdk-trace-base';
30+
import { ComposableSampler } from './types';
31+
import { parseOtelTraceState, serializeTraceState } from './tracestate';
32+
import {
33+
INVALID_THRESHOLD,
34+
isValidRandomValue,
35+
isValidThreshold,
36+
} from './util';
37+
38+
class CompositeSampler implements Sampler {
39+
constructor(private readonly delegate: ComposableSampler) {}
40+
41+
shouldSample(
42+
context: Context,
43+
traceId: string,
44+
spanName: string,
45+
spanKind: SpanKind,
46+
attributes: Attributes,
47+
links: Link[]
48+
): SamplingResult {
49+
const spanContext = trace.getSpanContext(context);
50+
51+
const traceState = spanContext?.traceState;
52+
let otTraceState = parseOtelTraceState(traceState);
53+
54+
const intent = this.delegate.getSamplingIntent(
55+
context,
56+
traceId,
57+
spanName,
58+
spanKind,
59+
attributes,
60+
links
61+
);
62+
63+
let adjustedCountCorrect = false;
64+
let sampled = false;
65+
if (isValidThreshold(intent.threshold)) {
66+
adjustedCountCorrect = intent.thresholdReliable;
67+
let randomness: bigint;
68+
if (isValidRandomValue(otTraceState.randomValue)) {
69+
randomness = otTraceState.randomValue;
70+
} else {
71+
// Use last 56 bits of trace_id as randomness.
72+
randomness = BigInt(`0x${traceId.slice(-14)}`);
73+
}
74+
sampled = intent.threshold <= randomness;
75+
}
76+
77+
const decision = sampled
78+
? SamplingDecision.RECORD_AND_SAMPLED
79+
: SamplingDecision.NOT_RECORD;
80+
if (sampled && adjustedCountCorrect) {
81+
otTraceState = {
82+
...otTraceState,
83+
threshold: intent.threshold,
84+
};
85+
} else {
86+
otTraceState = {
87+
...otTraceState,
88+
threshold: INVALID_THRESHOLD,
89+
};
90+
}
91+
92+
const otts = serializeTraceState(otTraceState);
93+
94+
let newTraceState: TraceState | undefined;
95+
if (traceState) {
96+
newTraceState = traceState;
97+
if (intent.updateTraceState) {
98+
newTraceState = intent.updateTraceState(newTraceState);
99+
}
100+
}
101+
if (otts) {
102+
if (!newTraceState) {
103+
newTraceState = new CoreTraceState();
104+
}
105+
newTraceState = newTraceState.set('ot', otts);
106+
}
107+
108+
return {
109+
decision,
110+
attributes: intent.attributes,
111+
traceState: newTraceState,
112+
};
113+
}
114+
115+
toString(): string {
116+
return this.delegate.toString();
117+
}
118+
}
119+
120+
/**
121+
* Returns a composite sampler that uses a composable sampler to make its
122+
* sampling decisions while handling tracestate.
123+
*/
124+
export function createCompositeSampler(delegate: ComposableSampler): Sampler {
125+
return new CompositeSampler(delegate);
126+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
export { createComposableAlwaysOffSampler } from './alwaysoff';
18+
export { createComposableAlwaysOnSampler } from './alwayson';
19+
export { createComposableTraceIDRatioBasedSampler } from './traceidratio';
20+
export { createComposableParentThresholdSampler } from './parentthreshold';
21+
export { createCompositeSampler } from './composite';
22+
export type { ComposableSampler, SamplingIntent } from './types';

0 commit comments

Comments
 (0)