Skip to content

Commit 032d6c6

Browse files
jj22eexrmx
andauthored
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 <[email protected]>
1 parent 973d10d commit 032d6c6

27 files changed

+1632
-6
lines changed

.github/component_owners.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,11 @@ components:
3333
- dorkolog
3434

3535
propagator/opentelemetry-propagator-aws-xray:
36-
- NathanielRN
36+
- jj22ee
3737

3838
sdk-extension/opentelemetry-sdk-extension-aws:
39-
- NathanielRN
40-
- Kausik-A
4139
- srprash
40+
- jj22ee
4241

4342
instrumentation/opentelemetry-instrumentation-tortoiseorm:
4443
- tonybaloney

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685))
2727
- `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics
2828
([#3666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3666))
29-
29+
- `opentelemetry-sdk-extension-aws` Add AWS X-Ray Remote Sampler with initial Rules Poller implementation
30+
([#3366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3366))
3031

3132
## Version 1.36.0/0.57b0 (2025-07-29)
3233

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ classifiers = [
2525
"Programming Language :: Python :: 3.13",
2626
]
2727
dependencies = [
28-
"opentelemetry-sdk ~= 1.12",
28+
"opentelemetry-api ~= 1.23",
29+
"opentelemetry-sdk ~= 1.23",
30+
"opentelemetry-instrumentation ~= 0.44b0",
31+
"opentelemetry-semantic-conventions ~= 0.44b0",
32+
"requests ~= 2.28",
2933
]
3034

3135
[project.entry-points.opentelemetry_id_generator]
@@ -38,6 +42,10 @@ aws_eks = "opentelemetry.sdk.extension.aws.resource.eks:AwsEksResourceDetector"
3842
aws_elastic_beanstalk = "opentelemetry.sdk.extension.aws.resource.beanstalk:AwsBeanstalkResourceDetector"
3943
aws_lambda = "opentelemetry.sdk.extension.aws.resource._lambda:AwsLambdaResourceDetector"
4044

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"
48+
4149
[project.urls]
4250
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/sdk-extension/opentelemetry-sdk-extension-aws"
4351
Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
# pylint: disable=no-name-in-module
16+
from opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler import (
17+
_AwsXRayRemoteSampler,
18+
)
19+
20+
__all__ = ["_AwsXRayRemoteSampler"]
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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 json
22+
from logging import getLogger
23+
from typing import List
24+
25+
import requests
26+
27+
# pylint: disable=no-name-in-module
28+
from opentelemetry.instrumentation.utils import suppress_instrumentation
29+
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import (
30+
_SamplingRule,
31+
)
32+
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import (
33+
_SamplingTargetResponse,
34+
)
35+
36+
_logger = getLogger(__name__)
37+
DEFAULT_SAMPLING_PROXY_ENDPOINT = "http://127.0.0.1:2000"
38+
39+
40+
class _AwsXRaySamplingClient:
41+
def __init__(
42+
self,
43+
endpoint: str = DEFAULT_SAMPLING_PROXY_ENDPOINT,
44+
log_level: str | None = None,
45+
):
46+
# Override default log level
47+
if log_level is not None:
48+
_logger.setLevel(log_level)
49+
50+
self.__get_sampling_rules_endpoint = endpoint + "/GetSamplingRules"
51+
self.__get_sampling_targets_endpoint = endpoint + "/SamplingTargets"
52+
53+
self.__session = requests.Session()
54+
55+
def get_sampling_rules(self) -> List[_SamplingRule]:
56+
sampling_rules: List["_SamplingRule"] = []
57+
headers = {"content-type": "application/json"}
58+
59+
with suppress_instrumentation():
60+
try:
61+
xray_response = self.__session.post(
62+
url=self.__get_sampling_rules_endpoint,
63+
headers=headers,
64+
timeout=20,
65+
)
66+
sampling_rules_response = xray_response.json()
67+
if (
68+
sampling_rules_response is None
69+
or "SamplingRuleRecords" not in sampling_rules_response
70+
):
71+
_logger.error(
72+
"SamplingRuleRecords is missing in getSamplingRules response: %s",
73+
sampling_rules_response,
74+
)
75+
return []
76+
sampling_rules_records = sampling_rules_response[
77+
"SamplingRuleRecords"
78+
]
79+
for record in sampling_rules_records:
80+
if "SamplingRule" not in record:
81+
_logger.error(
82+
"SamplingRule is missing in SamplingRuleRecord"
83+
)
84+
else:
85+
sampling_rules.append(
86+
_SamplingRule(**record["SamplingRule"])
87+
)
88+
89+
except requests.exceptions.RequestException as req_err:
90+
_logger.error("Request error occurred: %s", req_err)
91+
except json.JSONDecodeError as json_err:
92+
_logger.error("Error in decoding JSON response: %s", json_err)
93+
# pylint: disable=broad-exception-caught
94+
except Exception as err:
95+
_logger.error(
96+
"Error occurred when attempting to fetch rules: %s", err
97+
)
98+
99+
return sampling_rules
100+
101+
def get_sampling_targets(
102+
self, statistics: List["dict[str, str | float | int]"]
103+
) -> _SamplingTargetResponse:
104+
sampling_targets_response = _SamplingTargetResponse(
105+
LastRuleModification=None,
106+
SamplingTargetDocuments=None,
107+
UnprocessedStatistics=None,
108+
)
109+
headers = {"content-type": "application/json"}
110+
111+
with suppress_instrumentation():
112+
try:
113+
xray_response = self.__session.post(
114+
url=self.__get_sampling_targets_endpoint,
115+
headers=headers,
116+
timeout=20,
117+
json={"SamplingStatisticsDocuments": statistics},
118+
)
119+
xray_response_json = xray_response.json()
120+
if (
121+
xray_response_json is None
122+
or "SamplingTargetDocuments" not in xray_response_json
123+
or "LastRuleModification" not in xray_response_json
124+
):
125+
_logger.debug(
126+
"getSamplingTargets response is invalid. Unable to update targets."
127+
)
128+
return sampling_targets_response
129+
130+
sampling_targets_response = _SamplingTargetResponse(
131+
**xray_response_json
132+
)
133+
except requests.exceptions.RequestException as req_err:
134+
_logger.debug("Request error occurred: %s", req_err)
135+
except json.JSONDecodeError as json_err:
136+
_logger.debug("Error in decoding JSON response: %s", json_err)
137+
# pylint: disable=broad-exception-caught
138+
except Exception as err:
139+
_logger.debug(
140+
"Error occurred when attempting to fetch targets: %s", err
141+
)
142+
143+
return sampling_targets_response
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
import datetime
20+
21+
22+
class _Clock:
23+
def __init__(self):
24+
self.__datetime = datetime.datetime
25+
26+
def now(self) -> datetime.datetime:
27+
return self.__datetime.now()
28+
29+
# pylint: disable=no-self-use
30+
def from_timestamp(self, timestamp: float) -> datetime.datetime:
31+
return datetime.datetime.fromtimestamp(timestamp)
32+
33+
def time_delta(self, seconds: float) -> datetime.timedelta:
34+
return datetime.timedelta(seconds=seconds)
35+
36+
def max(self) -> datetime.datetime:
37+
return datetime.datetime.max
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
22+
# Disable snake_case naming style so this class can match the sampling rules response from X-Ray
23+
# pylint: disable=invalid-name
24+
class _SamplingRule:
25+
def __init__(
26+
self,
27+
Attributes: dict[str, str] | None = None,
28+
FixedRate: float | None = None,
29+
HTTPMethod: str | None = None,
30+
Host: str | None = None,
31+
Priority: int | None = None,
32+
ReservoirSize: int | None = None,
33+
ResourceARN: str | None = None,
34+
RuleARN: str | None = None,
35+
RuleName: str | None = None,
36+
ServiceName: str | None = None,
37+
ServiceType: str | None = None,
38+
URLPath: str | None = None,
39+
Version: int | None = None,
40+
):
41+
self.Attributes = Attributes if Attributes is not None else {}
42+
self.FixedRate = FixedRate if FixedRate is not None else 0.0
43+
self.HTTPMethod = HTTPMethod if HTTPMethod is not None else ""
44+
self.Host = Host if Host is not None else ""
45+
# Default to value with lower priority than default rule
46+
self.Priority = Priority if Priority is not None else 10001
47+
self.ReservoirSize = ReservoirSize if ReservoirSize is not None else 0
48+
self.ResourceARN = ResourceARN if ResourceARN is not None else ""
49+
self.RuleARN = RuleARN if RuleARN is not None else ""
50+
self.RuleName = RuleName if RuleName is not None else ""
51+
self.ServiceName = ServiceName if ServiceName is not None else ""
52+
self.ServiceType = ServiceType if ServiceType is not None else ""
53+
self.URLPath = URLPath if URLPath is not None else ""
54+
self.Version = Version if Version is not None else 0
55+
56+
def __lt__(self, other: "_SamplingRule") -> bool:
57+
if self.Priority == other.Priority:
58+
# String order priority example:
59+
# "A","Abc","a","ab","abc","abcdef"
60+
return self.RuleName < other.RuleName
61+
return self.Priority < other.Priority
62+
63+
def __eq__(self, other: object) -> bool:
64+
if not isinstance(other, _SamplingRule):
65+
return False
66+
return (
67+
self.FixedRate == other.FixedRate
68+
and self.HTTPMethod == other.HTTPMethod
69+
and self.Host == other.Host
70+
and self.Priority == other.Priority
71+
and self.ReservoirSize == other.ReservoirSize
72+
and self.ResourceARN == other.ResourceARN
73+
and self.RuleARN == other.RuleARN
74+
and self.RuleName == other.RuleName
75+
and self.ServiceName == other.ServiceName
76+
and self.ServiceType == other.ServiceType
77+
and self.URLPath == other.URLPath
78+
and self.Version == other.Version
79+
and self.Attributes == other.Attributes
80+
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
# pylint: disable=no-name-in-module
22+
from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock
23+
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import (
24+
_SamplingRule,
25+
)
26+
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_statistics_document import (
27+
_SamplingStatisticsDocument,
28+
)
29+
from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import (
30+
_SamplingTarget,
31+
)
32+
33+
34+
class _SamplingRuleApplier:
35+
def __init__(
36+
self,
37+
sampling_rule: _SamplingRule,
38+
client_id: str,
39+
clock: _Clock,
40+
statistics: _SamplingStatisticsDocument | None = None,
41+
target: _SamplingTarget | None = None,
42+
):
43+
self.__client_id = client_id # pylint: disable=W0238
44+
self._clock = clock
45+
self.sampling_rule = sampling_rule
46+
47+
# (TODO) Just store Sampling Rules for now, rest of implementation for later

0 commit comments

Comments
 (0)