Skip to content

Commit 543792f

Browse files
jj22eesrprash
andauthored
AWS X-Ray Remote Sampler Part 1 - Initial Rules Poller Implementation (#33)
*Issue #, if available:* First PR of 3 parts for adding the X-Ray remote sampling support for OTel Python SDK. *Description of changes:* - Python Classes - `AwsXRayRemoteSampler` - extends `opentelemetry.sdk.trace.sampling.Sampler` and implements `should_sample`. - Upon initialization, starts polling for sampling rules by scheduling a threading.Timer to execute a poll after a configurable interval of time. After this interval, it will repeat this process indefinitely by scheduling the same threading.Timer upon completion of the previous timer. - OTel `resource`, Collector `endpoint`, rules `polling_interval` are configurable. - `AwsXRaySamplingClient` - client to call GetSamplingRules - `SamplingRule` - Class for SamplingRules type Testing Script to poll Sampling Rules every 5 seconds: ``` import logging import time from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler from opentelemetry.sdk.resources import Resource logging.basicConfig(level=logging.INFO) sampler = AwsXRayRemoteSampler(Resource.get_empty(), polling_interval=5) time.sleep(15) ``` Output: ``` 88665a53c0dd:sampler jjllee$ python3 mytesting.py INFO:amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler:Got Sampling Rules: {'[{"Attributes": {}, "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}, {"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}]'} INFO:amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler:Got Sampling Rules: {'[{"Attributes": {}, "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}, {"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}]'} INFO:amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler:Got Sampling Rules: {'[{"Attributes": {}, "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}, {"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}]'} 88665a53c0dd:sampler jjllee$ ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Prashant Srivastava <[email protected]>
1 parent 552ba02 commit 543792f

File tree

6 files changed

+404
-0
lines changed

6 files changed

+404
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import json
4+
from logging import getLogger
5+
6+
import requests
7+
8+
from amazon.opentelemetry.distro.sampler._sampling_rule import _SamplingRule
9+
10+
_logger = getLogger(__name__)
11+
12+
13+
class _AwsXRaySamplingClient:
14+
def __init__(self, endpoint=None, log_level=None):
15+
# Override default log level
16+
if log_level is not None:
17+
_logger.setLevel(log_level)
18+
19+
if endpoint is None:
20+
_logger.error("endpoint must be specified")
21+
self.__get_sampling_rules_endpoint = endpoint + "/GetSamplingRules"
22+
23+
def get_sampling_rules(self) -> [_SamplingRule]:
24+
sampling_rules = []
25+
headers = {"content-type": "application/json"}
26+
27+
try:
28+
xray_response = requests.post(url=self.__get_sampling_rules_endpoint, headers=headers, timeout=20)
29+
if xray_response is None:
30+
_logger.error("GetSamplingRules response is None")
31+
return []
32+
sampling_rules_response = xray_response.json()
33+
if "SamplingRuleRecords" not in sampling_rules_response:
34+
_logger.error(
35+
"SamplingRuleRecords is missing in getSamplingRules response: %s", sampling_rules_response
36+
)
37+
return []
38+
39+
sampling_rules_records = sampling_rules_response["SamplingRuleRecords"]
40+
for record in sampling_rules_records:
41+
if "SamplingRule" not in record:
42+
_logger.error("SamplingRule is missing in SamplingRuleRecord")
43+
else:
44+
sampling_rules.append(_SamplingRule(**record["SamplingRule"]))
45+
46+
except requests.exceptions.RequestException as req_err:
47+
_logger.error("Request error occurred: %s", req_err)
48+
except json.JSONDecodeError as json_err:
49+
_logger.error("Error in decoding JSON response: %s", json_err)
50+
51+
return sampling_rules
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
5+
# Disable snake_case naming style so this class can match the sampling rules response from X-Ray
6+
# pylint: disable=invalid-name
7+
class _SamplingRule:
8+
def __init__(
9+
self,
10+
Attributes: dict = None,
11+
FixedRate=None,
12+
HTTPMethod=None,
13+
Host=None,
14+
Priority=None,
15+
ReservoirSize=None,
16+
ResourceARN=None,
17+
RuleARN=None,
18+
RuleName=None,
19+
ServiceName=None,
20+
ServiceType=None,
21+
URLPath=None,
22+
Version=None,
23+
):
24+
self.Attributes = Attributes if Attributes is not None else {}
25+
self.FixedRate = FixedRate if FixedRate is not None else 0.0
26+
self.HTTPMethod = HTTPMethod if HTTPMethod is not None else ""
27+
self.Host = Host if Host is not None else ""
28+
# Default to value with lower priority than default rule
29+
self.Priority = Priority if Priority is not None else 10001
30+
self.ReservoirSize = ReservoirSize if ReservoirSize is not None else 0
31+
self.ResourceARN = ResourceARN if ResourceARN is not None else ""
32+
self.RuleARN = RuleARN if RuleARN is not None else ""
33+
self.RuleName = RuleName if RuleName is not None else ""
34+
self.ServiceName = ServiceName if ServiceName is not None else ""
35+
self.ServiceType = ServiceType if ServiceType is not None else ""
36+
self.URLPath = URLPath if URLPath is not None else ""
37+
self.Version = Version if Version is not None else 0
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import json
4+
from logging import getLogger
5+
from threading import Timer
6+
from typing import Optional, Sequence
7+
8+
from typing_extensions import override
9+
10+
from amazon.opentelemetry.distro.sampler._aws_xray_sampling_client import _AwsXRaySamplingClient
11+
from opentelemetry.context import Context
12+
from opentelemetry.sdk.resources import Resource
13+
from opentelemetry.sdk.trace.sampling import ALWAYS_OFF, Sampler, SamplingResult
14+
from opentelemetry.trace import Link, SpanKind
15+
from opentelemetry.trace.span import TraceState
16+
from opentelemetry.util.types import Attributes
17+
18+
_logger = getLogger(__name__)
19+
20+
DEFAULT_RULES_POLLING_INTERVAL_SECONDS = 300
21+
DEFAULT_TARGET_POLLING_INTERVAL_SECONDS = 10
22+
DEFAULT_SAMPLING_PROXY_ENDPOINT = "http://127.0.0.1:2000"
23+
24+
25+
class AwsXRayRemoteSampler(Sampler):
26+
"""
27+
Remote Sampler for OpenTelemetry that gets sampling configurations from AWS X-Ray
28+
29+
Args:
30+
resource: OpenTelemetry Resource (Required)
31+
endpoint: proxy endpoint for AWS X-Ray Sampling (Optional)
32+
polling_interval: Polling interval for getSamplingRules call (Optional)
33+
log_level: custom log level configuration for remote sampler (Optional)
34+
"""
35+
36+
__resource: Resource
37+
__polling_interval: int
38+
__xray_client: _AwsXRaySamplingClient
39+
40+
def __init__(
41+
self,
42+
resource: Resource,
43+
endpoint=DEFAULT_SAMPLING_PROXY_ENDPOINT,
44+
polling_interval=DEFAULT_RULES_POLLING_INTERVAL_SECONDS,
45+
log_level=None,
46+
):
47+
# Override default log level
48+
if log_level is not None:
49+
_logger.setLevel(log_level)
50+
51+
self.__xray_client = _AwsXRaySamplingClient(endpoint, log_level=log_level)
52+
self.__polling_interval = polling_interval
53+
54+
# pylint: disable=unused-private-member
55+
if resource is not None:
56+
self.__resource = resource
57+
else:
58+
_logger.warning("OTel Resource provided is `None`. Defaulting to empty resource")
59+
self.__resource = Resource.get_empty()
60+
61+
# Schedule the next rule poll now
62+
# Python Timers only run once, so they need to be recreated for every poll
63+
self._timer = Timer(0, self.__start_sampling_rule_poller)
64+
self._timer.daemon = True # Ensures that when the main thread exits, the Timer threads are killed
65+
self._timer.start()
66+
67+
# pylint: disable=no-self-use
68+
@override
69+
def should_sample(
70+
self,
71+
parent_context: Optional["Context"],
72+
trace_id: int,
73+
name: str,
74+
kind: SpanKind = None,
75+
attributes: Attributes = None,
76+
links: Sequence["Link"] = None,
77+
trace_state: "TraceState" = None,
78+
) -> SamplingResult:
79+
# TODO: add sampling functionality
80+
return ALWAYS_OFF.should_sample(
81+
parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state
82+
)
83+
84+
# pylint: disable=no-self-use
85+
@override
86+
def get_description(self) -> str:
87+
description = "AwsXRayRemoteSampler{remote sampling with AWS X-Ray}"
88+
return description
89+
90+
def __get_and_update_sampling_rules(self):
91+
sampling_rules = self.__xray_client.get_sampling_rules()
92+
93+
# TODO: Update sampling rules cache
94+
_logger.info("Got Sampling Rules: %s", {json.dumps([ob.__dict__ for ob in sampling_rules])})
95+
96+
def __start_sampling_rule_poller(self):
97+
self.__get_and_update_sampling_rules()
98+
# Schedule the next sampling rule poll
99+
self._timer = Timer(self.__polling_interval, self.__start_sampling_rule_poller)
100+
self._timer.daemon = True
101+
self._timer.start()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"NextToken": null,
3+
"SamplingRuleRecords": [
4+
{
5+
"CreatedAt": 1.67799933E9,
6+
"ModifiedAt": 1.67799933E9,
7+
"SamplingRule": {
8+
"Attributes": {
9+
"foo": "bar",
10+
"doo": "baz"
11+
},
12+
"FixedRate": 0.05,
13+
"HTTPMethod": "*",
14+
"Host": "*",
15+
"Priority": 1000,
16+
"ReservoirSize": 10,
17+
"ResourceARN": "*",
18+
"RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule1",
19+
"RuleName": "Rule1",
20+
"ServiceName": "*",
21+
"ServiceType": "AWS::Foo::Bar",
22+
"URLPath": "*",
23+
"Version": 1
24+
}
25+
},
26+
{
27+
"CreatedAt": 0.0,
28+
"ModifiedAt": 1.611564245E9,
29+
"SamplingRule": {
30+
"Attributes": {},
31+
"FixedRate": 0.05,
32+
"HTTPMethod": "*",
33+
"Host": "*",
34+
"Priority": 10000,
35+
"ReservoirSize": 1,
36+
"ResourceARN": "*",
37+
"RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Default",
38+
"RuleName": "Default",
39+
"ServiceName": "*",
40+
"ServiceType": "*",
41+
"URLPath": "*",
42+
"Version": 1
43+
}
44+
},
45+
{
46+
"CreatedAt": 1.676038494E9,
47+
"ModifiedAt": 1.676038494E9,
48+
"SamplingRule": {
49+
"Attributes": {},
50+
"FixedRate": 0.2,
51+
"HTTPMethod": "GET",
52+
"Host": "*",
53+
"Priority": 1,
54+
"ReservoirSize": 10,
55+
"ResourceARN": "*",
56+
"RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule2",
57+
"RuleName": "Rule2",
58+
"ServiceName": "FooBar",
59+
"ServiceType": "*",
60+
"URLPath": "/foo/bar",
61+
"Version": 1
62+
}
63+
}
64+
]
65+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from logging import DEBUG
4+
from unittest import TestCase
5+
6+
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
7+
from opentelemetry.sdk.resources import Resource
8+
9+
10+
class TestAwsXRayRemoteSampler(TestCase):
11+
def test_create_remote_sampler_with_empty_resource(self):
12+
rs = AwsXRayRemoteSampler(resource=Resource.get_empty())
13+
self.assertIsNotNone(rs._timer)
14+
self.assertEqual(rs._AwsXRayRemoteSampler__polling_interval, 300)
15+
self.assertIsNotNone(rs._AwsXRayRemoteSampler__xray_client)
16+
self.assertIsNotNone(rs._AwsXRayRemoteSampler__resource)
17+
18+
def test_create_remote_sampler_with_populated_resource(self):
19+
rs = AwsXRayRemoteSampler(
20+
resource=Resource.create({"service.name": "test-service-name", "cloud.platform": "test-cloud-platform"})
21+
)
22+
self.assertIsNotNone(rs._timer)
23+
self.assertEqual(rs._AwsXRayRemoteSampler__polling_interval, 300)
24+
self.assertIsNotNone(rs._AwsXRayRemoteSampler__xray_client)
25+
self.assertIsNotNone(rs._AwsXRayRemoteSampler__resource)
26+
self.assertEqual(rs._AwsXRayRemoteSampler__resource.attributes["service.name"], "test-service-name")
27+
self.assertEqual(rs._AwsXRayRemoteSampler__resource.attributes["cloud.platform"], "test-cloud-platform")
28+
29+
def test_create_remote_sampler_with_all_fields_populated(self):
30+
rs = AwsXRayRemoteSampler(
31+
resource=Resource.create({"service.name": "test-service-name", "cloud.platform": "test-cloud-platform"}),
32+
endpoint="http://abc.com",
33+
polling_interval=120,
34+
log_level=DEBUG,
35+
)
36+
self.assertIsNotNone(rs._timer)
37+
self.assertEqual(rs._AwsXRayRemoteSampler__polling_interval, 120)
38+
self.assertIsNotNone(rs._AwsXRayRemoteSampler__xray_client)
39+
self.assertIsNotNone(rs._AwsXRayRemoteSampler__resource)
40+
self.assertEqual(
41+
rs._AwsXRayRemoteSampler__xray_client._AwsXRaySamplingClient__get_sampling_rules_endpoint,
42+
"http://abc.com/GetSamplingRules",
43+
)
44+
self.assertEqual(rs._AwsXRayRemoteSampler__resource.attributes["service.name"], "test-service-name")
45+
self.assertEqual(rs._AwsXRayRemoteSampler__resource.attributes["cloud.platform"], "test-cloud-platform")

0 commit comments

Comments
 (0)