diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 978ca31b371..7e800b4936e 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -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): Added experimental implementations of draft composite sampling spec [#5839](https://github.com/open-telemetry/opentelemetry-js/pull/5839) @anuraaga + ### :bug: Bug Fixes * fix(instrumentation-http): respect requireParent flag when INVALID_SPAN_CONTEXT is used [#4788](https://github.com/open-telemetry/opentelemetry-js/pull/4788) @reberhardt7 diff --git a/experimental/packages/sampler-composite/.eslintignore b/experimental/packages/sampler-composite/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/experimental/packages/sampler-composite/.eslintignore @@ -0,0 +1 @@ +build diff --git a/experimental/packages/sampler-composite/.eslintrc.js b/experimental/packages/sampler-composite/.eslintrc.js new file mode 100644 index 00000000000..0fe1bbf975f --- /dev/null +++ b/experimental/packages/sampler-composite/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "shared-node-browser": true + }, + ...require('../../../eslint.base.js') +} diff --git a/experimental/packages/sampler-composite/README.md b/experimental/packages/sampler-composite/README.md new file mode 100644 index 00000000000..6fce8346e41 --- /dev/null +++ b/experimental/packages/sampler-composite/README.md @@ -0,0 +1,57 @@ +# OpenTelemetry Composite Sampling + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +**Note: This is an experimental package under active development. New releases may include breaking changes.** + +This package provides implementations of composite samplers that propagate sampling information across a trace. +This implements the [experimental specification][probability-sampling]. + +Currently `ComposableRuleBased` and `ComposableAnnotating` are not implemented. + +## Quick Start + +To get started you will need to install a compatible OpenTelemetry SDK. + +### Samplers + +This module exports samplers that follow the general behavior of the standard SDK samplers, but ensuring +it is consistent across a trace by using the tracestate header. Notably, the tracestate can be examined +in exported spans to reconstruct population metrics. + +```typescript +import { + createCompositeSampler, + createComposableAlwaysOffSampler, + createComposableAlwaysOnSampler, + createComposableParentThresholdSampler, + createComposableTraceIDRatioBasedSampler, +} from '@opentelemetry/sampler-composite'; + +// never sample +const sampler = createCompositeSampler(createComposableAlwaysOffSampler()); +// always sample +const sampler = createCompositeSampler(createComposableAlwaysOnSampler()); +// follow the parent, or otherwise sample with a probability if root +const sampler = createCompositeSampler( + createComposableParentThresholdSampler(createComposableTraceIDRatioBasedSampler(0.3))); +``` + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/sampler-composite +[npm-img]: https://badge.fury.io/js/%40opentelemetry%sampler-composite.svg + +[probability-sampling]: https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/ diff --git a/experimental/packages/sampler-composite/package.json b/experimental/packages/sampler-composite/package.json new file mode 100644 index 00000000000..fef7b992500 --- /dev/null +++ b/experimental/packages/sampler-composite/package.json @@ -0,0 +1,70 @@ +{ + "name": "@opentelemetry/sampler-composite", + "private": false, + "publishConfig": { + "access": "public" + }, + "version": "0.205.0", + "description": "Composite samplers for OpenTelemetry tracing", + "module": "build/esm/index.js", + "esnext": "build/esnext/index.js", + "types": "build/src/index.d.ts", + "main": "build/src/index.js", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "prepublishOnly": "npm run compile", + "compile": "tsc --build", + "clean": "tsc --build --clean", + "test": "nyc mocha 'test/**/*.test.ts'", + "tdd": "npm run test -- --watch-extensions ts --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "version": "node ../../../scripts/version-update.js", + "watch": "tsc --build --watch", + "precompile": "lerna run version --scope @opentelemetry/sampler-composite --include-dependencies", + "prewatch": "npm run precompile", + "peer-api-check": "node ../../../scripts/peer-api-check.js", + "align-api-deps": "node ../../../scripts/align-api-deps.js" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "sampling", + "tracing" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "files": [ + "build/esm/**/*.js", + "build/esm/**/*.js.map", + "build/esm/**/*.d.ts", + "build/esnext/**/*.js", + "build/esnext/**/*.js.map", + "build/esnext/**/*.d.ts", + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "LICENSE", + "README.md" + ], + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "devDependencies": { + "@opentelemetry/api": "1.9.0", + "@types/mocha": "10.0.10", + "@types/node": "18.6.5", + "lerna": "6.6.2", + "mocha": "11.1.0", + "nyc": "17.1.0" + }, + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/sampler-composite", + "sideEffects": false +} diff --git a/experimental/packages/sampler-composite/src/alwaysoff.ts b/experimental/packages/sampler-composite/src/alwaysoff.ts new file mode 100644 index 00000000000..2c41635cdee --- /dev/null +++ b/experimental/packages/sampler-composite/src/alwaysoff.ts @@ -0,0 +1,41 @@ +/* + * 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 type { ComposableSampler, SamplingIntent } from './types'; +import { INVALID_THRESHOLD } from './util'; + +const intent: SamplingIntent = { + threshold: INVALID_THRESHOLD, + thresholdReliable: false, +}; + +class ComposableAlwaysOffSampler implements ComposableSampler { + getSamplingIntent(): SamplingIntent { + return intent; + } + + toString(): string { + return 'ComposableAlwaysOffSampler'; + } +} + +const _sampler = new ComposableAlwaysOffSampler(); + +/** + * Returns a composable sampler that does not sample any span. + */ +export function createComposableAlwaysOffSampler(): ComposableSampler { + return _sampler; +} diff --git a/experimental/packages/sampler-composite/src/alwayson.ts b/experimental/packages/sampler-composite/src/alwayson.ts new file mode 100644 index 00000000000..832d81e665b --- /dev/null +++ b/experimental/packages/sampler-composite/src/alwayson.ts @@ -0,0 +1,41 @@ +/* + * 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 type { ComposableSampler, SamplingIntent } from './types'; +import { MIN_THRESHOLD } from './util'; + +const intent: SamplingIntent = { + threshold: MIN_THRESHOLD, + thresholdReliable: true, +}; + +class ComposableAlwaysOnSampler implements ComposableSampler { + getSamplingIntent(): SamplingIntent { + return intent; + } + + toString(): string { + return 'ComposableAlwaysOnSampler'; + } +} + +const _sampler = new ComposableAlwaysOnSampler(); + +/** + * Returns a composable sampler that samples all span. + */ +export function createComposableAlwaysOnSampler(): ComposableSampler { + return _sampler; +} diff --git a/experimental/packages/sampler-composite/src/composite.ts b/experimental/packages/sampler-composite/src/composite.ts new file mode 100644 index 00000000000..0a8a90e0620 --- /dev/null +++ b/experimental/packages/sampler-composite/src/composite.ts @@ -0,0 +1,126 @@ +/* + * 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 { + Context, + SpanKind, + Attributes, + Link, + TraceState, + trace, +} from '@opentelemetry/api'; +import { TraceState as CoreTraceState } from '@opentelemetry/core'; +import { + Sampler, + SamplingDecision, + SamplingResult, +} from '@opentelemetry/sdk-trace-base'; +import { ComposableSampler } from './types'; +import { parseOtelTraceState, serializeTraceState } from './tracestate'; +import { + INVALID_THRESHOLD, + isValidRandomValue, + isValidThreshold, +} from './util'; + +class CompositeSampler implements Sampler { + constructor(private readonly delegate: ComposableSampler) {} + + shouldSample( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ): SamplingResult { + const spanContext = trace.getSpanContext(context); + + const traceState = spanContext?.traceState; + let otTraceState = parseOtelTraceState(traceState); + + const intent = this.delegate.getSamplingIntent( + context, + traceId, + spanName, + spanKind, + attributes, + links + ); + + let adjustedCountCorrect = false; + let sampled = false; + if (isValidThreshold(intent.threshold)) { + adjustedCountCorrect = intent.thresholdReliable; + let randomness: bigint; + if (isValidRandomValue(otTraceState.randomValue)) { + randomness = otTraceState.randomValue; + } else { + // Use last 56 bits of trace_id as randomness. + randomness = BigInt(`0x${traceId.slice(-14)}`); + } + sampled = intent.threshold <= randomness; + } + + const decision = sampled + ? SamplingDecision.RECORD_AND_SAMPLED + : SamplingDecision.NOT_RECORD; + if (sampled && adjustedCountCorrect) { + otTraceState = { + ...otTraceState, + threshold: intent.threshold, + }; + } else { + otTraceState = { + ...otTraceState, + threshold: INVALID_THRESHOLD, + }; + } + + const otts = serializeTraceState(otTraceState); + + let newTraceState: TraceState | undefined; + if (traceState) { + newTraceState = traceState; + if (intent.updateTraceState) { + newTraceState = intent.updateTraceState(newTraceState); + } + } + if (otts) { + if (!newTraceState) { + newTraceState = new CoreTraceState(); + } + newTraceState = newTraceState.set('ot', otts); + } + + return { + decision, + attributes: intent.attributes, + traceState: newTraceState, + }; + } + + toString(): string { + return this.delegate.toString(); + } +} + +/** + * Returns a composite sampler that uses a composable sampler to make its + * sampling decisions while handling tracestate. + */ +export function createCompositeSampler(delegate: ComposableSampler): Sampler { + return new CompositeSampler(delegate); +} diff --git a/experimental/packages/sampler-composite/src/index.ts b/experimental/packages/sampler-composite/src/index.ts new file mode 100644 index 00000000000..91e68bf22e6 --- /dev/null +++ b/experimental/packages/sampler-composite/src/index.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export { createComposableAlwaysOffSampler } from './alwaysoff'; +export { createComposableAlwaysOnSampler } from './alwayson'; +export { createComposableTraceIDRatioBasedSampler } from './traceidratio'; +export { createComposableParentThresholdSampler } from './parentthreshold'; +export { createCompositeSampler } from './composite'; +export type { ComposableSampler, SamplingIntent } from './types'; diff --git a/experimental/packages/sampler-composite/src/parentthreshold.ts b/experimental/packages/sampler-composite/src/parentthreshold.ts new file mode 100644 index 00000000000..a01a8d4eda1 --- /dev/null +++ b/experimental/packages/sampler-composite/src/parentthreshold.ts @@ -0,0 +1,89 @@ +/* + * 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, + isSpanContextValid, + Link, + SpanKind, + TraceFlags, + trace, +} from '@opentelemetry/api'; +import { ComposableSampler, SamplingIntent } from './types'; +import { parseOtelTraceState } from './tracestate'; +import { INVALID_THRESHOLD, isValidThreshold, MIN_THRESHOLD } from './util'; + +class ComposableParentThresholdSampler implements ComposableSampler { + private readonly description: string; + + constructor(private readonly rootSampler: ComposableSampler) { + this.description = `ComposableParentThresholdSampler(rootSampler=${rootSampler})`; + } + + getSamplingIntent( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ): SamplingIntent { + const parentSpanContext = trace.getSpanContext(context); + if (!parentSpanContext || !isSpanContextValid(parentSpanContext)) { + return this.rootSampler.getSamplingIntent( + context, + traceId, + spanName, + spanKind, + attributes, + links + ); + } + + const otTraceState = parseOtelTraceState(parentSpanContext.traceState); + + if (isValidThreshold(otTraceState.threshold)) { + return { + threshold: otTraceState.threshold, + thresholdReliable: true, + }; + } + + const threshold = + parentSpanContext.traceFlags & TraceFlags.SAMPLED + ? MIN_THRESHOLD + : INVALID_THRESHOLD; + return { + threshold, + thresholdReliable: false, + }; + } + + toString(): string { + return this.description; + } +} + +/** + * Returns a composable sampler that respects the sampling decision of the + * parent span or falls back to the given sampler if it is a root span. + */ +export function createComposableParentThresholdSampler( + rootSampler: ComposableSampler +): ComposableSampler { + return new ComposableParentThresholdSampler(rootSampler); +} diff --git a/experimental/packages/sampler-composite/src/traceidratio.ts b/experimental/packages/sampler-composite/src/traceidratio.ts new file mode 100644 index 00000000000..85f342c8c14 --- /dev/null +++ b/experimental/packages/sampler-composite/src/traceidratio.ts @@ -0,0 +1,78 @@ +/* + * 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 { ComposableSampler, SamplingIntent } from './types'; +import { INVALID_THRESHOLD, MAX_THRESHOLD } from './util'; +import { serializeTh } from './tracestate'; + +class ComposableTraceIDRatioBasedSampler implements ComposableSampler { + private readonly intent: SamplingIntent; + private readonly description: string; + + constructor(ratio: number) { + if (ratio < 0 || ratio > 1) { + throw new Error( + `Invalid sampling probability: ${ratio}. Must be between 0 and 1.` + ); + } + const threshold = calculateThreshold(ratio); + const thresholdStr = + threshold === MAX_THRESHOLD ? 'max' : serializeTh(threshold); + if (threshold !== MAX_THRESHOLD) { + this.intent = { + 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 = { + threshold: INVALID_THRESHOLD, + thresholdReliable: false, + }; + } + this.description = `ComposableTraceIDRatioBasedSampler(threshold=${thresholdStr}, ratio=${ratio})`; + } + + getSamplingIntent(): SamplingIntent { + return this.intent; + } + + toString(): string { + return this.description; + } +} + +/** + * Returns a composable sampler that samples each span with a fixed ratio. + */ +export function createComposableTraceIDRatioBasedSampler( + ratio: number +): ComposableSampler { + return new ComposableTraceIDRatioBasedSampler(ratio); +} + +const probabilityThresholdScale = Math.pow(2, 56); + +// TODO: Reduce threshold precision following spec recommendation of 4 +// to reduce size of serialized tracestate. +function calculateThreshold(samplingProbability: number): bigint { + return ( + MAX_THRESHOLD - + BigInt(Math.round(samplingProbability * probabilityThresholdScale)) + ); +} diff --git a/experimental/packages/sampler-composite/src/tracestate.ts b/experimental/packages/sampler-composite/src/tracestate.ts new file mode 100644 index 00000000000..8d12d218a65 --- /dev/null +++ b/experimental/packages/sampler-composite/src/tracestate.ts @@ -0,0 +1,156 @@ +/* + * 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 { TraceState } from '@opentelemetry/api'; +import { + INVALID_RANDOM_VALUE, + INVALID_THRESHOLD, + isValidRandomValue, + isValidThreshold, + MAX_THRESHOLD, +} from './util'; + +export type OtelTraceState = { + /** The random value for sampling decisions in the trace. */ + randomValue: bigint; + /** The upstream threshold for sampling decisions. */ + threshold: bigint; + /** The rest of the "ot" tracestate value. */ + rest?: string[]; +}; + +export const INVALID_TRACE_STATE: OtelTraceState = Object.freeze({ + randomValue: INVALID_RANDOM_VALUE, + threshold: INVALID_THRESHOLD, +}); + +const TRACE_STATE_SIZE_LIMIT = 256; +const MAX_VALUE_LENGTH = 14; // 56 bits, 4 bits per hex digit + +export function parseOtelTraceState( + traceState: TraceState | undefined +): OtelTraceState { + const ot = traceState?.get('ot'); + if (!ot || ot.length > TRACE_STATE_SIZE_LIMIT) { + return INVALID_TRACE_STATE; + } + + let threshold = INVALID_THRESHOLD; + let randomValue = INVALID_RANDOM_VALUE; + + // Parse based on https://opentelemetry.io/docs/specs/otel/trace/tracestate-handling/ + const members = ot.split(';'); + let rest: string[] | undefined; + for (const member of members) { + if (member.startsWith('th:')) { + threshold = parseTh(member.slice('th:'.length), INVALID_THRESHOLD); + continue; + } + if (member.startsWith('rv:')) { + randomValue = parseRv(member.slice('rv:'.length), INVALID_RANDOM_VALUE); + continue; + } + if (!rest) { + rest = []; + } + rest.push(member); + } + + return { + randomValue, + threshold, + rest, + }; +} + +export function serializeTraceState(otTraceState: OtelTraceState): string { + if ( + !isValidThreshold(otTraceState.threshold) && + !isValidRandomValue(otTraceState.randomValue) && + !otTraceState.rest + ) { + return ''; + } + + const parts: string[] = []; + if ( + isValidThreshold(otTraceState.threshold) && + otTraceState.threshold !== MAX_THRESHOLD + ) { + parts.push(`th:${serializeTh(otTraceState.threshold)}`); + } + if (isValidRandomValue(otTraceState.randomValue)) { + parts.push(`rv:${serializeRv(otTraceState.randomValue)}`); + } + if (otTraceState.rest) { + parts.push(...otTraceState.rest); + } + let res = parts.join(';'); + while (res.length > TRACE_STATE_SIZE_LIMIT) { + const lastSemicolon = res.lastIndexOf(';'); + if (lastSemicolon === -1) { + break; + } + res = res.slice(0, lastSemicolon); + } + return res; +} + +function parseTh(value: string, defaultValue: bigint): bigint { + if (!value || value.length > MAX_VALUE_LENGTH) { + return defaultValue; + } + + try { + return BigInt('0x' + value.padEnd(MAX_VALUE_LENGTH, '0')); + } catch { + return defaultValue; + } +} + +function parseRv(value: string, defaultValue: bigint): bigint { + if (!value || value.length !== MAX_VALUE_LENGTH) { + return defaultValue; + } + + try { + return BigInt(`0x${value}`); + } catch { + return defaultValue; + } +} + +// hex value without trailing zeros +export function serializeTh(threshold: bigint): string { + if (threshold === 0n) { + return '0'; + } + + const value = threshold.toString(16).padStart(MAX_VALUE_LENGTH, '0'); + let idxAfterNonZero = value.length; + for (let i = value.length - 1; i >= 0; i--) { + if (value[i] !== '0') { + idxAfterNonZero = i + 1; + break; + } + } + // Checked at beginning so there is definitely a nonzero. + return value.slice(0, idxAfterNonZero); +} + +function serializeRv(randomValue: bigint): string { + return randomValue.toString(16).padStart(MAX_VALUE_LENGTH, '0'); +} diff --git a/experimental/packages/sampler-composite/src/types.ts b/experimental/packages/sampler-composite/src/types.ts new file mode 100644 index 00000000000..3743a38c071 --- /dev/null +++ b/experimental/packages/sampler-composite/src/types.ts @@ -0,0 +1,44 @@ +/* + * 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, TraceState } from '@opentelemetry/api'; +import { type Sampler } from '@opentelemetry/sdk-trace-base'; + +/** Information to make a sampling decision. */ +export type SamplingIntent = { + /** The sampling threshold value. A lower threshold increases the likelihood of sampling. */ + threshold: bigint; + + /** Whether the threshold can be reliably used for Span-to-Metrics estimation. */ + thresholdReliable: boolean; + + /** Any attributes to add to the span for the sampling result. */ + attributes?: Attributes; + + /** How to update the TraceState for the span. */ + updateTraceState?: (ts: TraceState | undefined) => TraceState | undefined; +}; + +/** A sampler that can be composed to make a final sampling decision. */ +export interface ComposableSampler { + /** Returns the information to make a sampling decision. */ + getSamplingIntent( + ...args: Parameters + ): SamplingIntent; + + /** Returns the sampler name or short description with the configuration. */ + toString(): string; +} diff --git a/experimental/packages/sampler-composite/src/util.ts b/experimental/packages/sampler-composite/src/util.ts new file mode 100644 index 00000000000..5ba900c427e --- /dev/null +++ b/experimental/packages/sampler-composite/src/util.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +export const INVALID_THRESHOLD = -1n; +export const INVALID_RANDOM_VALUE = -1n; + +const RANDOM_VALUE_BITS = 56n; +export const MAX_THRESHOLD = 1n << RANDOM_VALUE_BITS; // 0% sampling +export const MIN_THRESHOLD = 0n; // 100% sampling +const MAX_RANDOM_VALUE = MAX_THRESHOLD - 1n; + +export function isValidThreshold(threshold: bigint): boolean { + return threshold >= MIN_THRESHOLD && threshold <= MAX_THRESHOLD; +} + +export function isValidRandomValue(randomValue: bigint): boolean { + return randomValue >= 0n && randomValue <= MAX_RANDOM_VALUE; +} diff --git a/experimental/packages/sampler-composite/test/alwaysoff.test.ts b/experimental/packages/sampler-composite/test/alwaysoff.test.ts new file mode 100644 index 00000000000..e10c9c28274 --- /dev/null +++ b/experimental/packages/sampler-composite/test/alwaysoff.test.ts @@ -0,0 +1,72 @@ +/* + * 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 { SamplingDecision } from '@opentelemetry/sdk-trace-base'; + +import { + createCompositeSampler, + createComposableAlwaysOffSampler, +} from '../src'; +import { traceIdGenerator } from './util'; + +describe('ComposableAlwaysOffSampler', () => { + const composableSampler = createComposableAlwaysOffSampler(); + + it('should have a description', () => { + assert.strictEqual( + composableSampler.toString(), + 'ComposableAlwaysOffSampler' + ); + }); + + it('should have a constant threshold', () => { + assert.strictEqual( + composableSampler.getSamplingIntent( + context.active(), + 'unused', + 'span', + SpanKind.SERVER, + {}, + [] + ).threshold, + -1n + ); + }); + + it('should never sample', () => { + const sampler = createCompositeSampler(composableSampler); + const generator = traceIdGenerator(); + let numSampled = 0; + for (let i = 0; i < 10000; i++) { + const result = sampler.shouldSample( + context.active(), + generator(), + 'span', + SpanKind.SERVER, + {}, + [] + ); + if (result.decision === SamplingDecision.RECORD_AND_SAMPLED) { + numSampled++; + } + assert.strictEqual(result.traceState, undefined); + } + assert.strictEqual(numSampled, 0); + }); +}); diff --git a/experimental/packages/sampler-composite/test/alwayson.test.ts b/experimental/packages/sampler-composite/test/alwayson.test.ts new file mode 100644 index 00000000000..7a29a1fa8c3 --- /dev/null +++ b/experimental/packages/sampler-composite/test/alwayson.test.ts @@ -0,0 +1,72 @@ +/* + * 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 { SamplingDecision } from '@opentelemetry/sdk-trace-base'; + +import { + createCompositeSampler, + createComposableAlwaysOnSampler, +} from '../src'; +import { traceIdGenerator } from './util'; + +describe('ComposableAlwaysOnSampler', () => { + const composableSampler = createComposableAlwaysOnSampler(); + + it('should have a description', () => { + assert.strictEqual( + composableSampler.toString(), + 'ComposableAlwaysOnSampler' + ); + }); + + it('should have a constant threshold', () => { + assert.strictEqual( + composableSampler.getSamplingIntent( + context.active(), + 'unused', + 'span', + SpanKind.SERVER, + {}, + [] + ).threshold, + 0n + ); + }); + + it('should always sample', () => { + const sampler = createCompositeSampler(composableSampler); + const generator = traceIdGenerator(); + let numSampled = 0; + for (let i = 0; i < 10000; i++) { + const result = sampler.shouldSample( + context.active(), + generator(), + 'span', + SpanKind.SERVER, + {}, + [] + ); + if (result.decision === SamplingDecision.RECORD_AND_SAMPLED) { + numSampled++; + } + assert.strictEqual(result.traceState?.get('ot'), 'th:0'); + } + assert.strictEqual(numSampled, 10000); + }); +}); diff --git a/experimental/packages/sampler-composite/test/sampler.test.ts b/experimental/packages/sampler-composite/test/sampler.test.ts new file mode 100644 index 00000000000..86383d93ec7 --- /dev/null +++ b/experimental/packages/sampler-composite/test/sampler.test.ts @@ -0,0 +1,192 @@ +/* + * 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, + SpanContext, + SpanKind, + TraceFlags, + trace, +} from '@opentelemetry/api'; +import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; + +import { + createCompositeSampler, + createComposableAlwaysOffSampler, + createComposableAlwaysOnSampler, + createComposableParentThresholdSampler, + createComposableTraceIDRatioBasedSampler, +} from '../src'; +import { INVALID_RANDOM_VALUE, INVALID_THRESHOLD } from '../src/util'; +import { + INVALID_TRACE_STATE, + parseOtelTraceState, + serializeTraceState, +} from '../src/tracestate'; +import { TraceState } from '@opentelemetry/core'; + +describe('ConsistentSampler', () => { + const traceId = '00112233445566778800000000000000'; + const spanId = '0123456789abcdef'; + + [ + { + sampler: createComposableAlwaysOnSampler(), + parentSampled: true, + parentThreshold: undefined, + parentRandomValue: undefined, + sampled: true, + threshold: 0n, + randomValue: INVALID_RANDOM_VALUE, + testId: 'min threshold no parent random value', + }, + { + sampler: createComposableAlwaysOnSampler(), + parentSampled: true, + parentThreshold: undefined, + parentRandomValue: 0x7f99aa40c02744n, + sampled: true, + threshold: 0n, + randomValue: 0x7f99aa40c02744n, + testId: 'min threshold with parent random value', + }, + { + sampler: createComposableAlwaysOffSampler(), + parentSampled: true, + parentThreshold: undefined, + parentRandomValue: undefined, + sampled: false, + threshold: INVALID_THRESHOLD, + randomValue: INVALID_RANDOM_VALUE, + testId: 'max threshold', + }, + { + sampler: createComposableParentThresholdSampler( + createComposableAlwaysOnSampler() + ), + parentSampled: false, + parentThreshold: 0x7f99aa40c02744n, + parentRandomValue: 0x7f99aa40c02744n, + sampled: true, + threshold: 0x7f99aa40c02744n, + randomValue: 0x7f99aa40c02744n, + testId: 'parent based in consistent mode', + }, + { + sampler: createComposableParentThresholdSampler( + createComposableAlwaysOnSampler() + ), + parentSampled: true, + parentThreshold: undefined, + parentRandomValue: undefined, + sampled: true, + threshold: INVALID_THRESHOLD, + randomValue: INVALID_RANDOM_VALUE, + testId: 'parent based in legacy mode', + }, + { + sampler: createComposableTraceIDRatioBasedSampler(0.5), + parentSampled: true, + parentThreshold: undefined, + parentRandomValue: 0x7fffffffffffffn, + sampled: false, + threshold: INVALID_THRESHOLD, + randomValue: 0x7fffffffffffffn, + testId: 'half threshold not sampled', + }, + { + sampler: createComposableTraceIDRatioBasedSampler(0.5), + parentSampled: false, + parentThreshold: undefined, + parentRandomValue: 0x80000000000000n, + sampled: true, + threshold: 0x80000000000000n, + randomValue: 0x80000000000000n, + testId: 'half threshold sampled', + }, + { + sampler: createComposableTraceIDRatioBasedSampler(1.0), + parentSampled: false, + parentThreshold: 0x80000000000000n, + parentRandomValue: 0x80000000000000n, + sampled: true, + threshold: 0n, + randomValue: 0x80000000000000n, + testId: 'parent inviolating invariant', + }, + ].forEach( + ({ + sampler, + parentSampled, + parentThreshold, + parentRandomValue, + sampled, + threshold, + randomValue, + testId, + }) => { + it(`should sample with ${testId}`, () => { + let parentOtTraceState = INVALID_TRACE_STATE; + if (parentThreshold !== undefined) { + parentOtTraceState = { + ...parentOtTraceState, + threshold: parentThreshold, + }; + } + if (parentRandomValue !== undefined) { + parentOtTraceState = { + ...parentOtTraceState, + randomValue: parentRandomValue, + }; + } + const parentOt = serializeTraceState(parentOtTraceState); + const parentTraceState = parentOt + ? new TraceState().set('ot', parentOt) + : undefined; + const traceFlags = parentSampled ? TraceFlags.SAMPLED : TraceFlags.NONE; + const parentSpanContext: SpanContext = { + traceId, + spanId, + traceFlags, + traceState: parentTraceState, + }; + const parentContext = trace.setSpanContext( + context.active(), + parentSpanContext + ); + + const result = createCompositeSampler(sampler).shouldSample( + parentContext, + traceId, + 'name', + SpanKind.INTERNAL, + {}, + [] + ); + const expectedDecision = sampled + ? SamplingDecision.RECORD_AND_SAMPLED + : SamplingDecision.NOT_RECORD; + const state = parseOtelTraceState(result.traceState); + + assert.strictEqual(result.decision, expectedDecision); + assert.strictEqual(state.threshold, threshold); + assert.strictEqual(state.randomValue, randomValue); + }); + } + ); +}); diff --git a/experimental/packages/sampler-composite/test/traceidratio.test.ts b/experimental/packages/sampler-composite/test/traceidratio.test.ts new file mode 100644 index 00000000000..fc9499aa088 --- /dev/null +++ b/experimental/packages/sampler-composite/test/traceidratio.test.ts @@ -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 { SamplingDecision } from '@opentelemetry/sdk-trace-base'; + +import { + createCompositeSampler, + createComposableTraceIDRatioBasedSampler, +} from '../src'; +import { traceIdGenerator } from './util'; +import { parseOtelTraceState } from '../src/tracestate'; +import { INVALID_RANDOM_VALUE } from '../src/util'; + +describe('ComposableTraceIDRatioBasedSampler', () => { + [ + { ratio: 1.0, thresholdStr: '0' }, + { ratio: 0.5, thresholdStr: '8' }, + { ratio: 0.25, thresholdStr: 'c' }, + { ratio: 1e-300, thresholdStr: 'max' }, + { ratio: 0, thresholdStr: 'max' }, + ].forEach(({ ratio, thresholdStr }) => { + it(`should have a description for ratio ${ratio}`, () => { + const sampler = createComposableTraceIDRatioBasedSampler(ratio); + assert.strictEqual( + sampler.toString(), + `ComposableTraceIDRatioBasedSampler(threshold=${thresholdStr}, ratio=${ratio})` + ); + }); + }); + + [ + { ratio: 1.0, threshold: 0n }, + { ratio: 0.5, threshold: 36028797018963968n }, + { ratio: 0.25, threshold: 54043195528445952n }, + { ratio: 0.125, threshold: 63050394783186944n }, + { ratio: 0.0, threshold: 72057594037927936n }, + { ratio: 0.45, threshold: 39631676720860364n }, + { ratio: 0.2, threshold: 57646075230342348n }, + { ratio: 0.13, threshold: 62690106812997304n }, + { ratio: 0.05, threshold: 68454714336031539n }, + ].forEach(({ ratio, threshold }) => { + it(`should sample spans with ratio ${ratio}`, () => { + const sampler = createCompositeSampler( + createComposableTraceIDRatioBasedSampler(ratio) + ); + + const generator = traceIdGenerator(); + let numSampled = 0; + for (let i = 0; i < 10000; i++) { + const result = sampler.shouldSample( + context.active(), + generator(), + 'span', + SpanKind.SERVER, + {}, + [] + ); + if (result.decision === SamplingDecision.RECORD_AND_SAMPLED) { + numSampled++; + const otTraceState = parseOtelTraceState(result.traceState); + assert.strictEqual(otTraceState?.threshold, threshold); + assert.strictEqual(otTraceState?.randomValue, INVALID_RANDOM_VALUE); + } + } + const expectedNumSampled = 10000 * ratio; + assert.ok( + Math.abs(numSampled - expectedNumSampled) < 50, + `expected ${expectedNumSampled}, have ${numSampled}` + ); + }); + }); +}); diff --git a/experimental/packages/sampler-composite/test/tracestate.test.ts b/experimental/packages/sampler-composite/test/tracestate.test.ts new file mode 100644 index 00000000000..3e439029533 --- /dev/null +++ b/experimental/packages/sampler-composite/test/tracestate.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { serializeTraceState, parseOtelTraceState } from '../src/tracestate'; +import { TraceState } from '@opentelemetry/core'; + +describe('OtelTraceState', () => { + [ + { input: 'a', output: 'a' }, + { input: '#', output: '#' }, + { input: 'rv:1234567890abcd', output: 'rv:1234567890abcd' }, + { input: 'rv:01020304050607', output: 'rv:01020304050607' }, + { input: 'rv:1234567890abcde', output: '' }, + { input: 'th:1234567890abcd', output: 'th:1234567890abcd' }, + { input: 'th:1234567890abcd', output: 'th:1234567890abcd' }, + { input: 'th:10000000000000', output: 'th:1' }, + { input: 'th:1234500000000', output: 'th:12345' }, + { input: 'th:0', output: 'th:0' }, + { input: 'th:100000000000000', output: '' }, + { input: 'th:1234567890abcde', output: '' }, + { + input: `a:${''.padEnd(214, 'X')};rv:1234567890abcd;th:1234567890abcd;x:3`, + output: `th:1234567890abcd;rv:1234567890abcd;a:${''.padEnd(214, 'X')};x:3`, + testId: 'long', + }, + { input: 'th:x', output: '' }, + { input: 'th:100000000000000', output: '' }, + { input: 'th:10000000000000', output: 'th:1' }, + { input: 'th:1000000000000', output: 'th:1' }, + { input: 'th:100000000000', output: 'th:1' }, + { input: 'th:10000000000', output: 'th:1' }, + { input: 'th:1000000000', output: 'th:1' }, + { input: 'th:100000000', output: 'th:1' }, + { input: 'th:10000000', output: 'th:1' }, + { input: 'th:1000000', output: 'th:1' }, + { input: 'th:100000', output: 'th:1' }, + { input: 'th:10000', output: 'th:1' }, + { input: 'th:1000', output: 'th:1' }, + { input: 'th:100', output: 'th:1' }, + { input: 'th:10', output: 'th:1' }, + { input: 'th:1', output: 'th:1' }, + { input: 'th:10000000000001', output: 'th:10000000000001' }, + { input: 'th:10000000000010', output: 'th:1000000000001' }, + { input: 'rv:x', output: '' }, + { input: 'rv:100000000000000', output: '' }, + { input: 'rv:10000000000000', output: 'rv:10000000000000' }, + { input: 'rv:1000000000000', output: '' }, + ].forEach(({ input, output, testId }) => { + it(`should round trip ${testId || `from ${input} to ${output}`}`, () => { + const result = serializeTraceState( + parseOtelTraceState(new TraceState().set('ot', input)) + ); + assert.strictEqual(result, output); + }); + }); +}); diff --git a/experimental/packages/sampler-composite/test/util.ts b/experimental/packages/sampler-composite/test/util.ts new file mode 100644 index 00000000000..a4b393aa5d4 --- /dev/null +++ b/experimental/packages/sampler-composite/test/util.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +// Use a fixed seed simple but reasonable random number generator for consistent tests. +// Unlike many languages, there isn't a way to set the seed of the built-in random. + +function splitmix32(a: number) { + return function () { + a |= 0; + a = (a + 0x9e3779b9) | 0; + let t = a ^ (a >>> 16); + t = Math.imul(t, 0x21f0aaad); + t = t ^ (t >>> 15); + t = Math.imul(t, 0x735a2d97); + return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296; + }; +} + +export function traceIdGenerator(): () => string { + const seed = 0xdeadbeef; + const random = splitmix32(seed); + // Pre-mix the state. + for (let i = 0; i < 15; i++) { + random(); + } + return () => { + const parts: string[] = []; + // 32-bit randoms, concatenate 4 of them + for (let i = 0; i < 4; i++) { + const val = Math.round(random() * 0xffffffff); + parts.push(val.toString(16).padStart(8, '0')); + } + return parts.join(''); + }; +} diff --git a/experimental/packages/sampler-composite/tsconfig.esm.json b/experimental/packages/sampler-composite/tsconfig.esm.json new file mode 100644 index 00000000000..5fe96d554ba --- /dev/null +++ b/experimental/packages/sampler-composite/tsconfig.esm.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.esm.json", + "compilerOptions": { + "allowJs": true, + "outDir": "build/esm", + "rootDir": "src", + "tsBuildInfoFile": "build/esm/tsconfig.esm.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" + } + ] +} diff --git a/experimental/packages/sampler-composite/tsconfig.esnext.json b/experimental/packages/sampler-composite/tsconfig.esnext.json new file mode 100644 index 00000000000..17ed0461704 --- /dev/null +++ b/experimental/packages/sampler-composite/tsconfig.esnext.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.esnext.json", + "compilerOptions": { + "allowJs": true, + "outDir": "build/esnext", + "rootDir": "src", + "tsBuildInfoFile": "build/esnext/tsconfig.esnext.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" + } + ] +} diff --git a/experimental/packages/sampler-composite/tsconfig.json b/experimental/packages/sampler-composite/tsconfig.json new file mode 100644 index 00000000000..eb6f0a3a273 --- /dev/null +++ b/experimental/packages/sampler-composite/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "outDir": "build", + "rootDir": "." + }, + "files": [], + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" + } + ] +} diff --git a/package-lock.json b/package-lock.json index 91defe033e4..f768afef19e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1448,6 +1448,257 @@ "@opentelemetry/api": "^1.3.0" } }, + "experimental/packages/sampler-composite": { + "name": "@opentelemetry/sampler-composite", + "version": "0.205.0", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" + }, + "devDependencies": { + "@opentelemetry/api": "1.9.0", + "@types/mocha": "10.0.10", + "@types/node": "18.6.5", + "lerna": "6.6.2", + "mocha": "11.1.0", + "nyc": "17.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "experimental/packages/sampler-composite/node_modules/@opentelemetry/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "experimental/packages/sampler-composite/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "experimental/packages/sampler-composite/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", + "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "experimental/packages/sampler-composite/node_modules/@types/node": { + "version": "18.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz", + "integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==", + "dev": true, + "license": "MIT" + }, + "experimental/packages/sampler-composite/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "experimental/packages/sampler-composite/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "experimental/packages/sampler-composite/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "experimental/packages/sampler-composite/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "experimental/packages/sampler-composite/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "experimental/packages/sampler-composite/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "experimental/packages/sampler-composite/node_modules/mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "experimental/packages/sampler-composite/node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "experimental/packages/sampler-composite/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "experimental/packages/sampler-composite/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "experimental/packages/sampler-composite/node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "experimental/packages/sampler-composite/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "experimental/packages/sampler-jaeger-remote": { "name": "@opentelemetry/sampler-jaeger-remote", "version": "0.205.0", @@ -5962,6 +6213,10 @@ "resolved": "packages/opentelemetry-resources", "link": true }, + "node_modules/@opentelemetry/sampler-composite": { + "resolved": "experimental/packages/sampler-composite", + "link": true + }, "node_modules/@opentelemetry/sampler-jaeger-remote": { "resolved": "experimental/packages/sampler-jaeger-remote", "link": true @@ -31040,6 +31295,175 @@ } } }, + "@opentelemetry/sampler-composite": { + "version": "file:experimental/packages/sampler-composite", + "requires": { + "@opentelemetry/api": "1.9.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1", + "@types/mocha": "10.0.10", + "@types/node": "18.6.5", + "lerna": "6.6.2", + "mocha": "11.1.0", + "nyc": "17.1.0" + }, + "dependencies": { + "@opentelemetry/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "requires": { + "@opentelemetry/semantic-conventions": "^1.29.0" + } + }, + "@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "requires": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", + "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", + "requires": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + } + }, + "@types/node": { + "version": "18.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz", + "integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "@opentelemetry/sampler-jaeger-remote": { "version": "file:experimental/packages/sampler-jaeger-remote", "requires": { diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 2bb983f4064..711743009ba 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -44,6 +44,9 @@ { "path": "experimental/packages/otlp-transformer/tsconfig.esm.json" }, + { + "path": "experimental/packages/sampler-composite/tsconfig.esm.json" + }, { "path": "experimental/packages/sdk-logs/tsconfig.esm.json" }, diff --git a/tsconfig.esnext.json b/tsconfig.esnext.json index ac540a88b2d..cfc96b3bda9 100644 --- a/tsconfig.esnext.json +++ b/tsconfig.esnext.json @@ -44,6 +44,9 @@ { "path": "experimental/packages/otlp-transformer/tsconfig.esnext.json" }, + { + "path": "experimental/packages/sampler-composite/tsconfig.esnext.json" + }, { "path": "experimental/packages/sdk-logs/tsconfig.esnext.json" }, diff --git a/tsconfig.json b/tsconfig.json index ee5c10de7b4..58f4ff7e340 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ "experimental/packages/otlp-exporter-base", "experimental/packages/otlp-grpc-exporter-base", "experimental/packages/otlp-transformer", + "experimental/packages/sampler-composite", "experimental/packages/sampler-jaeger-remote", "experimental/packages/sdk-logs", "experimental/packages/shim-opencensus", @@ -126,6 +127,9 @@ { "path": "experimental/packages/otlp-transformer" }, + { + "path": "experimental/packages/sampler-composite" + }, { "path": "experimental/packages/sampler-jaeger-remote" },