Skip to content

Commit f943b07

Browse files
committed
made botocore an optional dependency if not using otlp cw endpoint
1 parent ad4c0a0 commit f943b07

File tree

4 files changed

+85
-56
lines changed

4 files changed

+85
-56
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import re
5+
6+
7+
def is_otlp_endpoint_cloudwatch(otlp_endpoint=None):
8+
# Detects if it's the OTLP endpoint in CloudWatchs
9+
if not otlp_endpoint:
10+
return False
11+
12+
pattern = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$"
13+
14+
return bool(re.match(pattern, otlp_endpoint.lower()))

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE
1313
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
14+
from amazon.opentelemetry.distro._utils import is_otlp_endpoint_cloudwatch
1415
from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler
1516
from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import (
1617
AttributePropagatingSpanProcessorBuilder,
@@ -317,7 +318,7 @@ def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> Span
317318
traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000")
318319
span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint)
319320

320-
if _is_otlp_endpoint_cloudwatch():
321+
if is_otlp_endpoint_cloudwatch(os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)):
321322
span_exporter = OTLPAwsSigV4Exporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT))
322323

323324
if not _is_application_signals_enabled():
@@ -334,7 +335,7 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
334335
provider.add_span_processor(AttributePropagatingSpanProcessorBuilder().build())
335336

336337
# Do not export metrics if it's CloudWatch OTLP endpoint
337-
if _is_otlp_endpoint_cloudwatch():
338+
if is_otlp_endpoint_cloudwatch():
338339
return
339340

340341
# Export 100% spans and not export Application-Signals metrics if on Lambda.
@@ -446,16 +447,6 @@ def _is_lambda_environment():
446447
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
447448

448449

449-
def _is_otlp_endpoint_cloudwatch():
450-
# Detects if it's the OTLP endpoint in CloudWatchs
451-
otlp_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
452-
if not otlp_endpoint:
453-
return False
454-
pattern = r"xray\.([a-z0-9-]+)\.amazonaws\.com"
455-
456-
return bool(re.match(pattern, otlp_endpoint.lower()))
457-
458-
459450
def _get_metric_export_interval():
460451
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
461452
_logger.debug("Span Metrics export interval: %s", export_interval_millis)

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@
55
from typing import Dict, Optional
66

77
import requests
8-
from botocore import session
9-
from botocore.auth import NoCredentialsError, SigV4Auth
10-
from botocore.awsrequest import AWSRequest
118
from grpc import Compression
129

10+
from amazon.opentelemetry.distro._utils import is_otlp_endpoint_cloudwatch
1311
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
1412

1513
AWS_SERVICE = "xray"
16-
1714
_logger = logging.getLogger(__name__)
1815

1916

@@ -31,7 +28,33 @@ def __init__(
3128
rsession: Optional[requests.Session] = None,
3229
):
3330

