Skip to content

Commit 05113dc

Browse files
jj22eexrmxtammy-baylis-swi
authored
AWS X-Ray Remote Sampler Part 2 - Add Rules Caching, Rules Matching Logic, Rate Limiter, and Sampling Targets Poller (#3761)
* Add Rules Caching, Rules Matching Logic, Rate Limiter, and Sampling Targets Poller * update changelog * address comments wip 1 * typecheck fixes --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> Co-authored-by: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com>
1 parent c6225ea commit 05113dc

24 files changed

Lines changed: 2522 additions & 76 deletions

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ pythonVersion = "3.10"
202202
reportPrivateUsage = false # Ignore private attributes added by instrumentation packages.
203203
# Add progressively instrumentation packages here.
204204
include = [
205+
"instrumentation/opentelemetry-instrumentation-aiohttp-client",
205206
"instrumentation/opentelemetry-instrumentation-aiokafka",
206207
"instrumentation/opentelemetry-instrumentation-asyncclick",
207208
"instrumentation/opentelemetry-instrumentation-threading",
@@ -214,6 +215,7 @@ include = [
214215
"exporter/opentelemetry-exporter-credential-provider-gcp",
215216
"instrumentation/opentelemetry-instrumentation-aiohttp-client",
216217
"opamp/opentelemetry-opamp-client",
218+
"sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace",
217219
]
218220
# We should also add type hints to the test suite - It helps on finding bugs.
219221
# We are excluding for now because it's easier, and more important to add to the instrumentation packages.

sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
- Add caching, matching, and targets logic to complete AWS X-Ray Remote Sampler implementation
13+
([#3366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3761))
14+
1015
## Version 2.1.0 (2024-12-24)
1116

1217
- Make ec2 resource detector silent when loaded outside AWS

sdk-extension/opentelemetry-sdk-extension-aws/README.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ populate `resource` attributes by creating a `TraceProvider` using the `AwsEc2Re
7474
Refer to each detectors' docstring to determine any possible requirements for that
7575
detector.
7676

77+
78+
Usage (AWS X-Ray Remote Sampler)
79+
--------------------------------
80+
81+
Use the provided AWS X-Ray Remote Sampler by setting this sampler in your instrumented application:
82+
83+
.. code-block:: python
84+
85+
from opentelemetry.sdk.extension.aws.trace.sampler import AwsXRayRemoteSampler
86+
from opentelemetry import trace
87+
from opentelemetry.sdk.resources import Resource
88+
from opentelemetry.sdk.trace import TracerProvider
89+
from opentelemetry.semconv.resource import ResourceAttributes
90+
from opentelemetry.util.types import Attributes
91+
92+
resource = Resource.create(attributes={
93+
ResourceAttributes.SERVICE_NAME: "myService",
94+
ResourceAttributes.CLOUD_PLATFORM: "aws_ec2",
95+
})
96+
xraySampler = AwsXRayRemoteSampler(resource=resource, polling_interval=300)
97+
trace.set_tracer_provider(TracerProvider(sampler=xraySampler))
98+
99+
77100
References
78101
----------
79102

sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ classifiers = [
2525
"Programming Language :: Python :: 3.14",
2626
]
2727
dependencies = [
28-
"opentelemetry-api ~= 1.23",
29-
"opentelemetry-sdk ~= 1.23",
30-
"opentelemetry-instrumentation ~= 0.44b0",
31-
"opentelemetry-semantic-conventions ~= 0.44b0",
28+
"opentelemetry-api ~= 1.26",
29+
"opentelemetry-sdk ~= 1.26",
30+
"opentelemetry-instrumentation ~= 0.47b0",
31+
"opentelemetry-semantic-conventions ~= 0.47b0",
3232
"requests ~= 2.28",
3333
]
3434

@@ -42,9 +42,8 @@ aws_eks = "opentelemetry.sdk.extension.aws.resource.eks:AwsEksResourceDetector"
4242
aws_elastic_beanstalk = "opentelemetry.sdk.extension.aws.resource.beanstalk:AwsBeanstalkResourceDetector"
4343
aws_lambda = "opentelemetry.sdk.extension.aws.resource._lambda:AwsLambdaResourceDetector"
4444

45-
# TODO: Uncomment this when Sampler implementation is complete
46-
# [project.entry-points.opentelemetry_sampler]
47-
# aws_xray_remote_sampler = "opentelemetry.sdk.extension.aws.trace.sampler:AwsXRayRemoteSampler"
45+
[project.entry-points.opentelemetry_sampler]
46+
aws_xray_remote_sampler = "opentelemetry.sdk.extension.aws.trace.sampler:AwsXRayRemoteSampler"
4847

4948
[project.urls]
5049
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/sdk-extension/opentelemetry-sdk-extension-aws"

sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
# pylint: disable=no-name-in-module
1616
from opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler import (
17-
_AwsXRayRemoteSampler,
17+
AwsXRayRemoteSampler,
1818
)
1919

20-
__all__ = ["_AwsXRayRemoteSampler"]
20+
__all__ = ["AwsXRayRemoteSampler"]

sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler/_aws_xray_sampling_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
DEFAULT_SAMPLING_PROXY_ENDPOINT = "http://127.0.0.1:2000"
3838

3939

40-
class _AwsXRaySamplingClient:
40+
class _AwsXRaySamplingClient: # pyright: ignore[reportUnusedClass]
4141
def __init__(
4242
self,
4343
endpoint: str = DEFAULT_SAMPLING_PROXY_ENDPOINT,

sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler/_clock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import datetime
2020

2121

22-
class _Clock:
22+
class _Clock: # pyright: ignore[reportUnusedClass]
2323
def __init__(self):
2424
self.__datetime = datetime.datetime
2525

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Includes work from:
16+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
17+
# SPDX-License-Identifier: Apache-2.0
18+
19+
from __future__ import annotations
20+
21+
from typing import Sequence
22+
23+
# pylint: disable=no-name-in-module
24+
from opentelemetry.context import Context
25+
from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock
26+
from opentelemetry.sdk.extension.aws.trace.sampler._rate_limiting_sampler import (
27+
_RateLimitingSampler,
28+
)
29+
from opentelemetry.sdk.trace.sampling import (
30+
Decision,
31+
Sampler,
32+
SamplingResult,
33+
TraceIdRatioBased,
34+
)
35+
from opentelemetry.trace import Link, SpanKind
36+
from opentelemetry.trace.span import TraceState
37+
from opentelemetry.util.types import Attributes
38+
39+
40+
class _FallbackSampler(Sampler): # pyright: ignore[reportUnusedClass]
41+
def __init__(self, clock: _Clock):
42+
self.__rate_limiting_sampler = _RateLimitingSampler(1, clock)
43+
self.__fixed_rate_sampler = TraceIdRatioBased(0.05)
44+
45+
def should_sample(
46+
self,
47+
parent_context: Context | None,
48+
trace_id: int,
49+
name: str,
50+
kind: SpanKind | None = None,
51+
attributes: Attributes | None = None,
52+
links: Sequence["Link"] | None = None,
53+
trace_state: TraceState | None = None,
54+
) -> "SamplingResult":
55+
sampling_result = self.__rate_limiting_sampler.should_sample(
56+
parent_context,
57+
trace_id,
58+
name,
59+
kind=kind,
60+
attributes=attributes,
61+
links=links,
62+
trace_state=trace_state,
63+
)
64+
if sampling_result.decision is not Decision.DROP:
65+
return sampling_result
66+
return self.__fixed_rate_sampler.should_sample(
67+
parent_context,
68+
trace_id,
69+
name,
70+
kind=kind,
71+
attributes=attributes,
72+
links=links,
73+
trace_state=trace_state,
74+
)
75+
76+
# pylint: disable=no-self-use
77+
def get_description(self) -> str:
78+
description = "FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests}"
79+
return description
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Includes work from:
16+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
17+
# SPDX-License-Identifier: Apache-2.0
18+
19+
from __future__ import annotations
20+
21+
import re
22+
23+
from opentelemetry.semconv._incubating.attributes.cloud_attributes import (
24+
CloudPlatformValues,
25+
)
26+
from opentelemetry.util.types import Attributes, AttributeValue
27+
28+
cloud_platform_mapping = {
29+
CloudPlatformValues.AWS_LAMBDA.value: "AWS::Lambda::Function",
30+
CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value: "AWS::ElasticBeanstalk::Environment",
31+
CloudPlatformValues.AWS_EC2.value: "AWS::EC2::Instance",
32+
CloudPlatformValues.AWS_ECS.value: "AWS::ECS::Container",
33+
CloudPlatformValues.AWS_EKS.value: "AWS::EKS::Container",
34+
}
35+
36+
37+
class _Matcher:
38+
@staticmethod
39+
def wild_card_match(
40+
text: AttributeValue | None = None, pattern: str | None = None
41+
) -> bool:
42+
if pattern == "*":
43+
return True
44+
if not isinstance(text, str) or pattern is None:
45+
return False
46+
if len(pattern) == 0:
47+
return len(text) == 0
48+
for char in pattern:
49+
if char in ("*", "?"):
50+
return (
51+
re.fullmatch(_Matcher.to_regex_pattern(pattern), text)
52+
is not None
53+
)
54+
return pattern == text
55+
56+
@staticmethod
57+
def to_regex_pattern(rule_pattern: str) -> str:
58+
token_start = -1
59+
regex_pattern = ""
60+
for index, char in enumerate(rule_pattern):
61+
char = rule_pattern[index]
62+
if char in ("*", "?"):
63+
if token_start != -1:
64+
regex_pattern += re.escape(rule_pattern[token_start:index])
65+
token_start = -1
66+
if char == "*":
67+
regex_pattern += ".*"
68+
else:
69+
regex_pattern += "."
70+
else:
71+
if token_start == -1:
72+
token_start = index
73+
if token_start != -1:
74+
regex_pattern += re.escape(rule_pattern[token_start:])
75+
return regex_pattern
76+
77+
@staticmethod
78+
def attribute_match(
79+
attributes: Attributes | None = None,
80+
rule_attributes: dict[str, str] | None = None,
81+
) -> bool:
82+
if rule_attributes is None or len(rule_attributes) == 0:
83+
return True
84+
if (
85+
attributes is None
86+
or len(attributes) == 0
87+
or len(rule_attributes) > len(attributes)
88+
):
89+
return False
90+
91+
matched_count = 0
92+
for key, val in attributes.items():
93+
text_to_match = val
94+
pattern = rule_attributes.get(key, None)
95+
if pattern is None:
96+
continue
97+
if _Matcher.wild_card_match(text_to_match, pattern):
98+
matched_count += 1
99+
return matched_count == len(rule_attributes)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Includes work from:
16+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
17+
# SPDX-License-Identifier: Apache-2.0
18+
19+
from decimal import Decimal
20+
from threading import Lock
21+
22+
# pylint: disable=no-name-in-module
23+
from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock
24+
25+
26+
class _RateLimiter: # pyright: ignore[reportUnusedClass]
27+
def __init__(self, max_balance_in_seconds: int, quota: int, clock: _Clock):
28+
# max_balance_in_seconds is usually 1
29+
# pylint: disable=invalid-name
30+
self.MAX_BALANCE_MILLIS = Decimal(max_balance_in_seconds * 1000.0)
31+
self._clock = clock
32+
33+
self._quota = Decimal(quota)
34+
self.__wallet_floor_millis = Decimal(
35+
self._clock.now().timestamp() * 1000.0
36+
)
37+
# current "wallet_balance" would be ceiling - floor
38+
39+
self.__lock = Lock()
40+
41+
def try_spend(self, cost: float) -> bool:
42+
if self._quota == 0:
43+
return False
44+
45+
quota_per_millis = self._quota / Decimal(1000.0)
46+
47+
# assume divide by zero not possible
48+
cost_in_millis = Decimal(cost) / quota_per_millis
49+
50+
with self.__lock:
51+
wallet_ceiling_millis = Decimal(
52+
self._clock.now().timestamp() * 1000.0
53+
)
54+
current_balance_millis = (
55+
wallet_ceiling_millis - self.__wallet_floor_millis
56+
)
57+
current_balance_millis = min(
58+
current_balance_millis, self.MAX_BALANCE_MILLIS
59+
)
60+
pending_remaining_balance_millis = (
61+
current_balance_millis - cost_in_millis
62+
)
63+
if pending_remaining_balance_millis >= 0:
64+
self.__wallet_floor_millis = (
65+
wallet_ceiling_millis - pending_remaining_balance_millis
66+
)
67+
return True
68+
# No changes to the wallet state
69+
return False

0 commit comments

Comments
 (0)