diff --git a/incubator/opentelemetry-sampler-aws-xray/package.json b/incubator/opentelemetry-sampler-aws-xray/package.json index 551419838d..d79382921d 100644 --- a/incubator/opentelemetry-sampler-aws-xray/package.json +++ b/incubator/opentelemetry-sampler-aws-xray/package.json @@ -41,16 +41,17 @@ "watch": "tsc --build --watch tsconfig.json tsconfig.esm.json" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.9.0" }, "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/sdk-trace-base": "^1.8.0", - "axios": "^1.3.5" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "devDependencies": { - "@opentelemetry/api": "^1.3.0", - "@opentelemetry/contrib-test-utils": "^0.35.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-trace-node": "^2.0.0", "@types/mocha": "10.0.10", "@types/node": "18.18.14", "@types/sinon": "17.0.4", @@ -64,6 +65,6 @@ "typescript": "5.0.4" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" } } 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 new file mode 100644 index 0000000000..75c299295b --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/src/aws-xray-sampling-client.ts @@ -0,0 +1,118 @@ +/* + * 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 { DiagLogFunction, DiagLogger, context } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; +import * as http from 'http'; +import { + GetSamplingRulesResponse, + GetSamplingTargetsBody, + GetSamplingTargetsResponse, +} from './types'; + +export class AWSXRaySamplingClient { + private getSamplingRulesEndpoint: string; + private samplingTargetsEndpoint: string; + private samplerDiag: DiagLogger; + + constructor(endpoint: string, samplerDiag: DiagLogger) { + this.getSamplingRulesEndpoint = endpoint + '/GetSamplingRules'; + this.samplingTargetsEndpoint = endpoint + '/SamplingTargets'; + this.samplerDiag = samplerDiag; + } + + public fetchSamplingTargets( + requestBody: GetSamplingTargetsBody, + callback: (responseObject: GetSamplingTargetsResponse) => void + ) { + this.makeSamplingRequest( + this.samplingTargetsEndpoint, + callback, + this.samplerDiag.debug, + JSON.stringify(requestBody) + ); + } + + public fetchSamplingRules( + callback: (responseObject: GetSamplingRulesResponse) => void + ) { + this.makeSamplingRequest( + this.getSamplingRulesEndpoint, + callback, + this.samplerDiag.error + ); + } + + private makeSamplingRequest( + url: string, + callback: (responseObject: T) => void, + logger: DiagLogFunction, + requestBodyJsonString?: string + ): void { + const options: http.RequestOptions = { + method: 'POST', + headers: {}, + }; + + if (requestBodyJsonString) { + options.headers = { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestBodyJsonString), + }; + } + + // Ensure AWS X-Ray Sampler does not generate traces itself + context.with(suppressTracing(context.active()), () => { + const req: http.ClientRequest = http + .request(url, options, response => { + response.setEncoding('utf-8'); + let responseData = ''; + response.on('data', dataChunk => (responseData += dataChunk)); + response.on('end', () => { + if (response.statusCode === 200 && responseData.length > 0) { + let responseObject: T | undefined = undefined; + try { + responseObject = JSON.parse(responseData) as T; + } catch (e: unknown) { + logger(`Error occurred when parsing responseData from ${url}`); + } + + if (responseObject) { + callback(responseObject); + } + } else { + this.samplerDiag.debug( + `${url} Response Code is: ${response.statusCode}` + ); + this.samplerDiag.debug(`${url} responseData is: ${responseData}`); + } + }); + }) + .on('error', (error: unknown) => { + logger(`Error occurred when making an HTTP POST to ${url}: ${error}`); + }); + if (requestBodyJsonString) { + req.end(requestBodyJsonString); + } else { + req.end(); + } + }); + } +} diff --git a/incubator/opentelemetry-sampler-aws-xray/src/index.ts b/incubator/opentelemetry-sampler-aws-xray/src/index.ts index eb37ee9739..7051864c14 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/index.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ export * from './remote-sampler'; -export { AWSXRaySamplerConfig } from './types'; +export { AWSXRayRemoteSamplerConfig } from './types'; diff --git a/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts b/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts index efa169c52a..01f4f253bf 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/remote-sampler.ts @@ -14,101 +14,179 @@ * 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, + DiagLogger, + Link, + SpanKind, + diag, +} from '@opentelemetry/api'; import { + ParentBasedSampler, Sampler, SamplingDecision, SamplingResult, } from '@opentelemetry/sdk-trace-base'; -import { diag, DiagLogger } from '@opentelemetry/api'; +import { AWSXRaySamplingClient } from './aws-xray-sampling-client'; import { - SamplingRule, - AWSXRaySamplerConfig, + AWSXRayRemoteSamplerConfig, + GetSamplingRulesResponse, SamplingRuleRecord, } from './types'; -import axios from 'axios'; +import { SamplingRuleApplier } from './sampling-rule-applier'; -// 5 minute interval on sampling rules fetch (default polling interval) -const DEFAULT_INTERVAL_MS = 5 * 60 * 1000; +// 5 minute default sampling rules polling interval +const DEFAULT_RULES_POLLING_INTERVAL_SECONDS: number = 5 * 60; // Default endpoint for awsproxy : https://aws-otel.github.io/docs/getting-started/remote-sampling#enable-awsproxy-extension const DEFAULT_AWS_PROXY_ENDPOINT = 'http://localhost:2000'; -const SAMPLING_RULES_PATH = '/GetSamplingRules'; -// IN PROGRESS - SKELETON CLASS +// Wrapper class to ensure that all XRay Sampler Functionality in _AWSXRayRemoteSampler +// uses ParentBased logic to respect the parent span's sampling decision export class AWSXRayRemoteSampler implements Sampler { - private _pollingInterval: number; - private _awsProxyEndpoint: string; - private _samplerDiag: DiagLogger; - - constructor(samplerConfig: AWSXRaySamplerConfig) { - this._pollingInterval = - samplerConfig.pollingIntervalMs ?? DEFAULT_INTERVAL_MS; - this._awsProxyEndpoint = samplerConfig.endpoint + private _root: ParentBasedSampler; + private internalXraySampler: _AWSXRayRemoteSampler; + constructor(samplerConfig: AWSXRayRemoteSamplerConfig) { + this.internalXraySampler = new _AWSXRayRemoteSampler(samplerConfig); + this._root = new ParentBasedSampler({ + root: this.internalXraySampler, + }); + } + public shouldSample( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ): SamplingResult { + return this._root.shouldSample( + context, + traceId, + spanName, + spanKind, + attributes, + links + ); + } + + public toString(): string { + return `AWSXRayRemoteSampler{root=${this._root.toString()}`; + } + + public stopPollers() { + this.internalXraySampler.stopPollers(); + } +} + +// IN PROGRESS - SKELETON CLASS +// +// _AWSXRayRemoteSampler contains all core XRay Sampler Functionality, +// however it is NOT Parent-based (e.g. Sample logic runs for each span) +// Not intended for external use, use Parent-based `AWSXRayRemoteSampler` instead. +export class _AWSXRayRemoteSampler implements Sampler { + private rulePollingIntervalMillis: number; + private awsProxyEndpoint: string; + private samplerDiag: DiagLogger; + private rulePoller: NodeJS.Timeout | undefined; + private rulePollingJitterMillis: number; + private samplingClient: AWSXRaySamplingClient; + + constructor(samplerConfig: AWSXRayRemoteSamplerConfig) { + this.samplerDiag = diag; + + if ( + samplerConfig.pollingInterval == null || + samplerConfig.pollingInterval < 10 + ) { + this.samplerDiag.warn( + `'pollingInterval' is undefined or too small. Defaulting to ${DEFAULT_RULES_POLLING_INTERVAL_SECONDS} seconds` + ); + this.rulePollingIntervalMillis = + DEFAULT_RULES_POLLING_INTERVAL_SECONDS * 1000; + } else { + this.rulePollingIntervalMillis = samplerConfig.pollingInterval * 1000; + } + + this.rulePollingJitterMillis = Math.random() * 5 * 1000; + + this.awsProxyEndpoint = samplerConfig.endpoint ? samplerConfig.endpoint : DEFAULT_AWS_PROXY_ENDPOINT; - if (this._pollingInterval <= 0) { - throw new TypeError('pollingInterval must be a positive integer'); - } + this.samplingClient = new AWSXRaySamplingClient( + this.awsProxyEndpoint, + this.samplerDiag + ); - this._samplerDiag = diag.createComponentLogger({ - namespace: '@opentelemetry/sampler-aws-xray', - }); + // Start the Sampling Rules poller + this.startSamplingRulesPoller(); - // execute first get Sampling rules update using polling interval - this.startRulePoller(); + // TODO: Start the Sampling Targets poller } - shouldSample(): SamplingResult { + public shouldSample( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ): SamplingResult { // Implementation to be added return { decision: SamplingDecision.NOT_RECORD }; } - toString(): string { - return `AWSXRayRemoteSampler{endpoint=${this._awsProxyEndpoint}, pollingInterval=${this._pollingInterval}}`; + public toString(): string { + return `_AWSXRayRemoteSampler{awsProxyEndpoint=${ + this.awsProxyEndpoint + }, rulePollingIntervalMillis=${this.rulePollingIntervalMillis.toString()}}`; } - private getAndUpdateSamplingRules = async (): Promise => { - let samplingRules: SamplingRule[] = []; // reset rules array - - const requestConfig = { - headers: { - 'Content-Type': 'application/json', - }, - }; - - try { - const samplingRulesEndpoint = - this._awsProxyEndpoint + SAMPLING_RULES_PATH; - const response = await axios.post( - samplingRulesEndpoint, - {}, - requestConfig - ); - const responseJson = response.data; - - samplingRules = - responseJson?.SamplingRuleRecords.map( - (record: SamplingRuleRecord) => record.SamplingRule - ).filter(Boolean) ?? []; - - // TODO: pass samplingRules to rule cache, temporarily logging the samplingRules array - this._samplerDiag.debug('sampling rules: ', samplingRules); - } catch (error) { - // Log error - this._samplerDiag.warn('Error fetching sampling rules: ', error); - } - }; + public stopPollers() { + clearInterval(this.rulePoller); + } - // fetch sampling rules every polling interval - private startRulePoller(): void { - // execute first update - // this.getAndUpdateSamplingRules() never rejects. Using void operator to suppress @typescript-eslint/no-floating-promises. - void this.getAndUpdateSamplingRules(); + private startSamplingRulesPoller(): void { + // Execute first update + this.getAndUpdateSamplingRules(); // Update sampling rules every 5 minutes (or user-defined polling interval) - const rulePoller = setInterval( + this.rulePoller = setInterval( () => this.getAndUpdateSamplingRules(), - this._pollingInterval + this.rulePollingIntervalMillis + this.rulePollingJitterMillis ); - rulePoller.unref(); + this.rulePoller.unref(); + } + + private getAndUpdateSamplingRules(): void { + this.samplingClient.fetchSamplingRules(this.updateSamplingRules.bind(this)); + } + + private updateSamplingRules(responseObject: GetSamplingRulesResponse): void { + let samplingRules: SamplingRuleApplier[] = []; + + samplingRules = []; + if (responseObject.SamplingRuleRecords) { + responseObject.SamplingRuleRecords.forEach( + (record: SamplingRuleRecord) => { + if (record.SamplingRule) { + samplingRules.push( + new SamplingRuleApplier(record.SamplingRule, undefined) + ); + } + } + ); + + // TODO: pass samplingRules to rule cache, temporarily logging the samplingRules array + this.samplerDiag.debug('sampling rules: ', samplingRules); + } else { + this.samplerDiag.error( + 'SamplingRuleRecords from GetSamplingRules request is not defined' + ); + } } } diff --git a/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts new file mode 100644 index 0000000000..92424317ea --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule-applier.ts @@ -0,0 +1,35 @@ +/* + * 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 { ISamplingRule, SamplingTargetDocument } from './types'; +import { SamplingRule } from './sampling-rule'; +import { Statistics } from './statistics'; + +export class SamplingRuleApplier { + public samplingRule: SamplingRule; + + constructor( + samplingRule: ISamplingRule, + statistics: Statistics = new Statistics(), + target?: SamplingTargetDocument + ) { + this.samplingRule = new SamplingRule(samplingRule); + } +} diff --git a/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule.ts b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule.ts new file mode 100644 index 0000000000..fd40e86618 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/src/sampling-rule.ts @@ -0,0 +1,91 @@ +/* + * 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 { ISamplingRule } from './types'; + +export class SamplingRule implements ISamplingRule { + public RuleName: string; + public RuleARN: string | undefined; + public Priority: number; + public ReservoirSize: number; + public FixedRate: number; + public ServiceName: string; + public ServiceType: string; + public Host: string; + public HTTPMethod: string; + public URLPath: string; + public ResourceARN: string; + public Attributes: { [key: string]: string } | undefined; + public Version: number; + + constructor(samplingRule: ISamplingRule) { + // The AWS API docs mark `RuleName` as an optional field but in practice it seems to always be + // present, and sampling targets could not be computed without it. For now provide an arbitrary fallback just in + // case the AWS API docs are correct. + this.RuleName = samplingRule.RuleName ? samplingRule.RuleName : 'Default'; + this.RuleARN = samplingRule.RuleARN; + this.Priority = samplingRule.Priority; + this.ReservoirSize = samplingRule.ReservoirSize; + this.FixedRate = samplingRule.FixedRate; + this.ServiceName = samplingRule.ServiceName; + this.ServiceType = samplingRule.ServiceType; + this.Host = samplingRule.Host; + this.HTTPMethod = samplingRule.HTTPMethod; + this.URLPath = samplingRule.URLPath; + this.ResourceARN = samplingRule.ResourceARN; + this.Version = samplingRule.Version; + this.Attributes = samplingRule.Attributes; + } + + public equals(other: ISamplingRule): boolean { + let attributesEquals: boolean; + + if (this.Attributes === undefined || other.Attributes === undefined) { + attributesEquals = this.Attributes === other.Attributes; + } else { + attributesEquals = this.Attributes.length === other.Attributes.length; + for (const attributeKey in other.Attributes) { + if ( + !(attributeKey in this.Attributes) || + this.Attributes[attributeKey] !== other.Attributes[attributeKey] + ) { + attributesEquals = false; + break; + } + } + } + + return ( + this.FixedRate === other.FixedRate && + this.HTTPMethod === other.HTTPMethod && + this.Host === other.Host && + this.Priority === other.Priority && + this.ReservoirSize === other.ReservoirSize && + this.ResourceARN === other.ResourceARN && + this.RuleARN === other.RuleARN && + this.RuleName === other.RuleName && + this.ServiceName === other.ServiceName && + this.ServiceType === other.ServiceType && + this.URLPath === other.URLPath && + this.Version === other.Version && + attributesEquals + ); + } +} diff --git a/incubator/opentelemetry-sampler-aws-xray/src/semconv.ts b/incubator/opentelemetry-sampler-aws-xray/src/semconv.ts new file mode 100644 index 0000000000..3e2fe891c9 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/src/semconv.ts @@ -0,0 +1,168 @@ +/* + * 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. + */ + +/* + * This file contains a copy of unstable semantic convention definitions + * used by this package. + * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv + */ + +/** + * The cloud platform in use. + * + * @note The prefix of the service **SHOULD** match the one specified in `cloud.provider`. + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_CLOUD_PLATFORM = 'cloud.platform' as const; + +/** + * The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). + * + * @example arn:aws:lambda:us-east-1:123456:function:myfunction:myalias + * + * @note This may be different from `cloud.resource_id` if an alias is involved. + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_AWS_LAMBDA_INVOKED_ARN = 'aws.lambda.invoked_arn' as const; + +/** + * The ARN of an [ECS cluster](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html). + * + * @example arn:aws:ecs:us-west-2:123456789123:cluster/my-cluster + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_AWS_ECS_CLUSTER_ARN = 'aws.ecs.cluster.arn' as const; + +/** + * The Amazon Resource Name (ARN) of an [ECS container instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html). + * + * @example arn:aws:ecs:us-west-1:123456789123:container/32624152-9086-4f0e-acae-1a75b14fe4d9 + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_AWS_ECS_CONTAINER_ARN = 'aws.ecs.container.arn' as const; + +/** + * The ARN of an EKS cluster. + * + * @example arn:aws:ecs:us-west-2:123456789123:cluster/my-cluster + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_AWS_EKS_CLUSTER_ARN = 'aws.eks.cluster.arn' as const; + +/** + * Deprecated, use one of `server.address`, `client.address` or `http.request.header.host` instead, depending on the usage. + * + * @example www.example.org + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replaced by one of `server.address`, `client.address` or `http.request.header.host`, depending on the usage. + */ +export const ATTR_HTTP_HOST = 'http.host' as const; + +/** + * Deprecated, use `http.request.method` instead. + * + * @example GET + * @example POST + * @example HEAD + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replaced by `http.request.method`. + */ +export const ATTR_HTTP_METHOD = 'http.method' as const; + +/** + * Deprecated, use `url.path` and `url.query` instead. + * + * @example /search?q=OpenTelemetry#SemConv + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Split to `url.path` and `url.query. + */ +export const ATTR_HTTP_TARGET = 'http.target' as const; + +/** + * Deprecated, use `url.full` instead. + * + * @example https://www.foo.bar/search?q=OpenTelemetry#SemConv + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replaced by `url.full`. + */ +export const ATTR_HTTP_URL = 'http.url' as const; + +/** + * Enum value "aws_ec2" for attribute {@link ATTR_CLOUD_PLATFORM}. + */ +export const CLOUD_PLATFORM_VALUE_AWS_EC2 = 'aws_ec2' as const; + +/** + * Enum value "aws_ecs" for attribute {@link ATTR_CLOUD_PLATFORM}. + */ +export const CLOUD_PLATFORM_VALUE_AWS_ECS = 'aws_ecs' as const; + +/** + * Enum value "aws_eks" for attribute {@link ATTR_CLOUD_PLATFORM}. + */ +export const CLOUD_PLATFORM_VALUE_AWS_EKS = 'aws_eks' as const; + +/** + * Enum value "aws_elastic_beanstalk" for attribute {@link ATTR_CLOUD_PLATFORM}. + */ +export const CLOUD_PLATFORM_VALUE_AWS_ELASTIC_BEANSTALK = + 'aws_elastic_beanstalk' as const; + +/** + * Enum value "aws_lambda" for attribute {@link ATTR_CLOUD_PLATFORM}. + */ +export const CLOUD_PLATFORM_VALUE_AWS_LAMBDA = 'aws_lambda' as const; + +/** + * Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://cloud.google.com/apis/design/resource_names#full_resource_name) on GCP) + * + * @example arn:aws:lambda:REGION:ACCOUNT_ID:function:my-function + * @example //run.googleapis.com/projects/PROJECT_ID/locations/LOCATION_ID/services/SERVICE_ID + * @example /subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/ + * + * @note On some cloud providers, it may not be possible to determine the full ID at startup, + * so it may be necessary to set `cloud.resource_id` as a span attribute instead. + * + * The exact value to use for `cloud.resource_id` depends on the cloud provider. + * The following well-known definitions **MUST** be used if you set this attribute and they apply: + * + * - **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). + * Take care not to use the "invoked ARN" directly but replace any + * [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) + * with the resolved function version, as the same runtime instance may be invocable with + * multiple different aliases. + * - **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) + * - **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/rest/api/resources/resources/get-by-id) of the invoked function, + * *not* the function app, having the form + * `/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/`. + * This means that a span attribute **MUST** be used, as an Azure function app can host multiple functions that would usually share + * a TracerProvider. + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_CLOUD_RESOURCE_ID = 'cloud.resource_id' as const; diff --git a/incubator/opentelemetry-sampler-aws-xray/src/statistics.ts b/incubator/opentelemetry-sampler-aws-xray/src/statistics.ts new file mode 100644 index 0000000000..056f01832a --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/src/statistics.ts @@ -0,0 +1,47 @@ +/* + * 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 { ISamplingStatistics } from './types'; + +export class Statistics implements ISamplingStatistics { + public RequestCount: number; + public SampleCount: number; + public BorrowCount: number; + + constructor(requestCount = 0, sampleCount = 0, borrowCount = 0) { + this.RequestCount = requestCount; + this.SampleCount = sampleCount; + this.BorrowCount = borrowCount; + } + + public getStatistics(): ISamplingStatistics { + return { + RequestCount: this.RequestCount, + SampleCount: this.SampleCount, + BorrowCount: this.BorrowCount, + }; + } + + public resetStatistics(): void { + this.RequestCount = 0; + this.SampleCount = 0; + this.BorrowCount = 0; + } +} diff --git a/incubator/opentelemetry-sampler-aws-xray/src/types.ts b/incubator/opentelemetry-sampler-aws-xray/src/types.ts index de595ad77b..3e55819c2c 100644 --- a/incubator/opentelemetry-sampler-aws-xray/src/types.ts +++ b/incubator/opentelemetry-sampler-aws-xray/src/types.ts @@ -14,10 +14,30 @@ * limitations under the License. */ -// X-Ray Sampling rule reference: https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingRules.html -export interface SamplingRule { - // a unique name for the rule - RuleName: string; +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Resource } from '@opentelemetry/resources'; + +export interface AWSXRayRemoteSamplerConfig { + // resource to control sampling at the service level + resource: Resource; + + // endpoint of awsproxy - for more information see https://aws-otel.github.io/docs/getting-started/remote-sampling + endpoint?: string; + + // interval for polling sampling rules + pollingInterval?: number; +} + +// https://docs.aws.amazon.com/xray/latest/api/API_SamplingRule.html +export interface ISamplingRule { + // A unique name for the rule + RuleName?: string; + + // The ARN of the sampling rule + RuleARN?: string; // (integer between 1 and 9999) - the priority of the sampling rule. Services evaluate rules in ascending order of // priority, and make a sampling decision with the first rule that matches. @@ -47,23 +67,38 @@ export interface SamplingRule { // The ARN of the AWS resource running the service. ResourceARN: string; - // (Optional) segment attributes that are known when the sampling decision is made. + // Segment attributes to be matched when the sampling decision is being made. Attributes?: { [key: string]: string }; + Version: number; } +// https://docs.aws.amazon.com/xray/latest/api/API_SamplingRuleRecord.html export interface SamplingRuleRecord { - CreatedAt: number; - ModifiedAt: number; - SamplingRule?: SamplingRule; + CreatedAt?: number; + ModifiedAt?: number; + SamplingRule?: ISamplingRule; } +// https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingRules.html#API_GetSamplingRules_ResponseSyntax export interface GetSamplingRulesResponse { NextToken?: string; SamplingRuleRecords?: SamplingRuleRecord[]; } -// samplingStatisticsDocument is used to store current state of sampling data. +export interface ISamplingStatistics { + // RequestCount is the number of requests matched against a specific rule. + RequestCount: number; + + // SampleCount is the number of requests sampled using a specific rule. + SampleCount: number; + + // BorrowCount is the number of requests borrowed using a specific rule. + BorrowCount: number; +} + +// https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html#API_GetSamplingTargets_RequestSyntax +// SamplingStatisticsDocument is used to store current state of sampling statistics. export interface SamplingStatisticsDocument { // A unique identifier for the service in hexadecimal. ClientID: string; @@ -71,7 +106,7 @@ export interface SamplingStatisticsDocument { RuleName: string; // The number of requests that matched the rule. RequestCount: number; - // The number of requests borrowed. + // The number of requests borrowed using the rule. BorrowCount: number; // The number of requests sampled using the rule. SampledCount: number; @@ -79,12 +114,39 @@ export interface SamplingStatisticsDocument { Timestamp: number; } -export interface AWSXRaySamplerConfig { - // endpoint of awsproxy - for more information see https://aws-otel.github.io/docs/getting-started/remote-sampling - // defaults to localhost:2000 if not specified - endpoint?: string; +// https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html#API_GetSamplingTargets_RequestBody +export interface SamplingTargetDocument { + // The percentage of matching requests to instrument, after the reservoir is exhausted. + FixedRate: number; + // The number of seconds to wait before fetching sampling targets again + Interval?: number | null; + // The number of requests per second that X-Ray allocated this service. + ReservoirQuota?: number | null; + // Time when the reservoir quota will expire + ReservoirQuotaTTL?: number | null; + // The name of the sampling rule + RuleName: string; +} + +// https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html#API_GetSamplingTargets_RequestBody +export interface UnprocessedStatistic { + ErrorCode: string; + Message: string; + RuleName: string; +} + +// https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html#API_GetSamplingTargets_RequestBody +export interface GetSamplingTargetsBody { + SamplingStatisticsDocuments: SamplingStatisticsDocument[]; +} + +// https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html#API_GetSamplingTargets_ResponseSyntax +export interface GetSamplingTargetsResponse { + LastRuleModification: number; + SamplingTargetDocuments: SamplingTargetDocument[]; + UnprocessedStatistics: UnprocessedStatistic[]; +} - // interval of polling sampling rules (in ms) - // defaults to 5 minutes if not specified - pollingIntervalMs?: number; +export interface TargetMap { + [targetName: string]: SamplingTargetDocument; } 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 new file mode 100644 index 0000000000..628b000110 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/aws-xray-sampling-client.test.ts @@ -0,0 +1,213 @@ +/* + * 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 nock from 'nock'; +import { DiagConsoleLogger } from '@opentelemetry/api'; +import { AWSXRaySamplingClient } from '../src/aws-xray-sampling-client'; +import { + GetSamplingRulesResponse, + GetSamplingTargetsBody, + GetSamplingTargetsResponse, +} from '../src/types'; + +const DATA_DIR = __dirname + '/data'; +const TEST_URL = 'http://127.0.0.1:2000'; + +describe('AWSXRaySamplingClient', () => { + it('testGetNoSamplingRules', done => { + nock(TEST_URL) + .post('/GetSamplingRules') + .reply(200, { SamplingRuleRecords: [] }); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + + client.fetchSamplingRules((response: GetSamplingRulesResponse) => { + expect(response.SamplingRuleRecords?.length).toEqual(0); + done(); + }); + }); + + it('testGetInvalidResponse', done => { + nock(TEST_URL).post('/GetSamplingRules').reply(200, {}); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + + client.fetchSamplingRules((response: GetSamplingRulesResponse) => { + expect(response.SamplingRuleRecords?.length).toEqual(undefined); + done(); + }); + }); + + it('testGetSamplingRuleMissingInRecords', done => { + nock(TEST_URL) + .post('/GetSamplingRules') + .reply(200, { SamplingRuleRecords: [{}] }); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + client.fetchSamplingRules((response: GetSamplingRulesResponse) => { + expect(response.SamplingRuleRecords?.length).toEqual(1); + done(); + }); + }); + + it('testDefaultValuesUsedWhenMissingPropertiesInSamplingRule', done => { + nock(TEST_URL) + .post('/GetSamplingRules') + .reply(200, { SamplingRuleRecords: [{ SamplingRule: {} }] }); + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + client.fetchSamplingRules((response: GetSamplingRulesResponse) => { + expect(response.SamplingRuleRecords?.length).toEqual(1); + expect( + response.SamplingRuleRecords?.[0].SamplingRule + ).not.toBeUndefined(); + expect( + response.SamplingRuleRecords?.[0].SamplingRule?.Attributes + ).toEqual(undefined); + expect(response.SamplingRuleRecords?.[0].SamplingRule?.FixedRate).toEqual( + undefined + ); + expect( + response.SamplingRuleRecords?.[0].SamplingRule?.HTTPMethod + ).toEqual(undefined); + expect(response.SamplingRuleRecords?.[0].SamplingRule?.Host).toEqual( + undefined + ); + expect(response.SamplingRuleRecords?.[0].SamplingRule?.Priority).toEqual( + undefined + ); + expect( + response.SamplingRuleRecords?.[0].SamplingRule?.ReservoirSize + ).toEqual(undefined); + expect( + response.SamplingRuleRecords?.[0].SamplingRule?.ResourceARN + ).toEqual(undefined); + expect(response.SamplingRuleRecords?.[0].SamplingRule?.RuleARN).toEqual( + undefined + ); + expect(response.SamplingRuleRecords?.[0].SamplingRule?.RuleName).toEqual( + undefined + ); + expect( + response.SamplingRuleRecords?.[0].SamplingRule?.ServiceName + ).toEqual(undefined); + expect( + response.SamplingRuleRecords?.[0].SamplingRule?.ServiceType + ).toEqual(undefined); + expect(response.SamplingRuleRecords?.[0].SamplingRule?.URLPath).toEqual( + undefined + ); + expect(response.SamplingRuleRecords?.[0].SamplingRule?.Version).toEqual( + undefined + ); + done(); + }); + }); + + it('testGetCorrectNumberOfSamplingRules', done => { + const data = require(DATA_DIR + '/get-sampling-rules-response-sample.json'); + const records = data['SamplingRuleRecords']; + nock(TEST_URL).post('/GetSamplingRules').reply(200, data); + + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + + client.fetchSamplingRules((response: GetSamplingRulesResponse) => { + expect(response.SamplingRuleRecords?.length).toEqual(records.length); + for (let i = 0; i < records.length; i++) { + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.Attributes + ).toEqual(records[i].SamplingRule.Attributes); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.FixedRate + ).toEqual(records[i].SamplingRule.FixedRate); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.HTTPMethod + ).toEqual(records[i].SamplingRule.HTTPMethod); + expect(response.SamplingRuleRecords?.[i].SamplingRule?.Host).toEqual( + records[i].SamplingRule.Host + ); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.Priority + ).toEqual(records[i].SamplingRule.Priority); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.ReservoirSize + ).toEqual(records[i].SamplingRule.ReservoirSize); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.ResourceARN + ).toEqual(records[i].SamplingRule.ResourceARN); + expect(response.SamplingRuleRecords?.[i].SamplingRule?.RuleARN).toEqual( + records[i].SamplingRule.RuleARN + ); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.RuleName + ).toEqual(records[i].SamplingRule.RuleName); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.ServiceName + ).toEqual(records[i].SamplingRule.ServiceName); + expect( + response.SamplingRuleRecords?.[i].SamplingRule?.ServiceType + ).toEqual(records[i].SamplingRule.ServiceType); + expect(response.SamplingRuleRecords?.[i].SamplingRule?.URLPath).toEqual( + records[i].SamplingRule.URLPath + ); + expect(response.SamplingRuleRecords?.[i].SamplingRule?.Version).toEqual( + records[i].SamplingRule.Version + ); + } + done(); + }); + }); + + it('testGetSamplingTargets', done => { + const data = require(DATA_DIR + + '/get-sampling-targets-response-sample.json'); + nock(TEST_URL).post('/SamplingTargets').reply(200, data); + + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + + client.fetchSamplingTargets( + data, + (response: GetSamplingTargetsResponse) => { + expect(response.SamplingTargetDocuments.length).toEqual(2); + expect(response.UnprocessedStatistics.length).toEqual(0); + expect(response.LastRuleModification).toEqual(1707551387); + done(); + } + ); + }); + + it('testGetInvalidSamplingTargets', done => { + const data = { + LastRuleModification: null, + SamplingTargetDocuments: null, + UnprocessedStatistics: null, + }; + nock(TEST_URL).post('/SamplingTargets').reply(200, data); + + const client = new AWSXRaySamplingClient(TEST_URL, new DiagConsoleLogger()); + + client.fetchSamplingTargets( + data as unknown as GetSamplingTargetsBody, + (response: GetSamplingTargetsResponse) => { + expect(response.SamplingTargetDocuments).toBe(null); + expect(response.UnprocessedStatistics).toBe(null); + expect(response.LastRuleModification).toBe(null); + done(); + } + ); + }); +}); diff --git a/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample-2.json b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample-2.json new file mode 100644 index 0000000000..6bf24ebac9 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample-2.json @@ -0,0 +1,48 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 1.676038494E9, + "ModifiedAt": 1.676038494E9, + "SamplingRule": { + "Attributes": { + "foo": "bar", + "abc": "1234" + }, + "FixedRate": 0.05, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 100, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1.67799933E9, + "ModifiedAt": 1.67799933E9, + "SamplingRule": { + "Attributes": { + "abc": "1234" + }, + "FixedRate": 0.11, + "HTTPMethod": "*", + "Host": "*", + "Priority": 20, + "ReservoirSize": 1, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/test", + "RuleName": "test", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample-sample-all.json b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample-sample-all.json new file mode 100644 index 0000000000..7a4b9ea0da --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample-sample-all.json @@ -0,0 +1,24 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0.0, + "ModifiedAt": 1.611564245E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 1.00, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 1, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample.json b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample.json new file mode 100644 index 0000000000..a0d3c5ba21 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-rules-response-sample.json @@ -0,0 +1,65 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 1.67799933E9, + "ModifiedAt": 1.67799933E9, + "SamplingRule": { + "Attributes": { + "foo": "bar", + "doo": "baz" + }, + "FixedRate": 0.05, + "HTTPMethod": "*", + "Host": "*", + "Priority": 1000, + "ReservoirSize": 10, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule1", + "RuleName": "Rule1", + "ServiceName": "*", + "ServiceType": "AWS::Foo::Bar", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 0.0, + "ModifiedAt": 1.611564245E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.05, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 1, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1.676038494E9, + "ModifiedAt": 1.676038494E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.2, + "HTTPMethod": "GET", + "Host": "*", + "Priority": 1, + "ReservoirSize": 10, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule2", + "RuleName": "Rule2", + "ServiceName": "FooBar", + "ServiceType": "*", + "URLPath": "/foo/bar", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-targets-response-sample.json b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-targets-response-sample.json new file mode 100644 index 0000000000..498fe1505b --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/data/get-sampling-targets-response-sample.json @@ -0,0 +1,20 @@ +{ + "LastRuleModification": 1707551387.0, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.10, + "Interval": 10, + "ReservoirQuota": 30, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "test" + }, + { + "FixedRate": 0.05, + "Interval": 10, + "ReservoirQuota": 0, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "Default" + } + ], + "UnprocessedStatistics": [] +} \ No newline at end of file diff --git a/incubator/opentelemetry-sampler-aws-xray/test/data/test-remote-sampler_sampling-rules-response-sample.json b/incubator/opentelemetry-sampler-aws-xray/test/data/test-remote-sampler_sampling-rules-response-sample.json new file mode 100644 index 0000000000..a5c0d2cb5b --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/data/test-remote-sampler_sampling-rules-response-sample.json @@ -0,0 +1,45 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 1.676038494E9, + "ModifiedAt": 1.676038494E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 1.0, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 0, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1.67799933E9, + "ModifiedAt": 1.67799933E9, + "SamplingRule": { + "Attributes": { + "abc": "1234" + }, + "FixedRate": 0, + "HTTPMethod": "*", + "Host": "*", + "Priority": 20, + "ReservoirSize": 0, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/test", + "RuleName": "test", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/incubator/opentelemetry-sampler-aws-xray/test/data/test-remote-sampler_sampling-targets-response-sample.json b/incubator/opentelemetry-sampler-aws-xray/test/data/test-remote-sampler_sampling-targets-response-sample.json new file mode 100644 index 0000000000..bc9b4718fb --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/data/test-remote-sampler_sampling-targets-response-sample.json @@ -0,0 +1,20 @@ +{ + "LastRuleModification": 1707551387.0, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.0, + "Interval": 100000, + "ReservoirQuota": 1000, + "ReservoirQuotaTTL": 9999999999.0, + "RuleName": "test" + }, + { + "FixedRate": 0.0, + "Interval": 1000, + "ReservoirQuota": 100, + "ReservoirQuotaTTL": 9999999999.0, + "RuleName": "Default" + } + ], + "UnprocessedStatistics": [] +} \ No newline at end of file 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 b7d31c7fd4..bd45697e94 100644 --- a/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts +++ b/incubator/opentelemetry-sampler-aws-xray/test/remote-sampler.test.ts @@ -14,160 +14,123 @@ * limitations under the License. */ +// Includes work from: +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { resourceFromAttributes, emptyResource } from '@opentelemetry/resources'; +import { + SEMRESATTRS_CLOUD_PLATFORM, + ATTR_SERVICE_NAME, +} from '@opentelemetry/semantic-conventions'; +import { expect } from 'expect'; +import { + _AWSXRayRemoteSampler, + AWSXRayRemoteSampler, +} from '../src/remote-sampler'; import * as sinon from 'sinon'; -import axios from 'axios'; -import * as nock from 'nock'; -import * as assert from 'assert'; - -import { AWSXRayRemoteSampler } from '../src'; - -describe('GetSamplingRules', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getSamplingRulesResponseStub: any = { - NextToken: null, - SamplingRuleRecords: [ - { - CreatedAt: 1.67799933e9, - ModifiedAt: 1.67799933e9, - SamplingRule: { - Attributes: { - foo: 'bar', - doo: 'baz', - }, - FixedRate: 0.05, - HTTPMethod: '*', - Host: '*', - Priority: 1000, - ReservoirSize: 10, - ResourceARN: '*', - RuleARN: 'arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule1', - RuleName: 'Rule1', - ServiceName: '*', - ServiceType: 'AWS::Foo::Bar', - URLPath: '*', - Version: 1, - }, - }, - { - CreatedAt: 0.0, - ModifiedAt: 1.611564245e9, - SamplingRule: { - Attributes: {}, - FixedRate: 0.05, - HTTPMethod: '*', - Host: '*', - Priority: 10000, - ReservoirSize: 1, - ResourceARN: '*', - RuleARN: 'arn:aws:xray:us-west-2:123456789000:sampling-rule/Default', - RuleName: 'Default', - ServiceName: '*', - ServiceType: '*', - URLPath: '*', - Version: 1, - }, - }, - { - CreatedAt: 1.676038494e9, - ModifiedAt: 1.676038494e9, - SamplingRule: { - Attributes: {}, - FixedRate: 0.2, - HTTPMethod: 'GET', - Host: '*', - Priority: 1, - ReservoirSize: 10, - ResourceARN: '*', - RuleARN: 'arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule2', - RuleName: 'Rule2', - ServiceName: 'FooBar', - ServiceType: '*', - URLPath: '/foo/bar', - Version: 1, - }, - }, - ], - }; +import * as http from 'http'; - let clock: sinon.SinonFakeTimers; - let sampler: AWSXRayRemoteSampler; - let axiosPostSpy: sinon.SinonSpy; - - const defaultEndpoint = 'http://localhost:1234'; - const pollingInterval = 60 * 1000; - const config = { - endpoint: defaultEndpoint, - pollingIntervalMs: pollingInterval, - }; - - before(() => { - nock('http://localhost:2000') - .persist() - .post('/GetSamplingRules') - .reply(200, getSamplingRulesResponseStub); - }); - - beforeEach(() => { - clock = sinon.useFakeTimers(); - axiosPostSpy = sinon.spy(axios, 'post'); - sampler = new AWSXRayRemoteSampler(config); - }); +describe('AWSXRayRemoteSampler', () => { + let sampler: AWSXRayRemoteSampler | undefined; afterEach(() => { - clock.restore(); - axiosPostSpy.restore(); + if (sampler != null) { + sampler.stopPollers(); + } }); - it('should throw TypeError when an invalid polling interval is passed in', async () => { - const configWithZeroPollingInterval = { - pollingIntervalMs: 0, - }; - const configWithNegativeInterval = { - pollingIntervalMs: -5, - }; - - assert.throws( - () => new AWSXRayRemoteSampler(configWithZeroPollingInterval), - TypeError + it('testCreateRemoteSamplerWithEmptyResource', () => { + sampler = new AWSXRayRemoteSampler({ + resource: emptyResource(), + }); + + expect((sampler as any)._root._root.rulePoller).not.toBeFalsy(); + expect((sampler as any)._root._root.rulePollingIntervalMillis).toEqual( + 300 * 1000 ); - assert.throws( - () => new AWSXRayRemoteSampler(configWithNegativeInterval), - TypeError + expect((sampler as any)._root._root.samplingClient).not.toBeFalsy(); + }); + + it('testCreateRemoteSamplerWithPopulatedResource', () => { + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'test-service-name', + [SEMRESATTRS_CLOUD_PLATFORM]: 'test-cloud-platform', + }); + sampler = new AWSXRayRemoteSampler({ resource: resource }); + + expect((sampler as any)._root._root.rulePoller).not.toBeFalsy(); + expect((sampler as any)._root._root.rulePollingIntervalMillis).toEqual( + 300 * 1000 ); + expect((sampler as any)._root._root.samplingClient).not.toBeFalsy(); }); - it('should make a POST request to the /GetSamplingRules endpoint upon initialization', async () => { - sinon.assert.calledOnce(axiosPostSpy); + it('testCreateRemoteSamplerWithAllFieldsPopulated', () => { + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'test-service-name', + [SEMRESATTRS_CLOUD_PLATFORM]: 'test-cloud-platform', + }); + sampler = new AWSXRayRemoteSampler({ + resource: resource, + endpoint: 'http://abc.com', + pollingInterval: 120, // seconds + }); + + expect((sampler as any)._root._root.rulePoller).not.toBeFalsy(); + expect((sampler as any)._root._root.rulePollingIntervalMillis).toEqual( + 120 * 1000 + ); + expect((sampler as any)._root._root.samplingClient).not.toBeFalsy(); + expect((sampler as any)._root._root.awsProxyEndpoint).toEqual( + 'http://abc.com' + ); }); - it('should make a POST request to the /GetSamplingRules endpoint', async () => { - clock.tick(pollingInterval); - sinon.assert.calledTwice(axiosPostSpy); + it('toString()', () => { + expect( + new AWSXRayRemoteSampler({ resource: emptyResource() }).toString() + ).toEqual( + 'AWSXRayRemoteSampler{root=ParentBased{root=_AWSXRayRemoteSampler{awsProxyEndpoint=http://localhost:2000, rulePollingIntervalMillis=300000}, remoteParentSampled=AlwaysOnSampler, remoteParentNotSampled=AlwaysOffSampler, localParentSampled=AlwaysOnSampler, localParentNotSampled=AlwaysOffSampler}' + ); }); +}); - it('should make 3 POST requests to the /GetSamplingRules endpoint after 3 intervals have passed', async () => { - clock.tick(pollingInterval); - clock.tick(pollingInterval); +describe('_AWSXRayRemoteSampler', () => { + const pollingInterval = 60; + let clock: sinon.SinonFakeTimers; + let xrayClientSpy: sinon.SinonSpy; + let sampler: _AWSXRayRemoteSampler | undefined; - sinon.assert.calledThrice(axiosPostSpy); + beforeEach(() => { + xrayClientSpy = sinon.spy(http, 'request'); + clock = sinon.useFakeTimers(); }); - it('should initialize endpoint and polling interval from config correctly', async () => { - assert.strictEqual( - sampler.toString(), - `AWSXRayRemoteSampler{endpoint=${defaultEndpoint}, pollingInterval=${pollingInterval}}` - ); + afterEach(() => { + if (sampler != null) { + sampler.stopPollers(); + } + xrayClientSpy.restore(); + clock.restore(); }); - it('should fall back to default polling interval and endpoint if not specified in config', async () => { - const sampler = new AWSXRayRemoteSampler({}); + it('should make a POST request to the /GetSamplingRules endpoint upon initialization', async () => { + sampler = new _AWSXRayRemoteSampler({ + resource: emptyResource(), + pollingInterval: pollingInterval, + }); + sinon.assert.calledOnce(xrayClientSpy); + }); - // default polling interval (5 minutes) = 5 * 60 * 1000 - assert.strictEqual( - sampler.toString(), - `AWSXRayRemoteSampler{endpoint=http://localhost:2000, pollingInterval=${ - 5 * 60 * 1000 - }}` - ); + it('should make 3 POST requests to the /GetSamplingRules endpoint after 3 intervals have passed', async () => { + sampler = new _AWSXRayRemoteSampler({ + resource: emptyResource(), + pollingInterval: pollingInterval, + }); + clock.tick(pollingInterval * 1000 + 5000); + clock.tick(pollingInterval * 1000 + 5000); + + sinon.assert.calledThrice(xrayClientSpy); }); }); diff --git a/incubator/opentelemetry-sampler-aws-xray/test/sampling-rule-applier.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/sampling-rule-applier.test.ts new file mode 100644 index 0000000000..9f08020195 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/sampling-rule-applier.test.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +describe('SamplingRuleApplier', () => {}); diff --git a/incubator/opentelemetry-sampler-aws-xray/test/sampling-rule.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/sampling-rule.test.ts new file mode 100644 index 0000000000..b39c17531e --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/sampling-rule.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { SamplingRule } from '../src/sampling-rule'; + +describe('SamplingRule', () => { + it('testSamplingRuleEquality', () => { + const rule = new SamplingRule({ + Attributes: { abc: '123', def: '4?6', ghi: '*89' }, + FixedRate: 0.11, + HTTPMethod: 'GET', + Host: 'localhost', + Priority: 20, + ReservoirSize: 1, + ResourceARN: '*', + RuleARN: 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + RuleName: 'test', + ServiceName: 'myServiceName', + ServiceType: 'AWS::EKS::Container', + URLPath: '/helloworld', + Version: 1, + }); + const rule_unordered_attributes = new SamplingRule({ + Attributes: { ghi: '*89', abc: '123', def: '4?6' }, + FixedRate: 0.11, + HTTPMethod: 'GET', + Host: 'localhost', + Priority: 20, + ReservoirSize: 1, + ResourceARN: '*', + RuleARN: 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + RuleName: 'test', + ServiceName: 'myServiceName', + ServiceType: 'AWS::EKS::Container', + URLPath: '/helloworld', + Version: 1, + }); + + expect(rule.equals(rule_unordered_attributes)); + + const rule_updated = new SamplingRule({ + Attributes: { ghi: '*89', abc: '123', def: '4?6' }, + FixedRate: 0.11, + HTTPMethod: 'GET', + Host: 'localhost', + Priority: 20, + ReservoirSize: 1, + ResourceARN: '*', + RuleARN: 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + RuleName: 'test', + ServiceName: 'myServiceName', + ServiceType: 'AWS::EKS::Container', + URLPath: '/helloworld_new', + Version: 1, + }); + const rule_updated_2 = new SamplingRule({ + Attributes: { abc: '128', def: '4?6', ghi: '*89' }, + FixedRate: 0.11, + HTTPMethod: 'GET', + Host: 'localhost', + Priority: 20, + ReservoirSize: 1, + ResourceARN: '*', + RuleARN: 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + RuleName: 'test', + ServiceName: 'myServiceName', + ServiceType: 'AWS::EKS::Container', + URLPath: '/helloworld', + Version: 1, + }); + + expect(rule.equals(rule_updated)).toEqual(false); + expect(rule.equals(rule_updated_2)).toEqual(false); + }); +}); diff --git a/incubator/opentelemetry-sampler-aws-xray/test/statistics.test.ts b/incubator/opentelemetry-sampler-aws-xray/test/statistics.test.ts new file mode 100644 index 0000000000..79ec859807 --- /dev/null +++ b/incubator/opentelemetry-sampler-aws-xray/test/statistics.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { Statistics } from '../src/statistics'; + +describe('Statistics', () => { + it('construct statistics and get statistics', () => { + const statistics = new Statistics(12, 3456, 7); + expect(statistics.RequestCount).toEqual(12); + expect(statistics.SampleCount).toEqual(3456); + expect(statistics.BorrowCount).toEqual(7); + const obtainedStatistics = statistics.getStatistics(); + expect(obtainedStatistics.RequestCount).toEqual(12); + expect(obtainedStatistics.SampleCount).toEqual(3456); + expect(obtainedStatistics.BorrowCount).toEqual(7); + }); +});