34-
self._aws_region = self._validate_exporter_endpoint(endpoint)
31+
self._aws_region = None
32+
33+
if endpoint and is_otlp_endpoint_cloudwatch(endpoint):
34+
try:
35+
from botocore import auth, awsrequest, session
36+
37+
self.boto_auth = auth
38+
self.boto_aws_request = awsrequest
39+
self.boto_session = session.Session()
40+
41+
self._aws_region = self._validate_exporter_endpoint(endpoint)
42+
43+
except ImportError:
44+
_logger.error(
45+
"botocore is required to export traces to %s. " "Please install it using `pip install botocore`",
46+
endpoint,
47+
)
48+
49+
else:
50+
_logger.error(
51+
"Invalid XRay traces endpoint: %s. Resolving to OTLPSpanExporter to handle exporting. "
52+
"The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. "
53+
"For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be "
54+
"https://xray.us-west-2.amazonaws.com/v1/traces.",
55+
endpoint,
56+
)
57+
3558
super().__init__(
3659
endpoint=endpoint,
3760
certificate_file=certificate_file,
@@ -45,54 +68,43 @@ def __init__(
4568

4669
def _export(self, serialized_data: bytes):
4770
if self._aws_region:
48-
request = AWSRequest(
71+
request = self.boto_aws_request.AWSRequest(
4972
method="POST",
5073
url=self._endpoint,
5174
data=serialized_data,
5275
headers={"Content-Type": "application/x-protobuf"},
5376
)
5477

55-
botocore_session = session.Session()
56-
credentials = botocore_session.get_credentials()
78+
credentials = self.boto_session.get_credentials()
79+
5780
if credentials is not None:
58-
signer = SigV4Auth(credentials, AWS_SERVICE, self._aws_region)
81+
signer = self.boto_auth.SigV4Auth(credentials, AWS_SERVICE, self._aws_region)
5982

6083
try:
6184
signer.add_auth(request)
6285
self._session.headers.update(dict(request.headers))
6386

64-
except NoCredentialsError as signing_error:
87+
except self.boto_auth.NoCredentialsError as signing_error:
6588
_logger.error("Failed to sign request: %s", signing_error)
6689

6790
else:
6891
_logger.error("Failed to get credentials to export span to OTLP CloudWatch endpoint")
6992

7093
return super()._export(serialized_data)
7194

72-
@staticmethod
73-
def _validate_exporter_endpoint(endpoint: str) -> Optional[str]:
95+
def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]:
7496
if not endpoint:
7597
return None
7698

77-
match = re.search(rf"{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com", endpoint)
99+
region = endpoint.split(".")[1]
100+
xray_regions = self.boto_session.get_available_regions(AWS_SERVICE)
78101

79-
if match:
80-
region = match.group(1)
81-
xray_regions = session.Session().get_available_regions(AWS_SERVICE)
82-
if region in xray_regions:
83-
return region
102+
if region not in xray_regions:
84103

85104
_logger.error(
86105
"Invalid AWS region: %s. Valid regions are %s. Resolving to default endpoint.", region, xray_regions
87106
)
88-
return None
89107

90-
_logger.error(
91-
"Invalid XRay traces endpoint: %s. Resolving to default endpoint. "
92-
"The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. "
93-
"For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be "
94-
"https://xray.us-west-2.amazonaws.com/v1/traces.",
95-
endpoint,
96-
)
108+
return None
97109

98-
return None
110+
return region

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
DEFAULT_ENDPOINT,
1414
DEFAULT_TIMEOUT,
1515
DEFAULT_TRACES_EXPORT_PATH,
16+
OTLPSpanExporter,
1617
)
1718
from opentelemetry.exporter.otlp.proto.http.version import __version__
1819
from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
@@ -42,24 +43,29 @@ def setUp(self):
4243
"https://xray.us-east-1.amaz.com/v1/traces",
4344
"https://logs.us-east-1.amazonaws.com/v1/logs",
4445
"https://test-endpoint123.com/test",
46+
"xray.us-east-1.amazonaws.com/v1/traces",
47+
"https://test-endpoint123.com/test https://xray.us-east-1.amazonaws.com/v1/traces",
48+
"https://xray.us-east-1.amazonaws.com/v1/tracesssda",
4549
]
4650

4751
self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request"
4852
self.expected_auth_x_amz_date = "some_date"
4953
self.expected_auth_security_token = "test_token"
5054

51-
# Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set
5255
@patch.dict(os.environ, {}, clear=True)
5356
def test_sigv4_exporter_init_default(self):
57+
"""Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set"""
58+
5459
exporter = OTLPAwsSigV4Exporter()
5560
self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH)
5661
self.assertIsInstance(exporter._session, requests.Session)
5762

58-
# Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http
59-
# Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set
6063
@patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}, clear=True)
6164
@patch("botocore.session.Session")
6265
def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock):
66+
"""Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http
67+
Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set."""
68+
6369
mock_session = MagicMock()
6470
session_mock.return_value = mock_session
6571

