From f5c1380e14ca3b678c9a2aef87ddf526f777d7d3 Mon Sep 17 00:00:00 2001 From: jjllee Date: Thu, 3 Jul 2025 10:23:11 -0700 Subject: [PATCH 1/5] feat(opentelemetry-sampler-aws-xray): Add Rate Limiter and Sampling Targets Poller Logic --- .../src/aws-xray-sampling-client.ts | 16 +- .../src/fallback-sampler.ts | 18 +- .../src/rate-limiter.ts | 67 +++++ .../src/rate-limiting-sampler.ts | 58 ++++ .../src/remote-sampler.ts | 108 ++++++-- .../src/rule-cache.ts | 68 ++++- .../src/sampling-rule-applier.ts | 81 +++++- .../test/aws-xray-sampling-client.test.ts | 15 +- .../test/fallback-sampler.test.ts | 178 +++++++++++- .../test/rate-limiter.test.ts | 62 +++++ .../test/rate-limiting-sampler.test.ts | 216 +++++++++++++++ .../test/remote-sampler.test.ts | 253 +++++++++++++++++- .../test/rule-cache.test.ts | 113 +++++++- 13 files changed, 1194 insertions(+), 59 deletions(-) create mode 100644 incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts create mode 100644 incubator/opentelemetry-sampler-aws-xray/src/rate-limiting-sampler.ts create mode 100644 incubator/opentelemetry-sampler-aws-xray/test/rate-limiter.test.ts create mode 100644 incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts diff --git a/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts b/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts index 75c299295b..43d4d1226b 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts @@ -18,7 +18,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { DiagLogFunction, DiagLogger, context } from '@opentelemetry/api'; +import { DiagLogFunction, context, diag } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import * as http from 'http'; import { @@ -30,12 +30,10 @@ import { export class AWSXRaySamplingClient { private getSamplingRulesEndpoint: string; private samplingTargetsEndpoint: string; - private samplerDiag: DiagLogger; - constructor(endpoint: string, samplerDiag: DiagLogger) { + constructor(endpoint: string) { this.getSamplingRulesEndpoint = endpoint + '/GetSamplingRules'; this.samplingTargetsEndpoint = endpoint + '/SamplingTargets'; - this.samplerDiag = samplerDiag; } public fetchSamplingTargets( @@ -45,7 +43,7 @@ export class AWSXRaySamplingClient { this.makeSamplingRequest( this.samplingTargetsEndpoint, callback, - this.samplerDiag.debug, + diag.debug, JSON.stringify(requestBody) ); } @@ -56,7 +54,7 @@ export class AWSXRaySamplingClient { this.makeSamplingRequest( this.getSamplingRulesEndpoint, callback, - this.samplerDiag.error + diag.error ); } @@ -98,10 +96,8 @@ export class AWSXRaySamplingClient { callback(responseObject); } } else { - this.samplerDiag.debug( - `${url} Response Code is: ${response.statusCode}` - ); - this.samplerDiag.debug(`${url} responseData is: ${responseData}`); + diag.debug(`${url} Response Code is: ${response.statusCode}`); + diag.debug(`${url} responseData is: ${responseData}`); } }); }) diff --git a/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts b/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts index d61dd8fb60..660ef37fe8 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts @@ -21,16 +21,20 @@ import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; import { Sampler, + SamplingDecision, SamplingResult, TraceIdRatioBasedSampler, } from '@opentelemetry/sdk-trace-base'; +import { RateLimitingSampler } from './rate-limiting-sampler'; // FallbackSampler samples 1 req/sec and additional 5% of requests using TraceIdRatioBasedSampler. export class FallbackSampler implements Sampler { private fixedRateSampler: TraceIdRatioBasedSampler; + private rateLimitingSampler: RateLimitingSampler; constructor() { this.fixedRateSampler = new TraceIdRatioBasedSampler(0.05); + this.rateLimitingSampler = new RateLimitingSampler(1); } shouldSample( @@ -41,7 +45,19 @@ export class FallbackSampler implements Sampler { attributes: Attributes, links: Link[] ): SamplingResult { - // TODO: implement and use Rate Limiting Sampler + const samplingResult: SamplingResult = + this.rateLimitingSampler.shouldSample( + context, + traceId, + spanName, + spanKind, + attributes, + links + ); + + if (samplingResult.decision !== SamplingDecision.NOT_RECORD) { + return samplingResult; + } return this.fixedRateSampler.shouldSample(context, traceId); } diff --git a/incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts b/incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts new file mode 100644 index 0000000000..fab426cd99 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ + +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* + * The RateLimiter keeps track of the current reservoir quota balance available (measured via available time) + * If enough time has elapsed, the RateLimiter will allow quota balance to be consumed/taken (decrease available time) + * A RateLimitingSampler uses this RateLimiter to determine if it should sample or not based on the quota balance available. + */ +export class RateLimiter { + // Quota assigned to client to dictate maximum quota balance that can be consumed per second. + private quota: number; + private MAX_BALANCE_MILLIS: number; + // Used to measure current quota balance. + private walletFloorMillis: number; + + constructor(quota: number, maxBalanceInSeconds = 1) { + this.MAX_BALANCE_MILLIS = maxBalanceInSeconds * 1000.0; + this.quota = quota; + this.walletFloorMillis = Date.now(); + // current "balance" would be `ceiling - floor` + } + + public take(cost = 1): boolean { + if (this.quota === 0) { + return false; + } + + const quotaPerMillis: number = this.quota / 1000.0; + + // assume divide by zero not possible + const costInMillis: number = cost / quotaPerMillis; + + const walletCeilingMillis: number = Date.now(); + let currentBalanceMillis: number = + walletCeilingMillis - this.walletFloorMillis; + currentBalanceMillis = Math.min( + currentBalanceMillis, + this.MAX_BALANCE_MILLIS + ); + const pendingRemainingBalanceMillis: number = + currentBalanceMillis - costInMillis; + if (pendingRemainingBalanceMillis >= 0) { + this.walletFloorMillis = + walletCeilingMillis - pendingRemainingBalanceMillis; + return true; + } + // No changes to the wallet state + return false; + } +} diff --git a/incubator/opentelemetry-sampler-aws-xray/src/rate-limiting-sampler.ts b/incubator/opentelemetry-sampler-aws-xray/src/rate-limiting-sampler.ts new file mode 100644 index 0000000000..c384ced22e --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/src/rate-limiting-sampler.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; +import { + Sampler, + SamplingDecision, + SamplingResult, +} from '@opentelemetry/sdk-trace-base'; +import { RateLimiter } from './rate-limiter'; + +export class RateLimitingSampler implements Sampler { + private quota: number; + private reservoir: RateLimiter; + + constructor(quota: number) { + this.quota = quota; + this.reservoir = new RateLimiter(quota); + } + + shouldSample( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ): SamplingResult { + if (this.reservoir.take(1)) { + return { + decision: SamplingDecision.RECORD_AND_SAMPLED, + attributes: attributes, + }; + } + return { decision: SamplingDecision.NOT_RECORD, attributes: attributes }; + } + + public toString(): string { + return `RateLimitingSampler{rate limiting sampling with sampling config of ${this.quota} req/sec and 0% of additional requests}`; + } +} diff --git a/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts b/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts index 5cc080ee90..ffd5b2c949 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts @@ -18,14 +18,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - Attributes, - Context, - DiagLogger, - Link, - SpanKind, - diag, -} from '@opentelemetry/api'; +import { Attributes, Context, Link, SpanKind, diag } from '@opentelemetry/api'; import { ParentBasedSampler, Sampler, @@ -36,10 +29,16 @@ import { FallbackSampler } from './fallback-sampler'; import { AWSXRayRemoteSamplerConfig, GetSamplingRulesResponse, + GetSamplingTargetsBody, + GetSamplingTargetsResponse, SamplingRuleRecord, + SamplingTargetDocument, + TargetMap, } from './types'; -import { RuleCache } from './rule-cache'; - +import { + DEFAULT_TARGET_POLLING_INTERVAL_SECONDS, + RuleCache, +} from './rule-cache'; import { SamplingRuleApplier } from './sampling-rule-applier'; // 5 minute default sampling rules polling interval @@ -94,23 +93,23 @@ export class AWSXRayRemoteSampler implements Sampler { // Not intended for external use, use Parent-based `AWSXRayRemoteSampler` instead. export class _AWSXRayRemoteSampler implements Sampler { private rulePollingIntervalMillis: number; + private targetPollingInterval: number; private awsProxyEndpoint: string; private ruleCache: RuleCache; private fallbackSampler: FallbackSampler; - private samplerDiag: DiagLogger; private rulePoller: NodeJS.Timeout | undefined; + private targetPoller: NodeJS.Timeout | undefined; private clientId: string; private rulePollingJitterMillis: number; + private targetPollingJitterMillis: number; private samplingClient: AWSXRaySamplingClient; constructor(samplerConfig: AWSXRayRemoteSamplerConfig) { - this.samplerDiag = diag; - if ( samplerConfig.pollingInterval == null || samplerConfig.pollingInterval < 10 ) { - this.samplerDiag.warn( + diag.warn( `'pollingInterval' is undefined or too small. Defaulting to ${DEFAULT_RULES_POLLING_INTERVAL_SECONDS} seconds` ); this.rulePollingIntervalMillis = @@ -120,6 +119,8 @@ export class _AWSXRayRemoteSampler implements Sampler { } this.rulePollingJitterMillis = Math.random() * 5 * 1000; + this.targetPollingInterval = this.getDefaultTargetPollingInterval(); + this.targetPollingJitterMillis = (Math.random() / 10) * 1000; this.awsProxyEndpoint = samplerConfig.endpoint ? samplerConfig.endpoint @@ -129,15 +130,17 @@ export class _AWSXRayRemoteSampler implements Sampler { this.clientId = _AWSXRayRemoteSampler.generateClientId(); this.ruleCache = new RuleCache(samplerConfig.resource); - this.samplingClient = new AWSXRaySamplingClient( - this.awsProxyEndpoint, - this.samplerDiag - ); + this.samplingClient = new AWSXRaySamplingClient(this.awsProxyEndpoint); // Start the Sampling Rules poller this.startSamplingRulesPoller(); - // TODO: Start the Sampling Targets poller + // Start the Sampling Targets poller where the first poll occurs after the default interval + this.startSamplingTargetsPoller(); + } + + public getDefaultTargetPollingInterval(): number { + return DEFAULT_TARGET_POLLING_INTERVAL_SECONDS; } public shouldSample( @@ -149,9 +152,7 @@ export class _AWSXRayRemoteSampler implements Sampler { links: Link[] ): SamplingResult { if (this.ruleCache.isExpired()) { - this.samplerDiag.debug( - 'Rule cache is expired, so using fallback sampling strategy' - ); + diag.debug('Rule cache is expired, so using fallback sampling strategy'); return this.fallbackSampler.shouldSample( context, traceId, @@ -176,13 +177,13 @@ export class _AWSXRayRemoteSampler implements Sampler { ); } } catch (e: unknown) { - this.samplerDiag.debug( + diag.debug( 'Unexpected error occurred when trying to match or applying a sampling rule', e ); } - this.samplerDiag.debug( + diag.debug( 'Using fallback sampler as no rule match was found. This is likely due to a bug, since default rule should always match' ); return this.fallbackSampler.shouldSample( @@ -203,6 +204,7 @@ export class _AWSXRayRemoteSampler implements Sampler { public stopPollers() { clearInterval(this.rulePoller); + clearInterval(this.targetPoller); } private startSamplingRulesPoller(): void { @@ -216,6 +218,27 @@ export class _AWSXRayRemoteSampler implements Sampler { this.rulePoller.unref(); } + private startSamplingTargetsPoller(): void { + // Update sampling targets every targetPollingInterval (usually 10 seconds) + this.targetPoller = setInterval( + () => this.getAndUpdateSamplingTargets(), + this.targetPollingInterval * 1000 + this.targetPollingJitterMillis + ); + this.targetPoller.unref(); + } + + private getAndUpdateSamplingTargets(): void { + const requestBody: GetSamplingTargetsBody = { + SamplingStatisticsDocuments: + this.ruleCache.createSamplingStatisticsDocuments(this.clientId), + }; + + this.samplingClient.fetchSamplingTargets( + requestBody, + this.updateSamplingTargets.bind(this) + ); + } + private getAndUpdateSamplingRules(): void { this.samplingClient.fetchSamplingRules(this.updateSamplingRules.bind(this)); } @@ -236,12 +259,47 @@ export class _AWSXRayRemoteSampler implements Sampler { ); this.ruleCache.updateRules(samplingRules); } else { - this.samplerDiag.error( + diag.error( 'SamplingRuleRecords from GetSamplingRules request is not defined' ); } } + private updateSamplingTargets( + responseObject: GetSamplingTargetsResponse + ): void { + try { + const targetDocuments: TargetMap = {}; + + // Create Target-Name-to-Target-Map from sampling targets response + responseObject.SamplingTargetDocuments.forEach( + (newTarget: SamplingTargetDocument) => { + targetDocuments[newTarget.RuleName] = newTarget; + } + ); + + // Update targets in the cache + const [refreshSamplingRules, nextPollingInterval]: [boolean, number] = + this.ruleCache.updateTargets( + targetDocuments, + responseObject.LastRuleModification + ); + this.targetPollingInterval = nextPollingInterval; + clearInterval(this.targetPoller); + this.startSamplingTargetsPoller(); + + if (refreshSamplingRules) { + diag.debug( + 'Performing out-of-band sampling rule polling to fetch updated rules.' + ); + clearInterval(this.rulePoller); + this.startSamplingRulesPoller(); + } + } catch (error: unknown) { + diag.debug('Error occurred when updating Sampling Targets'); + } + } + private static generateClientId(): string { const hexChars: string[] = [ '0', diff --git a/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts b/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts index cf762d454c..630cca5ad0 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts @@ -18,13 +18,22 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Attributes } from '@opentelemetry/api'; +import { Attributes, diag } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; +import { + ISamplingStatistics, + SamplingStatisticsDocument, + SamplingTargetDocument, + TargetMap, +} from './types'; import { SamplingRuleApplier } from './sampling-rule-applier'; // The cache expires 1 hour after the last refresh time. const RULE_CACHE_TTL_MILLIS: number = 60 * 60 * 1000; +// 10 second default sampling targets polling interval +export const DEFAULT_TARGET_POLLING_INTERVAL_SECONDS = 10; + export class RuleCache { private ruleAppliers: SamplingRuleApplier[]; private lastUpdatedEpochMillis: number; @@ -89,4 +98,61 @@ export class RuleCache { this.sortRulesByPriority(); this.lastUpdatedEpochMillis = Date.now(); } + + public createSamplingStatisticsDocuments( + clientId: string + ): SamplingStatisticsDocument[] { + const statisticsDocuments: SamplingStatisticsDocument[] = []; + + this.ruleAppliers.forEach((rule: SamplingRuleApplier) => { + const statistics: ISamplingStatistics = rule.snapshotStatistics(); + const nowInSeconds: number = Math.floor(Date.now() / 1000); + + const samplingStatisticsDoc: SamplingStatisticsDocument = { + ClientID: clientId, + RuleName: rule.samplingRule.RuleName, + Timestamp: nowInSeconds, + RequestCount: statistics.RequestCount, + BorrowCount: statistics.BorrowCount, + SampledCount: statistics.SampleCount, + }; + + statisticsDocuments.push(samplingStatisticsDoc); + }); + return statisticsDocuments; + } + + // Update ruleAppliers based on the targets fetched from X-Ray service + public updateTargets( + targetDocuments: TargetMap, + lastRuleModification: number + ): [boolean, number] { + let minPollingInteral: number | undefined = undefined; + let nextPollingInterval: number = DEFAULT_TARGET_POLLING_INTERVAL_SECONDS; + this.ruleAppliers.forEach((rule: SamplingRuleApplier, index: number) => { + const target: SamplingTargetDocument = + targetDocuments[rule.samplingRule.RuleName]; + if (target) { + this.ruleAppliers[index] = rule.withTarget(target); + if (target.Interval) { + if ( + minPollingInteral === undefined || + minPollingInteral > target.Interval + ) { + minPollingInteral = target.Interval; + } + } + } else { + diag.debug('Invalid sampling target: missing rule name'); + } + }); + + if (minPollingInteral) { + nextPollingInterval = minPollingInteral; + } + + const refreshSamplingRules: boolean = + lastRuleModification * 1000 > this.lastUpdatedEpochMillis; + return [refreshSamplingRules, nextPollingInterval]; + } } diff --git a/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts index a148f2ca98..3307451eb0 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts @@ -54,15 +54,26 @@ import { ATTR_AWS_LAMBDA_INVOKED_ARN, ATTR_CLOUD_RESOURCE_ID, } from './semconv'; -import { ISamplingRule, SamplingTargetDocument } from './types'; +import { RateLimitingSampler } from './rate-limiting-sampler'; +import { + ISamplingRule, + ISamplingStatistics, + SamplingTargetDocument, +} from './types'; import { SamplingRule } from './sampling-rule'; import { Statistics } from './statistics'; import { CLOUD_PLATFORM_MAPPING, attributeMatch, wildcardMatch } from './utils'; +// Max date time in JavaScript +const MAX_DATE_TIME_MILLIS: number = new Date(8_640_000_000_000_000).getTime(); + export class SamplingRuleApplier { public samplingRule: SamplingRule; + private reservoirSampler: RateLimitingSampler; private fixedRateSampler: TraceIdRatioBasedSampler; private statistics: Statistics; + private borrowingEnabled: boolean; + private reservoirExpiryTimeInMillis: number; constructor( samplingRule: ISamplingRule, @@ -74,12 +85,44 @@ export class SamplingRuleApplier { this.fixedRateSampler = new TraceIdRatioBasedSampler( this.samplingRule.FixedRate ); - // TODO: Add Reservoir Sampler (Rate Limiting Sampler) + if (samplingRule.ReservoirSize > 0) { + this.reservoirSampler = new RateLimitingSampler(1); + } else { + this.reservoirSampler = new RateLimitingSampler(0); + } + this.reservoirExpiryTimeInMillis = MAX_DATE_TIME_MILLIS; this.statistics = statistics; this.statistics.resetStatistics(); + this.borrowingEnabled = true; + + if (target) { + this.borrowingEnabled = false; + if (target.ReservoirQuota) { + this.reservoirSampler = new RateLimitingSampler(target.ReservoirQuota); + } - // TODO: Update Sampling Targets using provided `target` parameter + if (target.ReservoirQuotaTTL) { + this.reservoirExpiryTimeInMillis = new Date( + target.ReservoirQuotaTTL * 1000 + ).getTime(); + } else { + this.reservoirExpiryTimeInMillis = Date.now(); + } + + if (target.FixedRate) { + this.fixedRateSampler = new TraceIdRatioBasedSampler(target.FixedRate); + } + } + } + + public withTarget(target: SamplingTargetDocument): SamplingRuleApplier { + const newApplier: SamplingRuleApplier = new SamplingRuleApplier( + this.samplingRule, + this.statistics, + target + ); + return newApplier; } public matches(attributes: Attributes, resource: Resource): boolean { @@ -153,19 +196,45 @@ export class SamplingRuleApplier { attributes: Attributes, links: Link[] ): SamplingResult { - // TODO: Record Sampling Statistics - + let hasBorrowed = false; let result: SamplingResult = { decision: SamplingDecision.NOT_RECORD }; - // TODO: Apply Reservoir Sampling + const nowInMillis: number = Date.now(); + const reservoirExpired: boolean = + nowInMillis >= this.reservoirExpiryTimeInMillis; + + if (!reservoirExpired) { + result = this.reservoirSampler.shouldSample( + context, + traceId, + spanName, + spanKind, + attributes, + links + ); + hasBorrowed = + this.borrowingEnabled && + result.decision !== SamplingDecision.NOT_RECORD; + } if (result.decision === SamplingDecision.NOT_RECORD) { result = this.fixedRateSampler.shouldSample(context, traceId); } + this.statistics.SampleCount += + result.decision !== SamplingDecision.NOT_RECORD ? 1 : 0; + this.statistics.BorrowCount += hasBorrowed ? 1 : 0; + this.statistics.RequestCount += 1; + return result; } + public snapshotStatistics(): ISamplingStatistics { + const statisticsCopy: ISamplingStatistics = { ...this.statistics }; + this.statistics.resetStatistics(); + return statisticsCopy; + } + private getArn( resource: Resource, attributes: Attributes diff --git a/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts index 628b000110..26f99219f9 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts @@ -20,7 +20,6 @@ import { expect } from 'expect'; import * as nock from 'nock'; -import { DiagConsoleLogger } from '@opentelemetry/api'; import { AWSXRaySamplingClient } from '../src/aws-xray-sampling-client'; import { GetSamplingRulesResponse, @@ -36,7 +35,7 @@ describe('AWSXRaySamplingClient', () => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, { SamplingRuleRecords: [] }); - const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + const client = new AWSXRaySamplingClient(TEST_URL); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(0); @@ -46,7 +45,7 @@ describe('AWSXRaySamplingClient', () => { it('testGetInvalidResponse', done => { nock(TEST_URL).post('/GetSamplingRules').reply(200, {}); - const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + const client = new AWSXRaySamplingClient(TEST_URL); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(undefined); @@ -58,7 +57,7 @@ describe('AWSXRaySamplingClient', () => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, { SamplingRuleRecords: [{}] }); - const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + const client = new AWSXRaySamplingClient(TEST_URL); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(1); done(); @@ -69,7 +68,7 @@ describe('AWSXRaySamplingClient', () => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, { SamplingRuleRecords: [{ SamplingRule: {} }] }); - const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + const client = new AWSXRaySamplingClient(TEST_URL); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(1); expect( @@ -123,7 +122,7 @@ describe('AWSXRaySamplingClient', () => { const records = data['SamplingRuleRecords']; nock(TEST_URL).post('/GetSamplingRules').reply(200, data); - const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + const client = new AWSXRaySamplingClient(TEST_URL); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(records.length); @@ -177,7 +176,7 @@ describe('AWSXRaySamplingClient', () => { '/get-sampling-targets-response-sample.json'); nock(TEST_URL).post('/SamplingTargets').reply(200, data); - const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + const client = new AWSXRaySamplingClient(TEST_URL); client.fetchSamplingTargets( data, @@ -198,7 +197,7 @@ describe('AWSXRaySamplingClient', () => { }; nock(TEST_URL).post('/SamplingTargets').reply(200, data); - const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + const client = new AWSXRaySamplingClient(TEST_URL); client.fetchSamplingTargets( data as unknown as GetSamplingTargetsBody, diff --git a/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts index 06ce23cb55..855a9c76d6 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts @@ -18,11 +18,187 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { SpanKind, context } from '@opentelemetry/api'; +import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { expect } from 'expect'; +import * as sinon from 'sinon'; import { FallbackSampler } from '../src/fallback-sampler'; +let clock: sinon.SinonFakeTimers; describe('FallBackSampler', () => { - // TODO: Add tests for Fallback sampler when Rate Limiter is implemented + beforeEach(() => { + clock = sinon.useFakeTimers(Date.now()); + }); + afterEach(() => { + try { + clock.restore(); + } catch { + // do nothing + } + }); + it('testShouldSample', () => { + const sampler = new FallbackSampler(); + + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ); + + // 0 seconds passed, 0 quota available + let sampled = 0; + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + // 0.4 seconds passed, 0.4 quota available + sampled = 0; + clock.tick(0.4 * 1000); + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + // 0.8 seconds passed, 0.8 quota available + sampled = 0; + clock.tick(0.4 * 1000); + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + // 1.2 seconds passed, 1 quota consumed, 0 quota available + sampled = 0; + clock.tick(0.4 * 1000); + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(1); + + // 1.6 seconds passed, 0.4 quota available + sampled = 0; + clock.tick(0.4 * 1000); + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + // 2.0 seconds passed, 0.8 quota available + sampled = 0; + clock.tick(0.4 * 1000); + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + // 2.4 seconds passed, one more quota consumed, 0 quota available + sampled = 0; + clock.tick(0.4 * 1000); + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(1); + + // 100 seconds passed, only one quota can be consumed + sampled = 0; + clock.tick(100 * 1000); + for (let i = 0; i < 30; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(1); + }); it('toString()', () => { expect(new FallbackSampler().toString()).toEqual( diff --git a/incubator/opentelemetry-sampler-aws-xray/test/rate-limiter.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/rate-limiter.test.ts new file mode 100644 index 0000000000..3a062dbcb3 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/rate-limiter.test.ts @@ -0,0 +1,62 @@ +/* + * 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. + */ + +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from 'expect'; +import * as sinon from 'sinon'; +import { RateLimiter } from '../src/rate-limiter'; + +let clock: sinon.SinonFakeTimers; +describe('RateLimiter', () => { + beforeEach(() => { + clock = sinon.useFakeTimers(Date.now()); + }); + afterEach(() => { + clock.restore(); + }); + it('testTake', () => { + const limiter = new RateLimiter(30, 1); + + let spent = 0; + for (let i = 0; i < 100; i++) { + if (limiter.take(1)) { + spent++; + } + } + expect(spent).toEqual(0); + + spent = 0; + clock.tick(0.5 * 1000); + for (let i = 0; i < 100; i++) { + if (limiter.take(1)) { + spent++; + } + } + expect(spent).toEqual(15); + + spent = 0; + clock.tick(1000 * 1000); + for (let i = 0; i < 100; i++) { + if (limiter.take(1)) { + spent++; + } + } + expect(spent).toEqual(30); + }); +}); diff --git a/incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts new file mode 100644 index 0000000000..6662949b88 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts @@ -0,0 +1,216 @@ +/* + * 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. + */ + +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SpanKind, context } from '@opentelemetry/api'; +import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; +import { expect } from 'expect'; +import * as sinon from 'sinon'; +import { RateLimitingSampler } from '../src/rate-limiting-sampler'; + +let clock: sinon.SinonFakeTimers; + +describe('RateLimitingSampler', () => { + beforeEach(() => { + clock = sinon.useFakeTimers(Date.now()); + }); + afterEach(() => { + clock.restore(); + }); + it('testShouldSample', () => { + const sampler = new RateLimitingSampler(30); + + let sampled = 0; + for (let i = 0; i < 100; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + clock.tick(0.5 * 1000); // Move forward half a second + + sampled = 0; + for (let i = 0; i < 100; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(15); + + clock.tick(1 * 1000); // Move forward 1 second + + sampled = 0; + for (let i = 0; i < 100; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(30); + + clock.tick(2.5 * 1000); // Move forward 2.5 seconds + + sampled = 0; + for (let i = 0; i < 100; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(30); + + clock.tick(1000 * 1000); // Move forward 1000 seconds + + sampled = 0; + for (let i = 0; i < 100; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(30); + }); + + it('testShouldSampleWithQuotaOfOne', () => { + const sampler = new RateLimitingSampler(1); + + let sampled = 0; + for (let i = 0; i < 50; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + clock.tick(0.5 * 1000); // Move forward half a second + + sampled = 0; + for (let i = 0; i < 50; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(0); + + clock.tick(0.5 * 1000); + + sampled = 0; + for (let i = 0; i < 50; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(1); + + clock.tick(1000 * 1000); // Move forward 1000 seconds + + sampled = 0; + for (let i = 0; i < 50; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + {}, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled += 1; + } + } + expect(sampled).toEqual(1); + }); + + it('toString()', () => { + expect(new RateLimitingSampler(123).toString()).toEqual( + 'RateLimitingSampler{rate limiting sampling with sampling config of 123 req/sec and 0% of additional requests}' + ); + }); +}); diff --git a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts index 27fecc0670..731bee48fc 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts @@ -40,9 +40,11 @@ import { AWSXRaySamplingClient } from '../src/aws-xray-sampling-client'; const DATA_DIR_SAMPLING_RULES = __dirname + '/data/test-remote-sampler_sampling-rules-response-sample.json'; +const DATA_DIR_SAMPLING_TARGETS = + __dirname + '/data/test-remote-sampler_sampling-targets-response-sample.json'; const TEST_URL = 'http://localhost:2000'; -describe('AWSXRayRemoteSampler', () => { +describe('AwsXrayRemoteSampler', () => { let sampler: AWSXRayRemoteSampler; afterEach(() => { @@ -110,11 +112,13 @@ describe('AWSXRayRemoteSampler', () => { expect(sampler['internalXraySampler']['clientId']).toMatch(/[a-f0-9]{24}/); }); - it('testUpdateSamplingRulesAndTargetsWithPollersAndShouldSample', done => { + it('testUpdateSamplingRulesAndTargetsWithPollersAndShouldSampled', done => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, require(DATA_DIR_SAMPLING_RULES)); - + nock(TEST_URL) + .post('/SamplingTargets') + .reply(200, require(DATA_DIR_SAMPLING_TARGETS)); const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'test-service-name', [SEMRESATTRS_CLOUD_PLATFORM]: 'test-cloud-platform', @@ -140,8 +144,171 @@ describe('AWSXRayRemoteSampler', () => { ).decision ).toEqual(SamplingDecision.NOT_RECORD); - // TODO: Run more tests after updating Sampling Targets - done(); + sampler['internalXraySampler']['getAndUpdateSamplingTargets'](); + + setTimeout(() => { + expect( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + { abc: '1234' }, + [] + ).decision + ).toEqual(SamplingDecision.RECORD_AND_SAMPLED); + expect( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + { abc: '1234' }, + [] + ).decision + ).toEqual(SamplingDecision.RECORD_AND_SAMPLED); + expect( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + { abc: '1234' }, + [] + ).decision + ).toEqual(SamplingDecision.RECORD_AND_SAMPLED); + + done(); + }, 50); + }, 50); + }); + + it('testLargeReservoir', done => { + nock(TEST_URL) + .post('/GetSamplingRules') + .reply(200, require(DATA_DIR_SAMPLING_RULES)); + nock(TEST_URL) + .post('/SamplingTargets') + .reply(200, require(DATA_DIR_SAMPLING_TARGETS)); + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'test-service-name', + [SEMRESATTRS_CLOUD_PLATFORM]: 'test-cloud-platform', + }); + const attributes = { abc: '1234' }; + + sampler = new AWSXRayRemoteSampler({ + resource: resource, + }); + sampler['internalXraySampler']['getAndUpdateSamplingRules'](); + + setTimeout(() => { + expect( + sampler['internalXraySampler']['ruleCache']['ruleAppliers'][0] + .samplingRule.RuleName + ).toEqual('test'); + expect( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + attributes, + [] + ).decision + ).toEqual(SamplingDecision.NOT_RECORD); + sampler['internalXraySampler']['getAndUpdateSamplingTargets'](); + + setTimeout(() => { + const clock = sinon.useFakeTimers(Date.now()); + clock.tick(1500); + let sampled = 0; + for (let i = 0; i < 1005; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + attributes, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled++; + } + } + clock.restore(); + + expect( + sampler['internalXraySampler']['ruleCache']['ruleAppliers'][0][ + 'reservoirSampler' + ]['quota'] + ).toEqual(1000); + expect(sampled).toEqual(1000); + done(); + }, 50); + }, 50); + }); + + it('testSomeReservoir', done => { + nock(TEST_URL) + .post('/GetSamplingRules') + .reply(200, require(DATA_DIR_SAMPLING_RULES)); + nock(TEST_URL) + .post('/SamplingTargets') + .reply(200, require(DATA_DIR_SAMPLING_TARGETS)); + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'test-service-name', + [SEMRESATTRS_CLOUD_PLATFORM]: 'test-cloud-platform', + }); + const attributes = { + abc: 'non-matching attribute value, use default rule', + }; + + sampler = new AWSXRayRemoteSampler({ + resource: resource, + }); + sampler['internalXraySampler']['getAndUpdateSamplingRules'](); + + setTimeout(() => { + expect( + sampler['internalXraySampler']['ruleCache']['ruleAppliers'][0] + .samplingRule.RuleName + ).toEqual('test'); + expect( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + attributes, + [] + ).decision + ).toEqual(SamplingDecision.NOT_RECORD); + sampler['internalXraySampler']['getAndUpdateSamplingTargets'](); + + setTimeout(() => { + const clock = sinon.useFakeTimers(Date.now()); + clock.tick(1000); + let sampled = 0; + for (let i = 0; i < 1000; i++) { + if ( + sampler.shouldSample( + context.active(), + '1234', + 'name', + SpanKind.CLIENT, + attributes, + [] + ).decision !== SamplingDecision.NOT_RECORD + ) { + sampled++; + } + } + clock.restore(); + + expect(sampled).toEqual(100); + done(); + }, 50); }, 50); }); @@ -159,7 +326,81 @@ describe('AWSXRayRemoteSampler', () => { ); }); - // TODO: Run tests for Reservoir Sampling and Sampling Statistics + it('ParentBased AWSXRayRemoteSampler creates expected Statistics from the 1 Span with no Parent, disregarding 2 Child Spans', done => { + const defaultRuleDir = + __dirname + '/data/get-sampling-rules-response-sample-sample-all.json'; + nock(TEST_URL) + .post('/GetSamplingRules') + .reply(200, require(defaultRuleDir)); + + const sampler: AWSXRayRemoteSampler = new AWSXRayRemoteSampler({ + resource: emptyResource(), + }); + const tracerProvider: NodeTracerProvider = new NodeTracerProvider({ + sampler: sampler, + }); + const tracer: Tracer = tracerProvider.getTracer('test'); + + setTimeout(() => { + const span0 = tracer.startSpan('test0'); + const ctx = trace.setSpan(context.active(), span0); + const span1: Span = tracer.startSpan('test1', {}, ctx); + const span2: Span = tracer.startSpan('test2', {}, ctx); + span2.end(); + span1.end(); + span0.end(); + + // span1 and span2 are child spans of root span0 + // For AWSXRayRemoteSampler (ParentBased), expect only span0 to update statistics + expect( + sampler['internalXraySampler']['ruleCache']['ruleAppliers'][0][ + 'statistics' + ].RequestCount + ).toBe(1); + expect( + sampler['internalXraySampler']['ruleCache']['ruleAppliers'][0][ + 'statistics' + ].SampleCount + ).toBe(1); + done(); + }, 50); + }); + + it('Non-ParentBased _AWSXRayRemoteSampler creates expected Statistics based on all 3 Spans, disregarding Parent Span Sampling Decision', done => { + const defaultRuleDir = + __dirname + '/data/get-sampling-rules-response-sample-sample-all.json'; + nock(TEST_URL) + .post('/GetSamplingRules') + .reply(200, require(defaultRuleDir)); + + const sampler: _AWSXRayRemoteSampler = new _AWSXRayRemoteSampler({ + resource: emptyResource(), + }); + const tracerProvider: NodeTracerProvider = new NodeTracerProvider({ + sampler: sampler, + }); + const tracer: Tracer = tracerProvider.getTracer('test'); + + setTimeout(() => { + const span0 = tracer.startSpan('test0'); + const ctx = trace.setSpan(context.active(), span0); + const span1: Span = tracer.startSpan('test1', {}, ctx); + const span2: Span = tracer.startSpan('test2', {}, ctx); + span2.end(); + span1.end(); + span0.end(); + + // span1 and span2 are child spans of root span0 + // For _AWSXRayRemoteSampler (Non-ParentBased), expect all 3 spans to update statistics + expect( + sampler['ruleCache']['ruleAppliers'][0]['statistics'].RequestCount + ).toBe(3); + expect( + sampler['ruleCache']['ruleAppliers'][0]['statistics'].SampleCount + ).toBe(3); + done(); + }, 50); + }); }); describe('_AWSXRayRemoteSampler', () => { diff --git a/incubator/opentelemetry-sampler-aws-xray/test/rule-cache.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/rule-cache.test.ts index ae3cfdc725..952a0f9982 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/rule-cache.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/rule-cache.test.ts @@ -143,5 +143,116 @@ describe('RuleCache', () => { ); }); - // TODO: Add tests for updating Sampling Targets and getting statistics + it('testUpdateSamplingTargets', () => { + const rule1 = createRule('default', 10000, 1, 0.05); + const rule2 = createRule('test', 20, 10, 0.2); + const cache = new RuleCache(emptyResource()); + cache.updateRules([rule1, rule2]); + + expect(cache['ruleAppliers'][0]['reservoirSampler']['quota']).toEqual(1); + expect(cache['ruleAppliers'][0]['fixedRateSampler']['_ratio']).toEqual( + rule2.samplingRule.FixedRate + ); + + expect(cache['ruleAppliers'][1]['reservoirSampler']['quota']).toEqual(1); + expect(cache['ruleAppliers'][1]['fixedRateSampler']['_ratio']).toEqual( + rule1.samplingRule.FixedRate + ); + + const time = Date.now() / 1000; + const target1 = { + FixedRate: 0.05, + Interval: 15, + ReservoirQuota: 1, + ReservoirQuotaTTL: time + 10, + RuleName: 'default', + }; + const target2 = { + FixedRate: 0.15, + Interval: 12, + ReservoirQuota: 5, + ReservoirQuotaTTL: time + 10, + RuleName: 'test', + }; + const target3 = { + FixedRate: 0.15, + Interval: 3, + ReservoirQuota: 5, + ReservoirQuotaTTL: time + 10, + RuleName: 'associated rule does not exist', + }; + + const targetMap = { + default: target1, + test: target2, + 'associated rule does not exist': target3, + }; + const [refreshSamplingRules, nextPollingInterval] = cache.updateTargets( + targetMap, + time - 10 + ); + expect(refreshSamplingRules).toEqual(false); + expect(nextPollingInterval).toEqual(target2.Interval); + + // Ensure cache is still of length 2 + expect(cache['ruleAppliers'].length).toEqual(2); + + expect(cache['ruleAppliers'][0]['reservoirSampler']['quota']).toEqual( + target2.ReservoirQuota + ); + expect(cache['ruleAppliers'][0]['fixedRateSampler']['_ratio']).toEqual( + target2.FixedRate + ); + expect(cache['ruleAppliers'][1]['reservoirSampler']['quota']).toEqual( + target1.ReservoirQuota + ); + expect(cache['ruleAppliers'][1]['fixedRateSampler']['_ratio']).toEqual( + target1.FixedRate + ); + + const [refreshSamplingRulesAfter, _] = cache.updateTargets( + targetMap, + time + 1 + ); + expect(refreshSamplingRulesAfter).toBe(true); + }); + + it('testGetAllStatistics', () => { + const time = Date.now(); + const clock = sinon.useFakeTimers(time); + + const rule1 = createRule('test', 4, 2, 2.0); + const rule2 = createRule('default', 5, 5, 5.0); + + const cache = new RuleCache(emptyResource()); + cache.updateRules([rule1, rule2]); + + clock.tick(1); // ms + + const clientId = '12345678901234567890abcd'; + const statistics = cache.createSamplingStatisticsDocuments( + '12345678901234567890abcd' + ); + + // 1 ms should not be big enough to expect a timestamp difference + expect(statistics).toEqual([ + { + ClientID: clientId, + RuleName: 'test', + Timestamp: Math.floor(time / 1000), + RequestCount: 0, + BorrowCount: 0, + SampledCount: 0, + }, + { + ClientID: clientId, + RuleName: 'default', + Timestamp: Math.floor(time / 1000), + RequestCount: 0, + BorrowCount: 0, + SampledCount: 0, + }, + ]); + clock.restore(); + }); }); From c8a8bb9a98b40d8955c1e07a708aec2137a531c7 Mon Sep 17 00:00:00 2001 From: jjllee Date: Thu, 3 Jul 2025 14:05:59 -0700 Subject: [PATCH 2/5] Update diag usage --- .../src/aws-xray-sampling-client.ts | 16 +++++--- .../src/remote-sampler.ts | 37 ++++++++++++++----- .../test/aws-xray-sampling-client.test.ts | 15 ++++---- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts b/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts index 43d4d1226b..2b72e1337d 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts @@ -18,7 +18,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { DiagLogFunction, context, diag } from '@opentelemetry/api'; +import { DiagLogFunction, DiagLogger, context } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import * as http from 'http'; import { @@ -30,10 +30,12 @@ import { export class AWSXRaySamplingClient { private getSamplingRulesEndpoint: string; private samplingTargetsEndpoint: string; + private samplerDiag: DiagLogger; - constructor(endpoint: string) { + constructor(endpoint: string, samplerDiag: DiagLogger) { this.getSamplingRulesEndpoint = endpoint + '/GetSamplingRules'; this.samplingTargetsEndpoint = endpoint + '/SamplingTargets'; + this.samplerDiag = samplerDiag; } public fetchSamplingTargets( @@ -43,7 +45,7 @@ export class AWSXRaySamplingClient { this.makeSamplingRequest( this.samplingTargetsEndpoint, callback, - diag.debug, + this.samplerDiag.debug.bind(this.samplerDiag), JSON.stringify(requestBody) ); } @@ -54,7 +56,7 @@ export class AWSXRaySamplingClient { this.makeSamplingRequest( this.getSamplingRulesEndpoint, callback, - diag.error + this.samplerDiag.error.bind(this.samplerDiag) ); } @@ -96,8 +98,10 @@ export class AWSXRaySamplingClient { callback(responseObject); } } else { - diag.debug(`${url} Response Code is: ${response.statusCode}`); - diag.debug(`${url} responseData is: ${responseData}`); + this.samplerDiag.debug( + `${url} Response Code is: ${response.statusCode}` + ); + this.samplerDiag.debug(`${url} responseData is: ${responseData}`); } }); }) diff --git a/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts b/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts index ffd5b2c949..d6876bf60e 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts @@ -18,7 +18,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Attributes, Context, Link, SpanKind, diag } from '@opentelemetry/api'; +import { + Attributes, + Context, + DiagLogger, + Link, + SpanKind, + diag, +} from '@opentelemetry/api'; import { ParentBasedSampler, Sampler, @@ -39,7 +46,9 @@ import { DEFAULT_TARGET_POLLING_INTERVAL_SECONDS, RuleCache, } from './rule-cache'; + import { SamplingRuleApplier } from './sampling-rule-applier'; +import { PACKAGE_NAME } from './version'; // 5 minute default sampling rules polling interval const DEFAULT_RULES_POLLING_INTERVAL_SECONDS: number = 5 * 60; @@ -97,6 +106,7 @@ export class _AWSXRayRemoteSampler implements Sampler { private awsProxyEndpoint: string; private ruleCache: RuleCache; private fallbackSampler: FallbackSampler; + private samplerDiag: DiagLogger; private rulePoller: NodeJS.Timeout | undefined; private targetPoller: NodeJS.Timeout | undefined; private clientId: string; @@ -105,11 +115,15 @@ export class _AWSXRayRemoteSampler implements Sampler { private samplingClient: AWSXRaySamplingClient; constructor(samplerConfig: AWSXRayRemoteSamplerConfig) { + this.samplerDiag = diag.createComponentLogger({ + namespace: PACKAGE_NAME, + }); + if ( samplerConfig.pollingInterval == null || samplerConfig.pollingInterval < 10 ) { - diag.warn( + this.samplerDiag.warn( `'pollingInterval' is undefined or too small. Defaulting to ${DEFAULT_RULES_POLLING_INTERVAL_SECONDS} seconds` ); this.rulePollingIntervalMillis = @@ -130,7 +144,10 @@ export class _AWSXRayRemoteSampler implements Sampler { this.clientId = _AWSXRayRemoteSampler.generateClientId(); this.ruleCache = new RuleCache(samplerConfig.resource); - this.samplingClient = new AWSXRaySamplingClient(this.awsProxyEndpoint); + this.samplingClient = new AWSXRaySamplingClient( + this.awsProxyEndpoint, + this.samplerDiag + ); // Start the Sampling Rules poller this.startSamplingRulesPoller(); @@ -152,7 +169,9 @@ export class _AWSXRayRemoteSampler implements Sampler { links: Link[] ): SamplingResult { if (this.ruleCache.isExpired()) { - diag.debug('Rule cache is expired, so using fallback sampling strategy'); + this.samplerDiag.debug( + 'Rule cache is expired, so using fallback sampling strategy' + ); return this.fallbackSampler.shouldSample( context, traceId, @@ -177,13 +196,13 @@ export class _AWSXRayRemoteSampler implements Sampler { ); } } catch (e: unknown) { - diag.debug( + this.samplerDiag.debug( 'Unexpected error occurred when trying to match or applying a sampling rule', e ); } - diag.debug( + this.samplerDiag.debug( 'Using fallback sampler as no rule match was found. This is likely due to a bug, since default rule should always match' ); return this.fallbackSampler.shouldSample( @@ -259,7 +278,7 @@ export class _AWSXRayRemoteSampler implements Sampler { ); this.ruleCache.updateRules(samplingRules); } else { - diag.error( + this.samplerDiag.error( 'SamplingRuleRecords from GetSamplingRules request is not defined' ); } @@ -289,14 +308,14 @@ export class _AWSXRayRemoteSampler implements Sampler { this.startSamplingTargetsPoller(); if (refreshSamplingRules) { - diag.debug( + this.samplerDiag.debug( 'Performing out-of-band sampling rule polling to fetch updated rules.' ); clearInterval(this.rulePoller); this.startSamplingRulesPoller(); } } catch (error: unknown) { - diag.debug('Error occurred when updating Sampling Targets'); + this.samplerDiag.debug('Error occurred when updating Sampling Targets'); } } diff --git a/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts index 26f99219f9..628b000110 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts @@ -20,6 +20,7 @@ import { expect } from 'expect'; import * as nock from 'nock'; +import { DiagConsoleLogger } from '@opentelemetry/api'; import { AWSXRaySamplingClient } from '../src/aws-xray-sampling-client'; import { GetSamplingRulesResponse, @@ -35,7 +36,7 @@ describe('AWSXRaySamplingClient', () => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, { SamplingRuleRecords: [] }); - const client = new AWSXRaySamplingClient(TEST_URL); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(0); @@ -45,7 +46,7 @@ describe('AWSXRaySamplingClient', () => { it('testGetInvalidResponse', done => { nock(TEST_URL).post('/GetSamplingRules').reply(200, {}); - const client = new AWSXRaySamplingClient(TEST_URL); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(undefined); @@ -57,7 +58,7 @@ describe('AWSXRaySamplingClient', () => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, { SamplingRuleRecords: [{}] }); - const client = new AWSXRaySamplingClient(TEST_URL); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(1); done(); @@ -68,7 +69,7 @@ describe('AWSXRaySamplingClient', () => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, { SamplingRuleRecords: [{ SamplingRule: {} }] }); - const client = new AWSXRaySamplingClient(TEST_URL); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(1); expect( @@ -122,7 +123,7 @@ describe('AWSXRaySamplingClient', () => { const records = data['SamplingRuleRecords']; nock(TEST_URL).post('/GetSamplingRules').reply(200, data); - const client = new AWSXRaySamplingClient(TEST_URL); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); client.fetchSamplingRules((response: GetSamplingRulesResponse) => { expect(response.SamplingRuleRecords?.length).toEqual(records.length); @@ -176,7 +177,7 @@ describe('AWSXRaySamplingClient', () => { '/get-sampling-targets-response-sample.json'); nock(TEST_URL).post('/SamplingTargets').reply(200, data); - const client = new AWSXRaySamplingClient(TEST_URL); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); client.fetchSamplingTargets( data, @@ -197,7 +198,7 @@ describe('AWSXRaySamplingClient', () => { }; nock(TEST_URL).post('/SamplingTargets').reply(200, data); - const client = new AWSXRaySamplingClient(TEST_URL); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); client.fetchSamplingTargets( data as unknown as GetSamplingTargetsBody, From 496cc325508e6b8968f8a3cfd38c2bcdd3ce28d2 Mon Sep 17 00:00:00 2001 From: jjllee Date: Thu, 3 Jul 2025 14:26:57 -0700 Subject: [PATCH 3/5] undo unit test renaming --- .../test/remote-sampler.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts index 731bee48fc..a27bca61d0 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts @@ -44,7 +44,7 @@ const DATA_DIR_SAMPLING_TARGETS = __dirname + '/data/test-remote-sampler_sampling-targets-response-sample.json'; const TEST_URL = 'http://localhost:2000'; -describe('AwsXrayRemoteSampler', () => { +describe('AWSXRayRemoteSampler', () => { let sampler: AWSXRayRemoteSampler; afterEach(() => { @@ -112,7 +112,7 @@ describe('AwsXrayRemoteSampler', () => { expect(sampler['internalXraySampler']['clientId']).toMatch(/[a-f0-9]{24}/); }); - it('testUpdateSamplingRulesAndTargetsWithPollersAndShouldSampled', done => { + it('testUpdateSamplingRulesAndTargetsWithPollersAndShouldSample', done => { nock(TEST_URL) .post('/GetSamplingRules') .reply(200, require(DATA_DIR_SAMPLING_RULES)); From e2f97f55e58bc2618d7caed89ee8eaf872395b6d Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Mon, 21 Jul 2025 15:15:32 -0700 Subject: [PATCH 4/5] address comments --- .../src/aws-xray-sampling-client.ts | 4 +- .../src/fallback-sampler.ts | 6 +-- .../src/rate-limiter.ts | 4 +- .../src/rule-cache.ts | 19 ++++---- .../src/sampling-rule-applier.ts | 6 +-- .../test/fallback-sampler.test.ts | 25 +++++----- .../test/rate-limiting-sampler.test.ts | 19 ++++---- .../test/remote-sampler.test.ts | 48 +++++++++++++++---- 8 files changed, 81 insertions(+), 50 deletions(-) diff --git a/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts b/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts index 2b72e1337d..63e90c29fb 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts @@ -45,7 +45,7 @@ export class AWSXRaySamplingClient { this.makeSamplingRequest( this.samplingTargetsEndpoint, callback, - this.samplerDiag.debug.bind(this.samplerDiag), + (message: string) => this.samplerDiag.debug(message), JSON.stringify(requestBody) ); } @@ -56,7 +56,7 @@ export class AWSXRaySamplingClient { this.makeSamplingRequest( this.getSamplingRulesEndpoint, callback, - this.samplerDiag.error.bind(this.samplerDiag) + (message: string) => this.samplerDiag.error(message) ); } diff --git a/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts b/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts index 660ef37fe8..11aabd0ac7 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/fallback-sampler.ts @@ -32,9 +32,9 @@ export class FallbackSampler implements Sampler { private fixedRateSampler: TraceIdRatioBasedSampler; private rateLimitingSampler: RateLimitingSampler; - constructor() { - this.fixedRateSampler = new TraceIdRatioBasedSampler(0.05); - this.rateLimitingSampler = new RateLimitingSampler(1); + constructor(ratio = 0.05, quota = 1) { + this.fixedRateSampler = new TraceIdRatioBasedSampler(ratio); + this.rateLimitingSampler = new RateLimitingSampler(quota); } shouldSample( diff --git a/incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts b/incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts index fab426cd99..2c407fce6c 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/rate-limiter.ts @@ -42,10 +42,8 @@ export class RateLimiter { return false; } - const quotaPerMillis: number = this.quota / 1000.0; - // assume divide by zero not possible - const costInMillis: number = cost / quotaPerMillis; + const costInMillis: number = (cost * 1000.0) / this.quota; const walletCeilingMillis: number = Date.now(); let currentBalanceMillis: number = diff --git a/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts b/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts index 630cca5ad0..05960c3f44 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/rule-cache.ts @@ -127,28 +127,29 @@ export class RuleCache { targetDocuments: TargetMap, lastRuleModification: number ): [boolean, number] { - let minPollingInteral: number | undefined = undefined; + let minPollingInterval: number | undefined = undefined; let nextPollingInterval: number = DEFAULT_TARGET_POLLING_INTERVAL_SECONDS; - this.ruleAppliers.forEach((rule: SamplingRuleApplier, index: number) => { + + for (const [index, rule] of this.ruleAppliers.entries()) { const target: SamplingTargetDocument = targetDocuments[rule.samplingRule.RuleName]; if (target) { this.ruleAppliers[index] = rule.withTarget(target); - if (target.Interval) { + if (typeof target.Interval === 'number') { if ( - minPollingInteral === undefined || - minPollingInteral > target.Interval + minPollingInterval === undefined || + minPollingInterval > target.Interval ) { - minPollingInteral = target.Interval; + minPollingInterval = target.Interval; } } } else { diag.debug('Invalid sampling target: missing rule name'); } - }); + } - if (minPollingInteral) { - nextPollingInterval = minPollingInteral; + if (typeof minPollingInterval === 'number') { + nextPollingInterval = minPollingInterval; } const refreshSamplingRules: boolean = diff --git a/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts index 3307451eb0..9a4fd3c1ba 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts @@ -98,11 +98,11 @@ export class SamplingRuleApplier { if (target) { this.borrowingEnabled = false; - if (target.ReservoirQuota) { + if (typeof target.ReservoirQuota === 'number') { this.reservoirSampler = new RateLimitingSampler(target.ReservoirQuota); } - if (target.ReservoirQuotaTTL) { + if (typeof target.ReservoirQuotaTTL === 'number') { this.reservoirExpiryTimeInMillis = new Date( target.ReservoirQuotaTTL * 1000 ).getTime(); @@ -110,7 +110,7 @@ export class SamplingRuleApplier { this.reservoirExpiryTimeInMillis = Date.now(); } - if (target.FixedRate) { + if (typeof target.FixedRate === 'number') { this.fixedRateSampler = new TraceIdRatioBasedSampler(target.FixedRate); } } diff --git a/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts index 855a9c76d6..400bdfb18a 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/fallback-sampler.test.ts @@ -23,8 +23,10 @@ import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { expect } from 'expect'; import * as sinon from 'sinon'; import { FallbackSampler } from '../src/fallback-sampler'; +import { testTraceId } from './remote-sampler.test'; let clock: sinon.SinonFakeTimers; + describe('FallBackSampler', () => { beforeEach(() => { clock = sinon.useFakeTimers(Date.now()); @@ -36,12 +38,13 @@ describe('FallBackSampler', () => { // do nothing } }); - it('testShouldSample', () => { - const sampler = new FallbackSampler(); + it('testShouldSampleWithQuotaOnly', () => { + // Ensure FallbackSampler's internal TraceIdRatioBasedSampler will always return SamplingDecision.NOT_RECORD + const sampler = new FallbackSampler(0); sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -54,7 +57,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -73,7 +76,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -92,7 +95,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -111,7 +114,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -130,7 +133,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -149,7 +152,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -168,7 +171,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -187,7 +190,7 @@ describe('FallBackSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, diff --git a/incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts index 6662949b88..73adc55b45 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/rate-limiting-sampler.test.ts @@ -23,6 +23,7 @@ import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { expect } from 'expect'; import * as sinon from 'sinon'; import { RateLimitingSampler } from '../src/rate-limiting-sampler'; +import { testTraceId } from './remote-sampler.test'; let clock: sinon.SinonFakeTimers; @@ -41,7 +42,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -60,7 +61,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -79,7 +80,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -98,7 +99,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -117,7 +118,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -138,7 +139,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -157,7 +158,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -176,7 +177,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, @@ -195,7 +196,7 @@ describe('RateLimitingSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, {}, diff --git a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts index a27bca61d0..40f6281e67 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts @@ -22,7 +22,16 @@ import { resourceFromAttributes, emptyResource, } from '@opentelemetry/resources'; -import { context, Span, SpanKind, Tracer, trace } from '@opentelemetry/api'; +import { + context, + Span, + SpanKind, + Tracer, + trace, + Attributes, + Link, + Context, +} from '@opentelemetry/api'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { @@ -43,11 +52,13 @@ const DATA_DIR_SAMPLING_RULES = const DATA_DIR_SAMPLING_TARGETS = __dirname + '/data/test-remote-sampler_sampling-targets-response-sample.json'; const TEST_URL = 'http://localhost:2000'; +export const testTraceId = '0af7651916cd43dd8448eb211c80319c'; describe('AWSXRayRemoteSampler', () => { let sampler: AWSXRayRemoteSampler; afterEach(() => { + sinon.restore(); if (sampler != null) { sampler.stopPollers(); } @@ -136,7 +147,7 @@ describe('AWSXRayRemoteSampler', () => { expect( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, { abc: '1234' }, @@ -150,7 +161,7 @@ describe('AWSXRayRemoteSampler', () => { expect( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, { abc: '1234' }, @@ -160,7 +171,7 @@ describe('AWSXRayRemoteSampler', () => { expect( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, { abc: '1234' }, @@ -170,7 +181,7 @@ describe('AWSXRayRemoteSampler', () => { expect( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, { abc: '1234' }, @@ -209,7 +220,7 @@ describe('AWSXRayRemoteSampler', () => { expect( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, attributes, @@ -226,7 +237,7 @@ describe('AWSXRayRemoteSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, attributes, @@ -267,6 +278,23 @@ describe('AWSXRayRemoteSampler', () => { sampler = new AWSXRayRemoteSampler({ resource: resource, }); + sinon + .stub(sampler['internalXraySampler']['fallbackSampler'], 'shouldSample') + .callsFake( + ( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ) => { + return { + decision: SamplingDecision.NOT_RECORD, + attributes: attributes, + }; + } + ); sampler['internalXraySampler']['getAndUpdateSamplingRules'](); setTimeout(() => { @@ -277,13 +305,13 @@ describe('AWSXRayRemoteSampler', () => { expect( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, attributes, [] ).decision - ).toEqual(SamplingDecision.NOT_RECORD); + ).toEqual(SamplingDecision.RECORD_AND_SAMPLED); sampler['internalXraySampler']['getAndUpdateSamplingTargets'](); setTimeout(() => { @@ -294,7 +322,7 @@ describe('AWSXRayRemoteSampler', () => { if ( sampler.shouldSample( context.active(), - '1234', + testTraceId, 'name', SpanKind.CLIENT, attributes, From 39f6360b636ff5e2d7d744efbbfcdeeb739fa74e Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 22 Jul 2025 13:32:00 -0700 Subject: [PATCH 5/5] update unit test variables --- .../test/remote-sampler.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts index 40f6281e67..48f2e21a09 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts @@ -65,7 +65,7 @@ describe('AWSXRayRemoteSampler', () => { }); it('testCreateRemoteSamplerWithEmptyResource', () => { - const sampler: AWSXRayRemoteSampler = new AWSXRayRemoteSampler({ + sampler = new AWSXRayRemoteSampler({ resource: emptyResource(), }); @@ -361,7 +361,7 @@ describe('AWSXRayRemoteSampler', () => { .post('/GetSamplingRules') .reply(200, require(defaultRuleDir)); - const sampler: AWSXRayRemoteSampler = new AWSXRayRemoteSampler({ + sampler = new AWSXRayRemoteSampler({ resource: emptyResource(), }); const tracerProvider: NodeTracerProvider = new NodeTracerProvider({ @@ -401,11 +401,13 @@ describe('AWSXRayRemoteSampler', () => { .post('/GetSamplingRules') .reply(200, require(defaultRuleDir)); - const sampler: _AWSXRayRemoteSampler = new _AWSXRayRemoteSampler({ + sampler = new AWSXRayRemoteSampler({ resource: emptyResource(), }); + const internalSampler: _AWSXRayRemoteSampler = + sampler['internalXraySampler']; const tracerProvider: NodeTracerProvider = new NodeTracerProvider({ - sampler: sampler, + sampler: internalSampler, }); const tracer: Tracer = tracerProvider.getTracer('test'); @@ -421,10 +423,12 @@ describe('AWSXRayRemoteSampler', () => { // span1 and span2 are child spans of root span0 // For _AWSXRayRemoteSampler (Non-ParentBased), expect all 3 spans to update statistics expect( - sampler['ruleCache']['ruleAppliers'][0]['statistics'].RequestCount + internalSampler['ruleCache']['ruleAppliers'][0]['statistics'] + .RequestCount ).toBe(3); expect( - sampler['ruleCache']['ruleAppliers'][0]['statistics'].SampleCount + internalSampler['ruleCache']['ruleAppliers'][0]['statistics'] + .SampleCount ).toBe(3); done(); }, 50);