AWS X-Ray Remote Sampler Part 1 - Initial Classes and Rules Poller Implementation (#3366)

* remote sampling - initial classes and rules poller

* run generate-workflows and ruff

* add component owner for aws sampler, run lint

* move sampler into aws sdk-extensions

* move sampler tests to trace dir, update otel api/sdk deps, update changelog

* move mock_clock into tests dir

* update component owners for sdk-extension-aws

* ruff and lint

* address comments

* make sampler implementation internal until completion, update tests to not make http requests

* remove use of Optional, restore README of the package

* remove unused clock and client_id

* Update component_owners.yml

* Update CHANGELOG.md

---------

Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
This commit is contained in:
Jonathan Lee 2025-08-25 05:57:48 -07:00 committed by GitHub
parent 973d10d218
commit 032d6c67be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1632 additions and 6 deletions

View File

@ -33,12 +33,11 @@ components:
- dorkolog
propagator/opentelemetry-propagator-aws-xray:
- NathanielRN
- jj22ee
sdk-extension/opentelemetry-sdk-extension-aws:
- NathanielRN
- Kausik-A
- srprash
- jj22ee
instrumentation/opentelemetry-instrumentation-tortoiseorm:
- tonybaloney

View File

@ -26,7 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685))
- `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics
([#3666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3666))
- `opentelemetry-sdk-extension-aws` Add AWS X-Ray Remote Sampler with initial Rules Poller implementation
([#3366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3366))
## Version 1.36.0/0.57b0 (2025-07-29)

View File

@ -25,7 +25,11 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
dependencies = [
"opentelemetry-sdk ~= 1.12",
"opentelemetry-api ~= 1.23",
"opentelemetry-sdk ~= 1.23",
"opentelemetry-instrumentation ~= 0.44b0",
"opentelemetry-semantic-conventions ~= 0.44b0",
"requests ~= 2.28",
]
[project.entry-points.opentelemetry_id_generator]
@ -38,6 +42,10 @@ aws_eks = "opentelemetry.sdk.extension.aws.resource.eks:AwsEksResourceDetector"
aws_elastic_beanstalk = "opentelemetry.sdk.extension.aws.resource.beanstalk:AwsBeanstalkResourceDetector"
aws_lambda = "opentelemetry.sdk.extension.aws.resource._lambda:AwsLambdaResourceDetector"
# TODO: Uncomment this when Sampler implementation is complete
# [project.entry-points.opentelemetry_sampler]
# aws_xray_remote_sampler = "opentelemetry.sdk.extension.aws.trace.sampler:AwsXRayRemoteSampler"
[project.urls]
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/sdk-extension/opentelemetry-sdk-extension-aws"
Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"

View File

@ -0,0 +1,20 @@
# 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
#
# http://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.
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler import (
_AwsXRayRemoteSampler,
)
__all__ = ["_AwsXRayRemoteSampler"]

View File

@ -0,0 +1,143 @@
# 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
#
# http://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
from __future__ import annotations
import json
from logging import getLogger
from typing import List
import requests
# pylint: disable=no-name-in-module
from opentelemetry.instrumentation.utils import suppress_instrumentation
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import (
_SamplingRule,
)
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import (
_SamplingTargetResponse,
)
_logger = getLogger(__name__)
DEFAULT_SAMPLING_PROXY_ENDPOINT = "http://127.0.0.1:2000"
class _AwsXRaySamplingClient:
def __init__(
self,
endpoint: str = DEFAULT_SAMPLING_PROXY_ENDPOINT,
log_level: str | None = None,
):
# Override default log level
if log_level is not None:
_logger.setLevel(log_level)
self.__get_sampling_rules_endpoint = endpoint + "/GetSamplingRules"
self.__get_sampling_targets_endpoint = endpoint + "/SamplingTargets"
self.__session = requests.Session()
def get_sampling_rules(self) -> List[_SamplingRule]:
sampling_rules: List["_SamplingRule"] = []
headers = {"content-type": "application/json"}
with suppress_instrumentation():
try:
xray_response = self.__session.post(
url=self.__get_sampling_rules_endpoint,
headers=headers,
timeout=20,
)
sampling_rules_response = xray_response.json()
if (
sampling_rules_response is None
or "SamplingRuleRecords" not in sampling_rules_response
):
_logger.error(
"SamplingRuleRecords is missing in getSamplingRules response: %s",
sampling_rules_response,
)
return []
sampling_rules_records = sampling_rules_response[
"SamplingRuleRecords"
]
for record in sampling_rules_records:
if "SamplingRule" not in record:
_logger.error(
"SamplingRule is missing in SamplingRuleRecord"
)
else:
sampling_rules.append(
_SamplingRule(**record["SamplingRule"])
)
except requests.exceptions.RequestException as req_err:
_logger.error("Request error occurred: %s", req_err)
except json.JSONDecodeError as json_err:
_logger.error("Error in decoding JSON response: %s", json_err)
# pylint: disable=broad-exception-caught
except Exception as err:
_logger.error(
"Error occurred when attempting to fetch rules: %s", err
)
return sampling_rules
def get_sampling_targets(
self, statistics: List["dict[str, str | float | int]"]
) -> _SamplingTargetResponse:
sampling_targets_response = _SamplingTargetResponse(
LastRuleModification=None,
SamplingTargetDocuments=None,
UnprocessedStatistics=None,
)
headers = {"content-type": "application/json"}
with suppress_instrumentation():
try:
xray_response = self.__session.post(
url=self.__get_sampling_targets_endpoint,
headers=headers,
timeout=20,
json={"SamplingStatisticsDocuments": statistics},
)
xray_response_json = xray_response.json()
if (
xray_response_json is None
or "SamplingTargetDocuments" not in xray_response_json
or "LastRuleModification" not in xray_response_json
):
_logger.debug(
"getSamplingTargets response is invalid. Unable to update targets."
)
return sampling_targets_response
sampling_targets_response = _SamplingTargetResponse(
**xray_response_json
)
except requests.exceptions.RequestException as req_err:
_logger.debug("Request error occurred: %s", req_err)
except json.JSONDecodeError as json_err:
_logger.debug("Error in decoding JSON response: %s", json_err)
# pylint: disable=broad-exception-caught
except Exception as err:
_logger.debug(
"Error occurred when attempting to fetch targets: %s", err
)
return sampling_targets_response

View File

@ -0,0 +1,37 @@
# 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
#
# http://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 datetime
class _Clock:
def __init__(self):
self.__datetime = datetime.datetime
def now(self) -> datetime.datetime:
return self.__datetime.now()
# pylint: disable=no-self-use
def from_timestamp(self, timestamp: float) -> datetime.datetime:
return datetime.datetime.fromtimestamp(timestamp)
def time_delta(self, seconds: float) -> datetime.timedelta:
return datetime.timedelta(seconds=seconds)
def max(self) -> datetime.datetime:
return datetime.datetime.max

View File

@ -0,0 +1,80 @@
# 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
#
# http://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
from __future__ import annotations
# Disable snake_case naming style so this class can match the sampling rules response from X-Ray
# pylint: disable=invalid-name
class _SamplingRule:
def __init__(
self,
Attributes: dict[str, str] | None = None,
FixedRate: float | None = None,
HTTPMethod: str | None = None,
Host: str | None = None,
Priority: int | None = None,
ReservoirSize: int | None = None,
ResourceARN: str | None = None,
RuleARN: str | None = None,
RuleName: str | None = None,
ServiceName: str | None = None,
ServiceType: str | None = None,
URLPath: str | None = None,
Version: int | None = None,
):
self.Attributes = Attributes if Attributes is not None else {}
self.FixedRate = FixedRate if FixedRate is not None else 0.0
self.HTTPMethod = HTTPMethod if HTTPMethod is not None else ""
self.Host = Host if Host is not None else ""
# Default to value with lower priority than default rule
self.Priority = Priority if Priority is not None else 10001
self.ReservoirSize = ReservoirSize if ReservoirSize is not None else 0
self.ResourceARN = ResourceARN if ResourceARN is not None else ""
self.RuleARN = RuleARN if RuleARN is not None else ""
self.RuleName = RuleName if RuleName is not None else ""
self.ServiceName = ServiceName if ServiceName is not None else ""
self.ServiceType = ServiceType if ServiceType is not None else ""
self.URLPath = URLPath if URLPath is not None else ""
self.Version = Version if Version is not None else 0
def __lt__(self, other: "_SamplingRule") -> bool:
if self.Priority == other.Priority:
# String order priority example:
# "A","Abc","a","ab","abc","abcdef"
return self.RuleName < other.RuleName
return self.Priority < other.Priority
def __eq__(self, other: object) -> bool:
if not isinstance(other, _SamplingRule):
return False
return (
self.FixedRate == other.FixedRate
and self.HTTPMethod == other.HTTPMethod
and self.Host == other.Host
and self.Priority == other.Priority
and self.ReservoirSize == other.ReservoirSize
and self.ResourceARN == other.ResourceARN
and self.RuleARN == other.RuleARN
and self.RuleName == other.RuleName
and self.ServiceName == other.ServiceName
and self.ServiceType == other.ServiceType
and self.URLPath == other.URLPath
and self.Version == other.Version
and self.Attributes == other.Attributes
)

View File

@ -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
#
# http://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
from __future__ import annotations
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import (
_SamplingRule,
)
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_statistics_document import (
_SamplingStatisticsDocument,
)
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import (
_SamplingTarget,
)
class _SamplingRuleApplier:
def __init__(
self,
sampling_rule: _SamplingRule,
client_id: str,
clock: _Clock,
statistics: _SamplingStatisticsDocument | None = None,
target: _SamplingTarget | None = None,
):
self.__client_id = client_id # pylint: disable=W0238
self._clock = clock
self.sampling_rule = sampling_rule
# (TODO) Just store Sampling Rules for now, rest of implementation for later

View File

@ -0,0 +1,50 @@
# 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
#
# http://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
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock
# Disable snake_case naming style so this class can match the statistics document response from X-Ray
# pylint: disable=invalid-name
class _SamplingStatisticsDocument:
def __init__(
self,
clientID: str,
ruleName: str,
RequestCount: int = 0,
BorrowCount: int = 0,
SampleCount: int = 0,
):
self.ClientID = clientID
self.RuleName = ruleName
self.Timestamp = None
self.RequestCount = RequestCount
self.BorrowCount = BorrowCount
self.SampleCount = SampleCount
def snapshot(self, clock: _Clock):
return {
"ClientID": self.ClientID,
"RuleName": self.RuleName,
"Timestamp": clock.now().timestamp(),
"RequestCount": self.RequestCount,
"BorrowCount": self.BorrowCount,
"SampleCount": self.SampleCount,
}

View File

@ -0,0 +1,86 @@
# 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
#
# http://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
from __future__ import annotations
from logging import getLogger
from typing import List
_logger = getLogger(__name__)
# Disable snake_case naming style so this class can match the sampling rules response from X-Ray
# pylint: disable=invalid-name
class _SamplingTarget:
def __init__(
self,
FixedRate: float | None = None,
Interval: int | None = None,
ReservoirQuota: int | None = None,
ReservoirQuotaTTL: float | None = None,
RuleName: str | None = None,
):
self.FixedRate = FixedRate if FixedRate is not None else 0.0
self.Interval = Interval # can be None
self.ReservoirQuota = ReservoirQuota # can be None
self.ReservoirQuotaTTL = ReservoirQuotaTTL # can be None
self.RuleName = RuleName if RuleName is not None else ""
class _UnprocessedStatistics:
def __init__(
self,
ErrorCode: str | None = None,
Message: str | None = None,
RuleName: str | None = None,
):
self.ErrorCode = ErrorCode if ErrorCode is not None else ""
self.Message = Message if ErrorCode is not None else ""
self.RuleName = RuleName if ErrorCode is not None else ""
class _SamplingTargetResponse:
def __init__(
self,
LastRuleModification: float | None,
SamplingTargetDocuments: List[_SamplingTarget] | None = None,
UnprocessedStatistics: List[_UnprocessedStatistics] | None = None,
):
self.LastRuleModification: float = (
LastRuleModification if LastRuleModification is not None else 0.0
)
self.SamplingTargetDocuments: List[_SamplingTarget] = []
if SamplingTargetDocuments is not None:
for document in SamplingTargetDocuments:
try:
self.SamplingTargetDocuments.append(
_SamplingTarget(**document)
)
except TypeError as e:
_logger.debug("TypeError occurred: %s", e)
self.UnprocessedStatistics: List[_UnprocessedStatistics] = []
if UnprocessedStatistics is not None:
for unprocessed in UnprocessedStatistics:
try:
self.UnprocessedStatistics.append(
_UnprocessedStatistics(**unprocessed)
)
except TypeError as e:
_logger.debug("TypeError occurred: %s", e)

View File

@ -0,0 +1,200 @@
# 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
#
# http://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
from __future__ import annotations
import random
from logging import getLogger
from threading import Timer
from typing import Sequence
from typing_extensions import override
# pylint: disable=no-name-in-module
from opentelemetry.context import Context
from opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client import (
DEFAULT_SAMPLING_PROXY_ENDPOINT,
_AwsXRaySamplingClient,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.sampling import (
Decision,
ParentBased,
Sampler,
SamplingResult,
)
from opentelemetry.trace import Link, SpanKind
from opentelemetry.trace.span import TraceState
from opentelemetry.util.types import Attributes
_logger = getLogger(__name__)
DEFAULT_RULES_POLLING_INTERVAL_SECONDS = 300
# WORK IN PROGRESS
# TODO: Rename to AwsXRayRemoteSampler when the implementation is complete and is ready to use
#
# Wrapper class to ensure that all XRay Sampler Functionality in _InternalAwsXRayRemoteSampler
# uses ParentBased logic to respect the parent span's sampling decision
class _AwsXRayRemoteSampler(Sampler):
def __init__(
self,
resource: Resource,
endpoint: str | None = None,
polling_interval: int | None = None,
log_level: str | None = None,
):
self._root = ParentBased(
_InternalAwsXRayRemoteSampler(
resource=resource,
endpoint=endpoint,
polling_interval=polling_interval,
log_level=log_level,
)
)
@override
def should_sample(
self,
parent_context: Context | None,
trace_id: int,
name: str,
kind: SpanKind | None = None,
attributes: Attributes | None = None,
links: Sequence["Link"] | None = None,
trace_state: TraceState | None = None,
) -> "SamplingResult":
return self._root.should_sample(
parent_context,
trace_id,
name,
kind=kind,
attributes=attributes,
links=links,
trace_state=trace_state,
)
# pylint: disable=no-self-use
@override
def get_description(self) -> str:
return f"AwsXRayRemoteSampler{{root:{self._root.get_description()}}}"
# WORK IN PROGRESS
#
# _InternalAwsXRayRemoteSampler 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.
class _InternalAwsXRayRemoteSampler(Sampler):
"""
Remote Sampler for OpenTelemetry that gets sampling configurations from AWS X-Ray
Args:
resource: OpenTelemetry Resource (Required)
endpoint: proxy endpoint for AWS X-Ray Sampling (Optional)
polling_interval: Polling interval for getSamplingRules call (Optional)
log_level: custom log level configuration for remote sampler (Optional)
"""
def __init__(
self,
resource: Resource,
endpoint: str | None = None,
polling_interval: int | None = None,
log_level: str | None = None,
):
# Override default log level
if log_level is not None:
_logger.setLevel(log_level)
if endpoint is None:
_logger.info(
"`endpoint` is `None`. Defaulting to %s",
DEFAULT_SAMPLING_PROXY_ENDPOINT,
)
endpoint = DEFAULT_SAMPLING_PROXY_ENDPOINT
if polling_interval is None or polling_interval < 10:
_logger.info(
"`polling_interval` is `None` or too small. Defaulting to %s",
DEFAULT_RULES_POLLING_INTERVAL_SECONDS,
)
polling_interval = DEFAULT_RULES_POLLING_INTERVAL_SECONDS
self.__xray_client = _AwsXRaySamplingClient(
endpoint, log_level=log_level
)
self.__polling_interval = polling_interval
self.__rule_polling_jitter = random.uniform(0.0, 5.0)
if resource is not None:
self.__resource = resource # pylint: disable=W0238
else:
_logger.warning(
"OTel Resource provided is `None`. Defaulting to empty resource"
)
self.__resource = Resource.get_empty() # pylint: disable=W0238
# Schedule the next rule poll now
# Python Timers only run once, so they need to be recreated for every poll
self._rules_timer = Timer(0, self.__start_sampling_rule_poller)
self._rules_timer.daemon = True # Ensures that when the main thread exits, the Timer threads are killed
self._rules_timer.start()
# (TODO) set up the target poller to go off once after the default interval. Subsequent polls may use new intervals.
# pylint: disable=no-self-use
@override
def should_sample(
self,
parent_context: Context | None,
trace_id: int,
name: str,
kind: SpanKind | None = None,
attributes: Attributes | None = None,
links: Sequence["Link"] | None = None,
trace_state: TraceState | None = None,
) -> "SamplingResult":
return SamplingResult(
decision=Decision.DROP,
attributes=attributes,
trace_state=trace_state,
)
# pylint: disable=no-self-use
@override
def get_description(self) -> str:
description = (
"_InternalAwsXRayRemoteSampler{remote sampling with AWS X-Ray}"
)
return description
def __get_and_update_sampling_rules(self) -> None:
sampling_rules = self.__xray_client.get_sampling_rules() # pylint: disable=W0612 # noqa: F841
# (TODO) update rules cache with sampling rules
def __start_sampling_rule_poller(self) -> None:
self.__get_and_update_sampling_rules()
# Schedule the next sampling rule poll
self._rules_timer = Timer(
self.__polling_interval + self.__rule_polling_jitter,
self.__start_sampling_rule_poller,
)
self._rules_timer.daemon = True
self._rules_timer.start()

View File

@ -6,10 +6,12 @@ pluggy==1.5.0
py-cpuinfo==9.0.0
pytest==7.4.4
pytest-benchmark==4.0.0
requests==2.32.3
tomli==2.0.1
typing_extensions==4.12.2
wrapt==1.16.0
zipp==3.19.2
opentelemetry-sdk==1.12 # when updating, also update in pyproject.toml
opentelemetry-api==1.23 # when updating, also update in pyproject.toml
opentelemetry-sdk==1.23 # when updating, also update in pyproject.toml
-e sdk-extension/opentelemetry-sdk-extension-aws

View File

@ -6,6 +6,7 @@ pluggy==1.5.0
py-cpuinfo==9.0.0
pytest==7.4.4
pytest-benchmark==4.0.0
requests==2.32.3
tomli==2.0.1
typing_extensions==4.12.2
wrapt==1.16.0

View File

@ -0,0 +1,37 @@
# 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
#
# http://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 datetime
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock
class MockClock(_Clock):
def __init__(self, dt: datetime.datetime = datetime.datetime.now()):
self.time_now = dt
super()
def now(self) -> datetime.datetime:
return self.time_now
def add_time(self, seconds: float) -> None:
self.time_now += self.time_delta(seconds)
def set_time(self, dt: datetime.datetime) -> None:
self.time_now = dt

View File

@ -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
}
}
]
}

View File

@ -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
}
}
]
}

View File

@ -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": []
}

View File

@ -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
}
}
]
}

View File

@ -0,0 +1,20 @@
{
"LastRuleModification": 1707551387.0,
"SamplingTargetDocuments": [
{
"FixedRate": 0.0,
"Interval": 100000,
"ReservoirQuota": 100000,
"ReservoirQuotaTTL": 9999999999.0,
"RuleName": "test"
},
{
"FixedRate": 0.0,
"Interval": 1000,
"ReservoirQuota": 100,
"ReservoirQuotaTTL": 9999999999.0,
"RuleName": "Default"
}
],
"UnprocessedStatistics": []
}

View File

@ -0,0 +1,204 @@
# 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
#
# http://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 json
import os
from logging import DEBUG
from unittest import TestCase
from unittest.mock import patch
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler import (
_AwsXRayRemoteSampler,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.sampling import Decision
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
DATA_DIR = os.path.join(TEST_DIR, "data")
def create_spans(
sampled_array, thread_id, span_attributes, remote_sampler, number_of_spans
):
sampled = 0
for _ in range(0, number_of_spans):
if (
remote_sampler.should_sample(
None, 0, "name", attributes=span_attributes
).decision
!= Decision.DROP
):
sampled += 1
sampled_array[thread_id] = sampled
def mocked_requests_get(*args, **kwargs):
class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code
def json(self):
return self.json_data
if kwargs["url"] == "http://127.0.0.1:2000/GetSamplingRules":
with open(
f"{DATA_DIR}/test-remote-sampler_sampling-rules-response-sample.json",
encoding="UTF-8",
) as file:
sample_response = json.load(file)
file.close()
return MockResponse(sample_response, 200)
if kwargs["url"] == "http://127.0.0.1:2000/SamplingTargets":
with open(
f"{DATA_DIR}/test-remote-sampler_sampling-targets-response-sample.json",
encoding="UTF-8",
) as file:
sample_response = json.load(file)
file.close()
return MockResponse(sample_response, 200)
return MockResponse(None, 404)
class TestAwsXRayRemoteSampler(TestCase):
def setUp(self):
self.rs = None
def tearDown(self):
# Clean up timers
if self.rs is not None:
self.rs._root._root._rules_timer.cancel()
@patch(
"opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client._AwsXRaySamplingClient.get_sampling_rules",
return_value=None,
)
def test_create_remote_sampler_with_empty_resource(
self, mocked_get_sampling_rules
):
self.rs = _AwsXRayRemoteSampler(resource=Resource.get_empty())
self.assertIsNotNone(self.rs._root._root._rules_timer)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__polling_interval,
300,
)
self.assertIsNotNone(
self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client
)
self.assertIsNotNone(
self.rs._root._root._InternalAwsXRayRemoteSampler__resource
)
@patch(
"opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client._AwsXRaySamplingClient.get_sampling_rules",
return_value=None,
)
def test_create_remote_sampler_with_populated_resource(
self, mocked_get_sampling_rules
):
self.rs = _AwsXRayRemoteSampler(
resource=Resource.create(
{
"service.name": "test-service-name",
"cloud.platform": "test-cloud-platform",
}
)
)
self.assertIsNotNone(self.rs._root._root._rules_timer)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__polling_interval,
300,
)
self.assertIsNotNone(
self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client
)
self.assertIsNotNone(
self.rs._root._root._InternalAwsXRayRemoteSampler__resource
)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes[
"service.name"
],
"test-service-name",
)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes[
"cloud.platform"
],
"test-cloud-platform",
)
@patch(
"opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client._AwsXRaySamplingClient.get_sampling_rules",
return_value=None,
)
def test_create_remote_sampler_with_all_fields_populated(
self, mocked_get_sampling_rules
):
self.rs = _AwsXRayRemoteSampler(
resource=Resource.create(
{
"service.name": "test-service-name",
"cloud.platform": "test-cloud-platform",
}
),
endpoint="http://abc.com",
polling_interval=120,
log_level=DEBUG,
)
self.assertIsNotNone(self.rs._root._root._rules_timer)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__polling_interval,
120,
)
self.assertIsNotNone(
self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client
)
self.assertIsNotNone(
self.rs._root._root._InternalAwsXRayRemoteSampler__resource
)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client._AwsXRaySamplingClient__get_sampling_rules_endpoint,
"http://abc.com/GetSamplingRules",
)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes[
"service.name"
],
"test-service-name",
)
self.assertEqual(
self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes[
"cloud.platform"
],
"test-cloud-platform",
)
@patch(
"opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client._AwsXRaySamplingClient.get_sampling_rules",
return_value=None,
)
def test_get_description(self, mocked_get_sampling_rules) -> str:
self.rs: _AwsXRayRemoteSampler = _AwsXRayRemoteSampler(
resource=Resource.create({"service.name": "dummy_name"})
)
self.assertEqual(
self.rs.get_description(),
"AwsXRayRemoteSampler{root:ParentBased{root:_InternalAwsXRayRemoteSampler{remote sampling with AWS X-Ray},remoteParentSampled:AlwaysOnSampler,remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler,localParentNotSampled:AlwaysOffSampler}}", # noqa: E501
)

View File

@ -0,0 +1,227 @@
# 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
#
# http://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 json
import os
from logging import getLogger
from unittest import TestCase
from unittest.mock import patch
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client import (
_AwsXRaySamplingClient,
)
SAMPLING_CLIENT_LOGGER_NAME = (
"opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client"
)
_sampling_client_logger = getLogger(SAMPLING_CLIENT_LOGGER_NAME)
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
DATA_DIR = os.path.join(TEST_DIR, "data")
class TestAwsXRaySamplingClient(TestCase):
@patch("requests.Session.post")
def test_get_no_sampling_rules(self, mock_post=None):
mock_post.return_value.configure_mock(
**{"json.return_value": {"SamplingRuleRecords": []}}
)
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
sampling_rules = client.get_sampling_rules()
self.assertTrue(len(sampling_rules) == 0)
@patch("requests.Session.post")
def test_get_invalid_responses(self, mock_post=None):
mock_post.return_value.configure_mock(**{"json.return_value": {}})
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
with self.assertLogs(_sampling_client_logger, level="ERROR"):
sampling_rules = client.get_sampling_rules()
self.assertTrue(len(sampling_rules) == 0)
@patch("requests.Session.post")
def test_get_sampling_rule_missing_in_records(self, mock_post=None):
mock_post.return_value.configure_mock(
**{"json.return_value": {"SamplingRuleRecords": [{}]}}
)
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
with self.assertLogs(_sampling_client_logger, level="ERROR"):
sampling_rules = client.get_sampling_rules()
self.assertTrue(len(sampling_rules) == 0)
@patch("requests.Session.post")
def test_default_values_used_when_missing_properties_in_sampling_rule(
self, mock_post=None
):
mock_post.return_value.configure_mock(
**{
"json.return_value": {
"SamplingRuleRecords": [{"SamplingRule": {}}]
}
}
)
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
sampling_rules = client.get_sampling_rules()
self.assertTrue(len(sampling_rules) == 1)
sampling_rule = sampling_rules[0]
self.assertEqual(sampling_rule.Attributes, {})
self.assertEqual(sampling_rule.FixedRate, 0.0)
self.assertEqual(sampling_rule.HTTPMethod, "")
self.assertEqual(sampling_rule.Host, "")
self.assertEqual(sampling_rule.Priority, 10001)
self.assertEqual(sampling_rule.ReservoirSize, 0)
self.assertEqual(sampling_rule.ResourceARN, "")
self.assertEqual(sampling_rule.RuleARN, "")
self.assertEqual(sampling_rule.RuleName, "")
self.assertEqual(sampling_rule.ServiceName, "")
self.assertEqual(sampling_rule.ServiceType, "")
self.assertEqual(sampling_rule.URLPath, "")
self.assertEqual(sampling_rule.Version, 0)
@patch("requests.Session.post")
def test_get_correct_number_of_sampling_rules(self, mock_post=None):
sampling_records = []
with open(
f"{DATA_DIR}/get-sampling-rules-response-sample.json",
encoding="UTF-8",
) as file:
sample_response = json.load(file)
sampling_records = sample_response["SamplingRuleRecords"]
mock_post.return_value.configure_mock(
**{"json.return_value": sample_response}
)
file.close()
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
sampling_rules = client.get_sampling_rules()
self.assertEqual(len(sampling_rules), 3)
self.assertEqual(len(sampling_rules), len(sampling_records))
self.validate_match_sampling_rules_properties_with_records(
sampling_rules, sampling_records
)
def validate_match_sampling_rules_properties_with_records(
self, sampling_rules, sampling_records
):
for _, (sampling_rule, sampling_record) in enumerate(
zip(sampling_rules, sampling_records)
):
self.assertIsNotNone(sampling_rule.Attributes)
self.assertEqual(
sampling_rule.Attributes,
sampling_record["SamplingRule"]["Attributes"],
)
self.assertIsNotNone(sampling_rule.FixedRate)
self.assertEqual(
sampling_rule.FixedRate,
sampling_record["SamplingRule"]["FixedRate"],
)
self.assertIsNotNone(sampling_rule.HTTPMethod)
self.assertEqual(
sampling_rule.HTTPMethod,
sampling_record["SamplingRule"]["HTTPMethod"],
)
self.assertIsNotNone(sampling_rule.Host)
self.assertEqual(
sampling_rule.Host, sampling_record["SamplingRule"]["Host"]
)
self.assertIsNotNone(sampling_rule.Priority)
self.assertEqual(
sampling_rule.Priority,
sampling_record["SamplingRule"]["Priority"],
)
self.assertIsNotNone(sampling_rule.ReservoirSize)
self.assertEqual(
sampling_rule.ReservoirSize,
sampling_record["SamplingRule"]["ReservoirSize"],
)
self.assertIsNotNone(sampling_rule.ResourceARN)
self.assertEqual(
sampling_rule.ResourceARN,
sampling_record["SamplingRule"]["ResourceARN"],
)
self.assertIsNotNone(sampling_rule.RuleARN)
self.assertEqual(
sampling_rule.RuleARN,
sampling_record["SamplingRule"]["RuleARN"],
)
self.assertIsNotNone(sampling_rule.RuleName)
self.assertEqual(
sampling_rule.RuleName,
sampling_record["SamplingRule"]["RuleName"],
)
self.assertIsNotNone(sampling_rule.ServiceName)
self.assertEqual(
sampling_rule.ServiceName,
sampling_record["SamplingRule"]["ServiceName"],
)
self.assertIsNotNone(sampling_rule.ServiceType)
self.assertEqual(
sampling_rule.ServiceType,
sampling_record["SamplingRule"]["ServiceType"],
)
self.assertIsNotNone(sampling_rule.URLPath)
self.assertEqual(
sampling_rule.URLPath,
sampling_record["SamplingRule"]["URLPath"],
)
self.assertIsNotNone(sampling_rule.Version)
self.assertEqual(
sampling_rule.Version,
sampling_record["SamplingRule"]["Version"],
)
@patch("requests.Session.post")
def test_get_sampling_targets(self, mock_post=None):
with open(
f"{DATA_DIR}/get-sampling-targets-response-sample.json",
encoding="UTF-8",
) as file:
sample_response = json.load(file)
mock_post.return_value.configure_mock(
**{"json.return_value": sample_response}
)
file.close()
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
sampling_targets_response = client.get_sampling_targets(statistics=[])
self.assertEqual(
len(sampling_targets_response.SamplingTargetDocuments), 2
)
self.assertEqual(
len(sampling_targets_response.UnprocessedStatistics), 0
)
self.assertEqual(
sampling_targets_response.LastRuleModification, 1707551387.0
)
@patch("requests.Session.post")
def test_get_invalid_sampling_targets(self, mock_post=None):
mock_post.return_value.configure_mock(
**{
"json.return_value": {
"LastRuleModification": None,
"SamplingTargetDocuments": None,
"UnprocessedStatistics": None,
}
}
)
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
sampling_targets_response = client.get_sampling_targets(statistics=[])
self.assertEqual(sampling_targets_response.SamplingTargetDocuments, [])
self.assertEqual(sampling_targets_response.UnprocessedStatistics, [])
self.assertEqual(sampling_targets_response.LastRuleModification, 0.0)

View File

@ -0,0 +1,34 @@
# 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
#
# http://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
from unittest import TestCase
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock
class TestClock(TestCase):
def test_from_timestamp(self):
pass
def test_time_delta(self):
clock = _Clock()
dt = clock.from_timestamp(1707551387.0)
delta = clock.time_delta(3600)
new_dt = dt + delta
self.assertTrue(new_dt.timestamp() - dt.timestamp() == 3600)

View File

@ -0,0 +1,106 @@
# 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
#
# http://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
from unittest import TestCase
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import (
_SamplingRule,
)
class TestSamplingRule(TestCase):
def test_sampling_rule_ordering(self):
rule1 = _SamplingRule(Priority=1, RuleName="abcdef", Version=1)
rule2 = _SamplingRule(Priority=100, RuleName="A", Version=1)
rule3 = _SamplingRule(Priority=100, RuleName="Abc", Version=1)
rule4 = _SamplingRule(Priority=100, RuleName="ab", Version=1)
rule5 = _SamplingRule(Priority=100, RuleName="abc", Version=1)
rule6 = _SamplingRule(Priority=200, RuleName="abcdef", Version=1)
self.assertTrue(rule1 < rule2 < rule3 < rule4 < rule5 < rule6)
def test_sampling_rule_equality(self):
sampling_rule = _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,
)
sampling_rule_attr_unordered = _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,
)
self.assertTrue(sampling_rule == sampling_rule_attr_unordered)
sampling_rule_updated = _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,
)
sampling_rule_updated_2 = _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,
)
self.assertFalse(sampling_rule == sampling_rule_updated)
self.assertFalse(sampling_rule == sampling_rule_updated_2)

View File

@ -0,0 +1,26 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from unittest import TestCase
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
DATA_DIR = os.path.join(TEST_DIR, "data")
CLIENT_ID = "12345678901234567890abcd"
# pylint: disable=no-member
class TestSamplingRuleApplier(TestCase):
pass

View File

@ -0,0 +1,55 @@
# 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
#
# http://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 datetime
from unittest import TestCase
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_statistics_document import (
_SamplingStatisticsDocument,
)
from ._mock_clock import MockClock
class TestSamplingStatisticsDocument(TestCase):
def test_sampling_statistics_document_inputs(self):
statistics = _SamplingStatisticsDocument("", "")
self.assertEqual(statistics.ClientID, "")
self.assertEqual(statistics.RuleName, "")
self.assertEqual(statistics.BorrowCount, 0)
self.assertEqual(statistics.SampleCount, 0)
self.assertEqual(statistics.RequestCount, 0)
statistics = _SamplingStatisticsDocument(
"client_id", "rule_name", 1, 2, 3
)
self.assertEqual(statistics.ClientID, "client_id")
self.assertEqual(statistics.RuleName, "rule_name")
self.assertEqual(statistics.RequestCount, 1)
self.assertEqual(statistics.BorrowCount, 2)
self.assertEqual(statistics.SampleCount, 3)
clock = MockClock(datetime.datetime.fromtimestamp(1707551387.0))
snapshot = statistics.snapshot(clock)
self.assertEqual(snapshot.get("ClientID"), "client_id")
self.assertEqual(snapshot.get("RuleName"), "rule_name")
self.assertEqual(snapshot.get("Timestamp"), 1707551387.0)
self.assertEqual(snapshot.get("RequestCount"), 1)
self.assertEqual(snapshot.get("BorrowCount"), 2)
self.assertEqual(snapshot.get("SampleCount"), 3)

View File

@ -0,0 +1,65 @@
# 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
#
# http://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
from unittest import TestCase
# pylint: disable=no-name-in-module
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import (
_SamplingTargetResponse,
)
class TestSamplingTarget(TestCase):
def test_sampling_target_response_with_none_inputs(self):
target_response = _SamplingTargetResponse(None, None, None)
self.assertEqual(target_response.LastRuleModification, 0.0)
self.assertEqual(target_response.SamplingTargetDocuments, [])
self.assertEqual(target_response.UnprocessedStatistics, [])
def test_sampling_target_response_with_invalid_inputs(self):
target_response = _SamplingTargetResponse(1.0, [{}], [{}])
self.assertEqual(target_response.LastRuleModification, 1.0)
self.assertEqual(len(target_response.SamplingTargetDocuments), 1)
self.assertEqual(
target_response.SamplingTargetDocuments[0].FixedRate, 0
)
self.assertEqual(
target_response.SamplingTargetDocuments[0].Interval, None
)
self.assertEqual(
target_response.SamplingTargetDocuments[0].ReservoirQuota, None
)
self.assertEqual(
target_response.SamplingTargetDocuments[0].ReservoirQuotaTTL, None
)
self.assertEqual(
target_response.SamplingTargetDocuments[0].RuleName, ""
)
self.assertEqual(len(target_response.UnprocessedStatistics), 1)
self.assertEqual(
target_response.UnprocessedStatistics[0].ErrorCode, ""
)
self.assertEqual(target_response.UnprocessedStatistics[0].Message, "")
self.assertEqual(target_response.UnprocessedStatistics[0].RuleName, "")
target_response = _SamplingTargetResponse(
1.0, [{"foo": "bar"}], [{"dog": "cat"}]
)
self.assertEqual(len(target_response.SamplingTargetDocuments), 0)
self.assertEqual(len(target_response.UnprocessedStatistics), 0)