@@ -71,12 +77,18 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock):
7177

7278
mock_session.get_available_regions.assert_called_once_with("xray")
7379

74-
# Tests that the exporter constructor behavior
75-
# is set by OTLP protobuf/http Span Exporter
76-
# if an invalid OTLP CloudWatch endpoint is set
80+
@patch.dict("sys.modules", {"botocore": None})
81+
def test_no_botocore_valid_xray_endpoint(self):
82+
"""Test that exporter defaults when using OTLP CW endpoint without botocore"""
83+
84+
exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT)
85+
86+
self.assertIsNone(exporter._aws_region)
87+
7788
@patch("botocore.session.Session")
7889
def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock):
79-
90+
"""Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter
91+
if an invalid OTLP CloudWatch endpoint is set"""
8092
for bad_endpoint in self.invalid_cw_otlp_tracing_endpoints:
8193
with self.subTest(endpoint=bad_endpoint):
8294
with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}):
@@ -90,15 +102,15 @@ def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock):
90102

91103
self.assertIsNone(exporter._aws_region)
92104

93-
# Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid,
94-
# SigV4 authentication method is NOT called and is
95-
# NOT injected into the existing Session headers.
96105
@patch("botocore.session.Session.get_available_regions")
97106
@patch("requests.Session.post")
98107
@patch("botocore.auth.SigV4Auth.add_auth")
99108
def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(
100109
self, mock_sigv4_auth, requests_mock, botocore_mock
101110
):
111+
"""Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid,
112+
SigV4 authentication method is NOT called and is NOT injected into the existing Session headers."""
113+
102114
# Setting the exporter response
103115
mock_response = MagicMock()
104116
mock_response.status_code = 200
@@ -155,17 +167,16 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(
155167
cert=ANY,
156168
)
157169

158-
# Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned,
159-
# SigV4 authentication method is NOT called and is NOT
160-
# injected into the existing Session headers.
161170
@patch("botocore.session.Session")
162171
@patch("requests.Session")
163172
@patch("botocore.auth.SigV4Auth.add_auth")
164173
@patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT})
165174
def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials(
166175
self, mock_sigv4_auth, requests_posts_mock, botocore_mock
167176
):
168-
177+
"""Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned,
178+
SigV4 authentication method is NOT called and is NOT injected into the existing
179+
Session headers."""
169180
# Setting the exporter response
170181
mock_response = MagicMock()
171182
mock_response.status_code = 200
@@ -201,16 +212,16 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials(
201212
self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers)
202213
self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers)
203214

204-
# Tests that if the OTLP endpoint is valid and credentials are valid,
205-
# SigV4 authentication method is called and is
206-
# injected into the existing Session headers.
207215
@patch("botocore.session.Session")
208216
@patch("requests.Session")
209217
@patch("botocore.auth.SigV4Auth.add_auth")
210218
@patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT})
211219
def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint(
212220
self, mock_sigv4_auth, requests_posts_mock, botocore_mock
213221
):
222+
"""Tests that if the OTLP endpoint is valid and credentials are valid,
223+
SigV4 authentication method is called and is
224+
injected into the existing Session headers."""
214225

215226
# Setting the exporter response
216227
mock_response = MagicMock()
@@ -251,6 +262,7 @@ def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint(
251262
self.assertEqual(actual_headers[X_AMZ_SECURITY_TOKEN_HEADER], self.expected_auth_security_token)
252263

253264
def validate_exporter_extends_http_span_exporter(self, exporter, endpoint):
265+
self.assertIsInstance(exporter, OTLPSpanExporter)
254266
self.assertEqual(exporter._endpoint, endpoint)
255267
self.assertEqual(exporter._certificate_file, True)
256268
self.assertEqual(exporter._client_certificate_file, None)

0 commit comments

Comments
 (0)