Skip to content

Commit 1093bd5

Browse files
committed
Add Rules Caching, Rules Matching Logic, Rate Limiter, and Sampling Targets Poller
1 parent 843f345 commit 1093bd5

File tree

17 files changed

+2493
-54
lines changed

17 files changed

+2493
-54
lines changed

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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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"]
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):
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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.resource import CloudPlatformValues
24+
from opentelemetry.util.types import Attributes, AttributeValue
25+
26+
cloud_platform_mapping = {
27+
CloudPlatformValues.AWS_LAMBDA.value: "AWS::Lambda::Function",
28+
CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value: "AWS::ElasticBeanstalk::Environment",
29+
CloudPlatformValues.AWS_EC2.value: "AWS::EC2::Instance",
30+
CloudPlatformValues.AWS_ECS.value: "AWS::ECS::Container",
31+
CloudPlatformValues.AWS_EKS.value: "AWS::EKS::Container",
32+
}
33+
34+
35+
class _Matcher:
36+
@staticmethod
37+
def wild_card_match(
38+
text: AttributeValue | None = None, pattern: str | None = None
39+
) -> bool:
40+
if pattern == "*":
41+
return True
42+
if not isinstance(text, str) or pattern is None:
43+
return False
44+
if len(pattern) == 0:
45+
return len(text) == 0
46+
for char in pattern:
47+
if char in ("*", "?"):
48+
return (
49+
re.fullmatch(_Matcher.to_regex_pattern(pattern), text)
50+
is not None
51+
)
52+
return pattern == text
53+
54+
@staticmethod
55+
def to_regex_pattern(rule_pattern: str) -> str:
56+
token_start = -1
57+
regex_pattern = ""
58+
for index, char in enumerate(rule_pattern):
59+
char = rule_pattern[index]
60+
if char in ("*", "?"):
61+
if token_start != -1:
62+
regex_pattern += re.escape(rule_pattern[token_start:index])
63+
token_start = -1
64+
if char == "*":
65+
regex_pattern += ".*"
66+
else:
67+
regex_pattern += "."
68+
else:
69+
if token_start == -1:
70+
token_start = index
71+
if token_start != -1:
72+
regex_pattern += re.escape(rule_pattern[token_start:])
73+
return regex_pattern
74+
75+
@staticmethod
76+
def attribute_match(
77+
attributes: Attributes | None = None,
78+
rule_attributes: dict[str, str] | None = None,
79+
) -> bool:
80+
if rule_attributes is None or len(rule_attributes) == 0:
81+
return True
82+
if (
83+
attributes is None
84+
or len(attributes) == 0
85+
or len(rule_attributes) > len(attributes)
86+
):
87+
return False
88+
89+
matched_count = 0
90+
for key, val in attributes.items():
91+
text_to_match = val
92+
pattern = rule_attributes.get(key, None)
93+
if pattern is None:
94+
continue
95+
if _Matcher.wild_card_match(text_to_match, pattern):
96+
matched_count += 1
97+
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:
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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_limiter import (
27+
_RateLimiter,
28+
)
29+
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult
30+
from opentelemetry.trace import Link, SpanKind
31+
from opentelemetry.trace.span import TraceState
32+
from opentelemetry.util.types import Attributes
33+
34+
35+
class _RateLimitingSampler(Sampler):
36+
def __init__(self, quota: int, clock: _Clock):
37+
self.__quota = quota
38+
self.__reservoir = _RateLimiter(1, quota, clock)
39+
40+
def should_sample(
41+
self,
42+
parent_context: Context | None,
43+
trace_id: int,
44+
name: str,
45+
kind: SpanKind | None = None,
46+
attributes: Attributes | None = None,
47+
links: Sequence["Link"] | None = None,
48+
trace_state: TraceState | None = None,
49+
) -> "SamplingResult":
50+
if self.__reservoir.try_spend(1):
51+
return SamplingResult(
52+
decision=Decision.RECORD_AND_SAMPLE,
53+
attributes=attributes,
54+
trace_state=trace_state,
55+
)
56+
return SamplingResult(
57+
decision=Decision.DROP,
58+
attributes=attributes,
59+
trace_state=trace_state,
60+
)
61+
62+
def get_description(self) -> str:
63+
description = f"RateLimitingSampler{{rate limiting sampling with sampling config of {self.__quota} req/sec and 0% of additional requests}}"
64+
return description

0 commit comments

Comments
 (0)