From b629da39b42a4aa8f442a71c364e7ca39b4bf93d Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 5 Feb 2025 23:09:16 +0000 Subject: [PATCH 01/39] added sigv4 authentication to otlp exporter --- .../distro/aws_opentelemetry_configurator.py | 10 +- .../distro/otlp_sigv4_exporter.py | 100 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index c9d3680a5..b5444ae0e 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -1,6 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. +import logging import os from logging import Logger, getLogger from typing import ClassVar, Dict, List, Type, Union @@ -8,6 +9,7 @@ from importlib_metadata import version from typing_extensions import override +from amazon.opentelemetry.distro.otlp_sigv4_exporter import OTLPAwsSigV4Exporter from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler @@ -85,6 +87,7 @@ LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10 _logger: Logger = getLogger(__name__) +_logger.setLevel(logging.DEBUG) class AwsOpenTelemetryConfigurator(_OTelSDKConfigurator): @@ -309,11 +312,16 @@ def _customize_sampler(sampler: Sampler) -> Sampler: def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter: + traces_endpoint = os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + if _is_lambda_environment(): # Override OTLP http default endpoint to UDP - if isinstance(span_exporter, OTLPSpanExporter) and os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) is None: + if isinstance(span_exporter, OTLPSpanExporter) and traces_endpoint is None: traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000") span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint) + + if traces_endpoint and 'xray.' in traces_endpoint and '.amazonaws.com' in traces_endpoint: + span_exporter = OTLPAwsSigV4Exporter(endpoint=traces_endpoint) if not _is_application_signals_enabled(): return span_exporter diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py new file mode 100644 index 000000000..d4b840b63 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -0,0 +1,100 @@ +import logging +import re +from typing import Dict, Optional +from grpc import Compression +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace.export import SpanExportResult +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest +from botocore import session +import requests + +AWS_SERVICE = 'xray' + +_logger = logging.getLogger(__name__) + +class OTLPAwsSigV4Exporter(OTLPSpanExporter): + + def __init__( + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + client_key_file: Optional[str] = None, + client_certificate_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + session: Optional[requests.Session] = None + ): + + self._aws_region = self._validate_exporter_endpoint(endpoint) + + if self._aws_region is None: + endpoint = None + + super().__init__(endpoint=endpoint, + certificate_file=certificate_file, + client_key_file=client_key_file, + client_certificate_file=client_certificate_file, + headers=headers, + timeout=timeout, + compression=compression, + session=session) + + def _export(self, serialized_data: bytes): + if self._aws_region: + request = AWSRequest( + method='POST', + url=self._endpoint, + data=serialized_data, + headers={"Content-Type": "application/x-protobuf"} + ) + + botocore_session = session.Session() + credentials = botocore_session.get_credentials() + + if credentials is not None: + signer = SigV4Auth(credentials, AWS_SERVICE, self._aws_region) + + try: + signer.add_auth(request) + self._session.headers.update(dict(request.headers)) + + except Exception as signing_error: + _logger.error(f"Failed to sign request: {signing_error}") + + else: + _logger.error(f"Failed to get credentials for signing request") + + return super()._export(serialized_data) + + def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: + if not endpoint: + return None + + match = re.search(f'{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com', endpoint) + + if match: + region = match.group(1) + xray_regions = session.Session().get_available_regions(AWS_SERVICE) + + if region in xray_regions: + return region + + _logger.error(f"Invalid AWS region: {region}. Valid regions are {xray_regions}. Resolving to default endpoint.") + + return None + + else: + _logger.error(f"Invalid XRay traces endpoint: {endpoint}. Resolving to default endpoint. " + "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " + "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " + "https://xray.us-west-2.amazonaws.com/v1/traces.") + + + return None + + + + + From bd4e1d61d3ff80ab40fb210528d130916047354b Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 6 Feb 2025 22:43:02 +0000 Subject: [PATCH 02/39] added unit tests --- .../distro/aws_opentelemetry_configurator.py | 19 ++-- .../distro/otlp_sigv4_exporter.py | 14 +-- .../distro/test_otlp_sigv4_exporter.py | 97 +++++++++++++++++++ 3 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index b5444ae0e..a4ce65db9 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -311,17 +311,15 @@ def _customize_sampler(sampler: Sampler) -> Sampler: return AlwaysRecordSampler(sampler) -def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter: - traces_endpoint = os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) - +def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter: if _is_lambda_environment(): # Override OTLP http default endpoint to UDP - if isinstance(span_exporter, OTLPSpanExporter) and traces_endpoint is None: + if isinstance(span_exporter, OTLPSpanExporter) and os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) is None: traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000") span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint) - if traces_endpoint and 'xray.' in traces_endpoint and '.amazonaws.com' in traces_endpoint: - span_exporter = OTLPAwsSigV4Exporter(endpoint=traces_endpoint) + if _is_otlp_endpoint_cloudwatch(): + span_exporter = OTLPAwsSigV4Exporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) if not _is_application_signals_enabled(): return span_exporter @@ -336,11 +334,15 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) -> # Construct and set local and remote attributes span processor provider.add_span_processor(AttributePropagatingSpanProcessorBuilder().build()) + # Do not export metrics if it's CloudWatch OTLP endpoint + if _is_otlp_endpoint_cloudwatch(): + return + # Export 100% spans and not export Application-Signals metrics if on Lambda. if _is_lambda_environment(): _export_unsampled_span_for_lambda(provider, resource) return - + # Construct meterProvider _logger.info("AWS Application Signals enabled") otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter() @@ -444,6 +446,9 @@ def _is_lambda_environment(): # detect if running in AWS Lambda environment return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ +def _is_otlp_endpoint_cloudwatch(): + otlp_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + return otlp_endpoint and "xray." in otlp_endpoint.lower() and ".amazonaws.com" in otlp_endpoint.lower() def _get_metric_export_interval(): export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL)) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index d4b840b63..8797b666d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -3,7 +3,6 @@ from typing import Dict, Optional from grpc import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.trace.export import SpanExportResult from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest from botocore import session @@ -28,10 +27,6 @@ def __init__( ): self._aws_region = self._validate_exporter_endpoint(endpoint) - - if self._aws_region is None: - endpoint = None - super().__init__(endpoint=endpoint, certificate_file=certificate_file, client_key_file=client_key_file, @@ -52,7 +47,6 @@ def _export(self, serialized_data: bytes): botocore_session = session.Session() credentials = botocore_session.get_credentials() - if credentials is not None: signer = SigV4Auth(credentials, AWS_SERVICE, self._aws_region) @@ -80,18 +74,16 @@ def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: if region in xray_regions: return region - - _logger.error(f"Invalid AWS region: {region}. Valid regions are {xray_regions}. Resolving to default endpoint.") + _logger.error(f"Invalid AWS region: {region}. Valid regions are {xray_regions}.") return None else: - _logger.error(f"Invalid XRay traces endpoint: {endpoint}. Resolving to default endpoint. " + _logger.error(f"Invalid XRay traces endpoint: {endpoint}." "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " "https://xray.us-west-2.amazonaws.com/v1/traces.") - - + return None diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py new file mode 100644 index 000000000..fa633241c --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -0,0 +1,97 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import os +import requests +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSigV4Exporter +from grpc import Compression +from opentelemetry.exporter.otlp.proto.http.version import __version__ +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + DEFAULT_ENDPOINT, + DEFAULT_TRACES_EXPORT_PATH, + DEFAULT_TIMEOUT, + DEFAULT_COMPRESSION, +) + +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, +) + +OTLP_CW_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" + +class TestAwsSigV4Exporter(TestCase): + + @patch.dict(os.environ, {}, clear=True) + def test_sigv4_exporter_init_default(self): + exporter = OTLPAwsSigV4Exporter() + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._client_certificate_file, None) + self.assertEqual(exporter._client_key_file, None) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) + self.assertIn("User-Agent", exporter._session.headers) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/x-protobuf", + ) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "OTel-OTLP-Exporter-Python/" + __version__, + ) + + @patch.dict(os.environ, { + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT + }, clear=True) + @patch('botocore.session.Session') + def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): + mock_session = MagicMock() + session_mock.return_value = mock_session + + mock_session.get_available_regions.return_value = ['us-east-1', 'us-west-2'] + exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + + self.assertEqual( + exporter._endpoint, OTLP_CW_ENDPOINT + ) + self.assertEqual( + exporter._aws_region, "us-east-1" + ) + + mock_session.get_available_regions.assert_called_once_with('xray') + + @patch('botocore.session.Session') + def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, session_mock): + invalid_otlp_endpoints = [ + "https://xray.bad-region-1.amazonaws.com/v1/traces", + "https://xray.us-east-1.amaz.com/v1/traces" + "https://logs.us-east-1.amazonaws.com/v1/logs" + ] + + for bad_endpoint in invalid_otlp_endpoints: + with self.subTest(endpoint=bad_endpoint): + with patch.dict(os.environ, { + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint + }): + + mock_session = MagicMock() + session_mock.return_value = mock_session + + mock_session.get_available_regions.return_value = ['us-east-1', 'us-west-2'] + exporter = OTLPAwsSigV4Exporter(endpoint=bad_endpoint) + + self.assertIsNone(exporter._aws_region) + + @patch.dict(os.environ, { + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT + }, clear=True) + def test_sigv4_exporter_export_valid_otlp_endpoint(self): + exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + + From 7187839d26bd0df035f896c22768c8ec3c319ab2 Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 6 Feb 2025 22:44:20 +0000 Subject: [PATCH 03/39] removed logging --- .../opentelemetry/distro/aws_opentelemetry_configurator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index a4ce65db9..d4834d428 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -1,7 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. -import logging import os from logging import Logger, getLogger from typing import ClassVar, Dict, List, Type, Union @@ -87,8 +86,6 @@ LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10 _logger: Logger = getLogger(__name__) -_logger.setLevel(logging.DEBUG) - class AwsOpenTelemetryConfigurator(_OTelSDKConfigurator): """ From 6422607deb67562daf3fede81e19b6a7ad583c0f Mon Sep 17 00:00:00 2001 From: liustve Date: Fri, 7 Feb 2025 23:04:02 +0000 Subject: [PATCH 04/39] more testing --- .../distro/aws_opentelemetry_configurator.py | 13 +- .../distro/otlp_sigv4_exporter.py | 102 +++--- .../distro/test_otlp_sigv4_exporter.py | 306 ++++++++++++++---- 3 files changed, 307 insertions(+), 114 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index d4834d428..ff44a422c 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -8,7 +8,6 @@ from importlib_metadata import version from typing_extensions import override -from amazon.opentelemetry.distro.otlp_sigv4_exporter import OTLPAwsSigV4Exporter from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler @@ -20,6 +19,7 @@ AwsMetricAttributesSpanExporterBuilder, ) from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder +from amazon.opentelemetry.distro.otlp_sigv4_exporter import OTLPAwsSigV4Exporter from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader @@ -87,6 +87,7 @@ _logger: Logger = getLogger(__name__) + class AwsOpenTelemetryConfigurator(_OTelSDKConfigurator): """ This AwsOpenTelemetryConfigurator extend _OTelSDKConfigurator configuration with the following change: @@ -308,13 +309,13 @@ def _customize_sampler(sampler: Sampler) -> Sampler: return AlwaysRecordSampler(sampler) -def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter: +def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter: if _is_lambda_environment(): # Override OTLP http default endpoint to UDP if isinstance(span_exporter, OTLPSpanExporter) and os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) is None: traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000") span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint) - + if _is_otlp_endpoint_cloudwatch(): span_exporter = OTLPAwsSigV4Exporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) @@ -334,12 +335,12 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) -> # Do not export metrics if it's CloudWatch OTLP endpoint if _is_otlp_endpoint_cloudwatch(): return - + # Export 100% spans and not export Application-Signals metrics if on Lambda. if _is_lambda_environment(): _export_unsampled_span_for_lambda(provider, resource) return - + # Construct meterProvider _logger.info("AWS Application Signals enabled") otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter() @@ -443,10 +444,12 @@ def _is_lambda_environment(): # detect if running in AWS Lambda environment return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ + def _is_otlp_endpoint_cloudwatch(): otlp_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) return otlp_endpoint and "xray." in otlp_endpoint.lower() and ".amazonaws.com" in otlp_endpoint.lower() + def _get_metric_export_interval(): export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL)) _logger.debug("Span Metrics export interval: %s", export_interval_millis) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index 8797b666d..332e16011 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -1,73 +1,78 @@ import logging import re from typing import Dict, Optional -from grpc import Compression -from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + +import requests +from botocore import session from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest -from botocore import session -import requests +from grpc import Compression + +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter -AWS_SERVICE = 'xray' +AWS_SERVICE = "xray" _logger = logging.getLogger(__name__) + class OTLPAwsSigV4Exporter(OTLPSpanExporter): - + def __init__( - self, - endpoint: Optional[str] = None, - certificate_file: Optional[str] = None, - client_key_file: Optional[str] = None, - client_certificate_file: Optional[str] = None, - headers: Optional[Dict[str, str]] = None, - timeout: Optional[int] = None, - compression: Optional[Compression] = None, - session: Optional[requests.Session] = None + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + client_key_file: Optional[str] = None, + client_certificate_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, ): - - self._aws_region = self._validate_exporter_endpoint(endpoint) - super().__init__(endpoint=endpoint, - certificate_file=certificate_file, - client_key_file=client_key_file, - client_certificate_file=client_certificate_file, - headers=headers, - timeout=timeout, - compression=compression, - session=session) - + + self._aws_region = self._validate_exporter_endpoint(endpoint) + super().__init__( + endpoint=endpoint, + certificate_file=certificate_file, + client_key_file=client_key_file, + client_certificate_file=client_certificate_file, + headers=headers, + timeout=timeout, + compression=compression, + session=session, + ) + def _export(self, serialized_data: bytes): if self._aws_region: request = AWSRequest( - method='POST', + method="POST", url=self._endpoint, data=serialized_data, - headers={"Content-Type": "application/x-protobuf"} + headers={"Content-Type": "application/x-protobuf"}, ) - + botocore_session = session.Session() credentials = botocore_session.get_credentials() - if credentials is not None: + if credentials is not None: signer = SigV4Auth(credentials, AWS_SERVICE, self._aws_region) try: signer.add_auth(request) self._session.headers.update(dict(request.headers)) - + except Exception as signing_error: _logger.error(f"Failed to sign request: {signing_error}") - + else: - _logger.error(f"Failed to get credentials for signing request") - + _logger.error("Failed to get credentials to export span to OTLP CloudWatch endpoint") + return super()._export(serialized_data) - + def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: if not endpoint: return None - - match = re.search(f'{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com', endpoint) - + + match = re.search(rf"{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com", endpoint) + if match: region = match.group(1) xray_regions = session.Session().get_available_regions(AWS_SERVICE) @@ -75,18 +80,15 @@ def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: if region in xray_regions: return region _logger.error(f"Invalid AWS region: {region}. Valid regions are {xray_regions}.") - - return None - - else: - _logger.error(f"Invalid XRay traces endpoint: {endpoint}." - "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " - "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " - "https://xray.us-west-2.amazonaws.com/v1/traces.") - - return None - + return None + else: + _logger.error( + f"Invalid XRay traces endpoint: {endpoint}." + "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " + "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " + "https://xray.us-west-2.amazonaws.com/v1/traces." + ) - + return None diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index fa633241c..08b9b8915 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -1,41 +1,258 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import os -import requests +import time +from botocore.credentials import Credentials from unittest import TestCase -from unittest.mock import MagicMock, patch +from unittest.mock import ANY, MagicMock, PropertyMock, patch +from opentelemetry.sdk.trace import Tracer, Resource, SpanLimits, SpanContext +from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.sdk.trace.sampling import ALWAYS_ON + +from opentelemetry.trace import SpanKind + +import requests from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSigV4Exporter -from grpc import Compression -from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, - DEFAULT_TRACES_EXPORT_PATH, DEFAULT_TIMEOUT, - DEFAULT_COMPRESSION, + DEFAULT_TRACES_EXPORT_PATH, ) - +from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, ) +from opentelemetry.sdk.trace import _Span +from opentelemetry.trace import SpanKind, TraceFlags +from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.trace.export import SimpleSpanProcessor + OTLP_CW_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" - + + class TestAwsSigV4Exporter(TestCase): - - @patch.dict(os.environ, {}, clear=True) + def setUp(self): + self.testing_spans = [ + self.create_span("test_span1", SpanKind.INTERNAL), + self.create_span("test_span2", SpanKind.SERVER), + self.create_span("test_span3", SpanKind.CLIENT), + self.create_span("test_span4", SpanKind.PRODUCER), + self.create_span("test_span5", SpanKind.CONSUMER) + ] + + self.invalid_cw_otlp_tracing_endpoints = [ + "https://xray.bad-region-1.amazonaws.com/v1/traces", + "https://xray.us-east-1.amaz.com/v1/traces", + "https://logs.us-east-1.amazonaws.com/v1/logs" + "https://test-endpoint123.com/test" + ] + + self.expected_auth_header = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request' + self.expected_auth_x_amz_date = 'some_date' + self.expected_auth_security_token = 'test_token' + + # Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set + @patch.dict(os.environ, {}, clear=True) def test_sigv4_exporter_init_default(self): exporter = OTLPAwsSigV4Exporter() - self.assertEqual( - exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH + self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH) + self.assertIsInstance(exporter._session, requests.Session) + + # Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http + # Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}, clear=True) + @patch("botocore.session.Session") + def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): + mock_session = MagicMock() + session_mock.return_value = mock_session + + mock_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] + exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + + self.assertEqual(exporter._aws_region, "us-east-1") + self.validate_exporter_extends_http_span_exporter(exporter, OTLP_CW_ENDPOINT) + + mock_session.get_available_regions.assert_called_once_with("xray") + + # Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter + # if an invalid OTLP CloudWatch endpoint is set + @patch("botocore.session.Session") + def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): + + for bad_endpoint in self.invalid_cw_otlp_tracing_endpoints: + with self.subTest(endpoint=bad_endpoint): + with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): + + mock_session = MagicMock() + botocore_mock.return_value = mock_session + + mock_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] + exporter = OTLPAwsSigV4Exporter(endpoint=bad_endpoint) + self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) + + self.assertIsNone(exporter._aws_region) + + + # Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, SigV4 authentication method is NOT called and + # is NOT injected into the existing Session headers. + @patch("botocore.session.Session.get_available_regions") + @patch("requests.Session.post") + @patch("botocore.auth.SigV4Auth.add_auth") + def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, mock_sigv4_auth, requests_mock, botocore_mock): + # Setting the exporter response + mock_response = MagicMock() + mock_response.status_code = 200 + type(mock_response).ok = PropertyMock(return_value=True) + + # Setting the request session headers to make the call to endpoint + mock_session = MagicMock() + mock_session.headers = { + 'User-Agent': 'OTel-OTLP-Exporter-Python/' + __version__, + 'Content-Type': 'application/x-protobuf' + } + requests_mock.return_value = mock_session + mock_session.post.return_value = mock_response + + mock_botocore_session = MagicMock() + botocore_mock.return_value = mock_botocore_session + mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] + mock_botocore_session.get_credentials.return_value = Credentials( + access_key="test_key", + secret_key="test_secret", + token="test_token" ) + + #SigV4 mock authentication injection + mock_sigv4_auth.side_effect = self.mock_add_auth + + # Initialize and call exporter + exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + exporter.export(self.testing_spans) + + # For each invalid CW OTLP endpoint, vdalidate that the sigv4 + for bad_endpoint in self.invalid_cw_otlp_tracing_endpoints: + with self.subTest(endpoint=bad_endpoint): + with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): + botocore_mock.return_value = ["us-east-1", "us-west-2"] + + exporter = OTLPAwsSigV4Exporter(endpoint=bad_endpoint) + + self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) + self.assertIsNone(exporter._aws_region) + + exporter.export(self.testing_spans) + + mock_sigv4_auth.assert_not_called() + + # Verify that SigV4 request headers were not injected + actual_headers = mock_session.headers + self.assertNotIn('Authorization', actual_headers) + self.assertNotIn('X-Amz-Date', actual_headers) + self.assertNotIn('X-Amz-Security-Token', actual_headers) + + requests_mock.assert_called_with( + url=bad_endpoint, + data=ANY, + verify=ANY, + timeout=ANY, + cert=ANY, + ) + + # Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, SigV4 authentication method is NOT called and + # is NOT injected into the existing Session headers. + @patch("botocore.session.Session.get_available_regions") + @patch("requests.Session") + @patch("botocore.auth.SigV4Auth.add_auth") + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}) + def test_sigv4_exporter_export_does_not_add_sigv4_if_no_credentials(self, mock_sigv4, requests_mock, botcore_mock): + mock_response = MagicMock() + mock_response.status_code = 200 + type(mock_response).ok = PropertyMock(return_value=True) + + requests_mock.return_value = mock_response + + botcore_mock.return_value = ["us-east-1", "us-west-2"] + + exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + + self.validate_exporter_extends_http_span_exporter(exporter, OTLP_CW_ENDPOINT) + self.assertIsNone(exporter._aws_region) + + exporter.export(self.testing_spans) + + mock_sigv4.assert_not_called() + + requests_mock.assert_called_with( + url=OTLP_CW_ENDPOINT, + data=ANY, + verify=ANY, + timeout=ANY, + cert=ANY, + ) + + # Tests that if the OTLP endpoint is valid and credentials are valid, SigV4 authentication method is called and + # is injected into the existing Session headers. + @patch("botocore.session.Session") + @patch("requests.Session") + @patch("botocore.auth.SigV4Auth.add_auth") + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}) + def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint(self, mock_sigv4_auth, requests_posts_mock, botocore_mock): + + # Setting the exporter response + mock_response = MagicMock() + mock_response.status_code = 200 + type(mock_response).ok = PropertyMock(return_value=True) + + # Setting the request session headers to make the call to endpoint + mock_session = MagicMock() + mock_session.headers = { + 'User-Agent': 'OTel-OTLP-Exporter-Python/' + __version__, + 'Content-Type': 'application/x-protobuf' + } + requests_posts_mock.return_value = mock_session + mock_session.post.return_value = mock_response + + mock_botocore_session = MagicMock() + botocore_mock.return_value = mock_botocore_session + mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] + mock_botocore_session.get_credentials.return_value = Credentials( + access_key="test_key", + secret_key="test_secret", + token="test_token" + ) + + #SigV4 mock authentication injection + mock_sigv4_auth.side_effect = self.mock_add_auth + + # Initialize and call exporter + exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + exporter.export(self.testing_spans) + + # Verify SigV4 auth was called + mock_sigv4_auth.assert_called_once_with(ANY) + + # Verify that SigV4 request headers were properly injected + actual_headers = mock_session.headers + self.assertIn('Authorization', actual_headers) + self.assertIn('X-Amz-Date', actual_headers) + self.assertIn('X-Amz-Security-Token', actual_headers) + + self.assertEqual(actual_headers['Authorization'], self.expected_auth_header) + self.assertEqual(actual_headers['X-Amz-Date'], self.expected_auth_x_amz_date) + self.assertEqual(actual_headers['X-Amz-Security-Token'], self.expected_auth_security_token) + + + def validate_exporter_extends_http_span_exporter(self, exporter, endpoint): + self.assertEqual(exporter._endpoint, endpoint) self.assertEqual(exporter._certificate_file, True) self.assertEqual(exporter._client_certificate_file, None) self.assertEqual(exporter._client_key_file, None) self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertIs(exporter._compression, DEFAULT_COMPRESSION) self.assertEqual(exporter._headers, {}) - self.assertIsInstance(exporter._session, requests.Session) self.assertIn("User-Agent", exporter._session.headers) self.assertEqual( exporter._session.headers.get("Content-Type"), @@ -46,52 +263,23 @@ def test_sigv4_exporter_init_default(self): "OTel-OTLP-Exporter-Python/" + __version__, ) - @patch.dict(os.environ, { - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT - }, clear=True) - @patch('botocore.session.Session') - def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): - mock_session = MagicMock() - session_mock.return_value = mock_session - - mock_session.get_available_regions.return_value = ['us-east-1', 'us-west-2'] - exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) - self.assertEqual( - exporter._endpoint, OTLP_CW_ENDPOINT - ) - self.assertEqual( - exporter._aws_region, "us-east-1" + def create_span(self, name="test_span", kind=SpanKind.INTERNAL): + span = _Span( + name=name, + context=SpanContext( + trace_id=0x1234567890ABCDEF, + span_id=0x9876543210, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED) + ), + kind=kind ) - - mock_session.get_available_regions.assert_called_once_with('xray') - - @patch('botocore.session.Session') - def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, session_mock): - invalid_otlp_endpoints = [ - "https://xray.bad-region-1.amazonaws.com/v1/traces", - "https://xray.us-east-1.amaz.com/v1/traces" - "https://logs.us-east-1.amazonaws.com/v1/logs" - ] - - for bad_endpoint in invalid_otlp_endpoints: - with self.subTest(endpoint=bad_endpoint): - with patch.dict(os.environ, { - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint - }): - - mock_session = MagicMock() - session_mock.return_value = mock_session - - mock_session.get_available_regions.return_value = ['us-east-1', 'us-west-2'] - exporter = OTLPAwsSigV4Exporter(endpoint=bad_endpoint) - - self.assertIsNone(exporter._aws_region) - - @patch.dict(os.environ, { - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT - }, clear=True) - def test_sigv4_exporter_export_valid_otlp_endpoint(self): - exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) - + return span + def mock_add_auth(self, request): + request.headers._headers.extend([ + ('Authorization', self.expected_auth_header), + ('X-Amz-Date', self.expected_auth_x_amz_date), + ('X-Amz-Security-Token', self.expected_auth_security_token) + ]) \ No newline at end of file From a34e89928828ec923046f958b1e97cfe0022f8b6 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 05:36:47 +0000 Subject: [PATCH 05/39] added extra test --- .../distro/test_otlp_sigv4_exporter.py | 186 +++++++++--------- 1 file changed, 95 insertions(+), 91 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index 08b9b8915..dd79a88e8 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -2,16 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 import os import time -from botocore.credentials import Credentials from unittest import TestCase from unittest.mock import ANY, MagicMock, PropertyMock, patch -from opentelemetry.sdk.trace import Tracer, Resource, SpanLimits, SpanContext -from opentelemetry.sdk.trace.id_generator import RandomIdGenerator -from opentelemetry.sdk.trace.sampling import ALWAYS_ON - -from opentelemetry.trace import SpanKind import requests +from botocore.credentials import Credentials from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSigV4Exporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( @@ -24,13 +19,18 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, ) - -from opentelemetry.sdk.trace import _Span -from opentelemetry.trace import SpanKind, TraceFlags -from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.trace import Resource, SpanContext, SpanLimits, Tracer, _Span from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.sdk.trace.sampling import ALWAYS_ON +from opentelemetry.trace import SpanKind, TraceFlags OTLP_CW_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" +USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ +CONTENT_TYPE = "application/x-protobuf" +AUTHORIZATION_HEADER = "Authorization" +X_AMZ_DATE_HEADER = "X-Amz-Date" +X_AMZ_SECURITY_TOKEN_HEADER = "X-Amz-Security-Token" class TestAwsSigV4Exporter(TestCase): @@ -40,19 +40,18 @@ def setUp(self): self.create_span("test_span2", SpanKind.SERVER), self.create_span("test_span3", SpanKind.CLIENT), self.create_span("test_span4", SpanKind.PRODUCER), - self.create_span("test_span5", SpanKind.CONSUMER) + self.create_span("test_span5", SpanKind.CONSUMER), ] self.invalid_cw_otlp_tracing_endpoints = [ "https://xray.bad-region-1.amazonaws.com/v1/traces", "https://xray.us-east-1.amaz.com/v1/traces", - "https://logs.us-east-1.amazonaws.com/v1/logs" - "https://test-endpoint123.com/test" + "https://logs.us-east-1.amazonaws.com/v1/logs" "https://test-endpoint123.com/test", ] - self.expected_auth_header = 'AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request' - self.expected_auth_x_amz_date = 'some_date' - self.expected_auth_security_token = 'test_token' + self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request" + self.expected_auth_x_amz_date = "some_date" + self.expected_auth_security_token = "test_token" # Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set @patch.dict(os.environ, {}, clear=True) @@ -61,7 +60,7 @@ def test_sigv4_exporter_init_default(self): self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH) self.assertIsInstance(exporter._session, requests.Session) - # Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http + # Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http # Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}, clear=True) @patch("botocore.session.Session") @@ -77,7 +76,7 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): mock_session.get_available_regions.assert_called_once_with("xray") - # Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter + # Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter # if an invalid OTLP CloudWatch endpoint is set @patch("botocore.session.Session") def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): @@ -95,24 +94,22 @@ def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): self.assertIsNone(exporter._aws_region) - # Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, SigV4 authentication method is NOT called and # is NOT injected into the existing Session headers. @patch("botocore.session.Session.get_available_regions") @patch("requests.Session.post") @patch("botocore.auth.SigV4Auth.add_auth") - def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, mock_sigv4_auth, requests_mock, botocore_mock): + def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( + self, mock_sigv4_auth, requests_mock, botocore_mock + ): # Setting the exporter response mock_response = MagicMock() mock_response.status_code = 200 type(mock_response).ok = PropertyMock(return_value=True) - + # Setting the request session headers to make the call to endpoint mock_session = MagicMock() - mock_session.headers = { - 'User-Agent': 'OTel-OTLP-Exporter-Python/' + __version__, - 'Content-Type': 'application/x-protobuf' - } + mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} requests_mock.return_value = mock_session mock_session.post.return_value = mock_response @@ -120,98 +117,110 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, botocore_mock.return_value = mock_botocore_session mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] mock_botocore_session.get_credentials.return_value = Credentials( - access_key="test_key", - secret_key="test_secret", - token="test_token" + access_key="test_key", secret_key="test_secret", token="test_token" ) - #SigV4 mock authentication injection + # SigV4 mock authentication injection mock_sigv4_auth.side_effect = self.mock_add_auth # Initialize and call exporter exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) exporter.export(self.testing_spans) - # For each invalid CW OTLP endpoint, vdalidate that the sigv4 + # For each invalid CW OTLP endpoint, vdalidate that the sigv4 for bad_endpoint in self.invalid_cw_otlp_tracing_endpoints: with self.subTest(endpoint=bad_endpoint): with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): botocore_mock.return_value = ["us-east-1", "us-west-2"] exporter = OTLPAwsSigV4Exporter(endpoint=bad_endpoint) - + self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) + + # Test case, should not have detected a valid region self.assertIsNone(exporter._aws_region) - + exporter.export(self.testing_spans) mock_sigv4_auth.assert_not_called() - + # Verify that SigV4 request headers were not injected actual_headers = mock_session.headers - self.assertNotIn('Authorization', actual_headers) - self.assertNotIn('X-Amz-Date', actual_headers) - self.assertNotIn('X-Amz-Security-Token', actual_headers) - + self.assertNotIn(AUTHORIZATION_HEADER, actual_headers) + self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) + self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) + requests_mock.assert_called_with( - url=bad_endpoint, - data=ANY, - verify=ANY, - timeout=ANY, - cert=ANY, - ) + url=bad_endpoint, + data=ANY, + verify=ANY, + timeout=ANY, + cert=ANY, + ) # Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, SigV4 authentication method is NOT called and # is NOT injected into the existing Session headers. - @patch("botocore.session.Session.get_available_regions") + @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}) - def test_sigv4_exporter_export_does_not_add_sigv4_if_no_credentials(self, mock_sigv4, requests_mock, botcore_mock): + def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( + self, mock_sigv4_auth, requests_posts_mock, botocore_mock + ): + + # Setting the exporter response mock_response = MagicMock() mock_response.status_code = 200 type(mock_response).ok = PropertyMock(return_value=True) - - requests_mock.return_value = mock_response - botcore_mock.return_value = ["us-east-1", "us-west-2"] + # Setting the request session headers to make the call to endpoint + mock_session = MagicMock() + mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} + requests_posts_mock.return_value = mock_session + mock_session.post.return_value = mock_response + + mock_botocore_session = MagicMock() + botocore_mock.return_value = mock_botocore_session + mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] + + # Test case, return None for get credentials + mock_botocore_session.get_credentials.return_value = None + # Initialize and call exporter exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) - - self.validate_exporter_extends_http_span_exporter(exporter, OTLP_CW_ENDPOINT) - self.assertIsNone(exporter._aws_region) - + + # Validate that the region is valid + self.assertEqual(exporter._aws_region, "us-east-1") + exporter.export(self.testing_spans) - mock_sigv4.assert_not_called() + # Verify SigV4 auth was not called + mock_sigv4_auth.assert_not_called() + + # Verify that SigV4 request headers were properly injected + actual_headers = mock_session.headers + self.assertNotIn(AUTHORIZATION_HEADER, actual_headers) + self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) + self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - requests_mock.assert_called_with( - url=OTLP_CW_ENDPOINT, - data=ANY, - verify=ANY, - timeout=ANY, - cert=ANY, - ) - # Tests that if the OTLP endpoint is valid and credentials are valid, SigV4 authentication method is called and # is injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}) - def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint(self, mock_sigv4_auth, requests_posts_mock, botocore_mock): + def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( + self, mock_sigv4_auth, requests_posts_mock, botocore_mock + ): # Setting the exporter response mock_response = MagicMock() mock_response.status_code = 200 type(mock_response).ok = PropertyMock(return_value=True) - + # Setting the request session headers to make the call to endpoint mock_session = MagicMock() - mock_session.headers = { - 'User-Agent': 'OTel-OTLP-Exporter-Python/' + __version__, - 'Content-Type': 'application/x-protobuf' - } + mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} requests_posts_mock.return_value = mock_session mock_session.post.return_value = mock_response @@ -219,12 +228,10 @@ def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint(se botocore_mock.return_value = mock_botocore_session mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] mock_botocore_session.get_credentials.return_value = Credentials( - access_key="test_key", - secret_key="test_secret", - token="test_token" + access_key="test_key", secret_key="test_secret", token="test_token" ) - #SigV4 mock authentication injection + # SigV4 mock authentication injection mock_sigv4_auth.side_effect = self.mock_add_auth # Initialize and call exporter @@ -236,14 +243,13 @@ def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint(se # Verify that SigV4 request headers were properly injected actual_headers = mock_session.headers - self.assertIn('Authorization', actual_headers) - self.assertIn('X-Amz-Date', actual_headers) - self.assertIn('X-Amz-Security-Token', actual_headers) - - self.assertEqual(actual_headers['Authorization'], self.expected_auth_header) - self.assertEqual(actual_headers['X-Amz-Date'], self.expected_auth_x_amz_date) - self.assertEqual(actual_headers['X-Amz-Security-Token'], self.expected_auth_security_token) + self.assertIn("Authorization", actual_headers) + self.assertIn("X-Amz-Date", actual_headers) + self.assertIn("X-Amz-Security-Token", actual_headers) + self.assertEqual(actual_headers[AUTHORIZATION_HEADER], self.expected_auth_header) + self.assertEqual(actual_headers[X_AMZ_DATE_HEADER], self.expected_auth_x_amz_date) + self.assertEqual(actual_headers[X_AMZ_SECURITY_TOKEN_HEADER], self.expected_auth_security_token) def validate_exporter_extends_http_span_exporter(self, exporter, endpoint): self.assertEqual(exporter._endpoint, endpoint) @@ -256,13 +262,9 @@ def validate_exporter_extends_http_span_exporter(self, exporter, endpoint): self.assertIn("User-Agent", exporter._session.headers) self.assertEqual( exporter._session.headers.get("Content-Type"), - "application/x-protobuf", - ) - self.assertEqual( - exporter._session.headers.get("User-Agent"), - "OTel-OTLP-Exporter-Python/" + __version__, + CONTENT_TYPE, ) - + self.assertEqual(exporter._session.headers.get("User-Agent"), USER_AGENT) def create_span(self, name="test_span", kind=SpanKind.INTERNAL): span = _Span( @@ -271,15 +273,17 @@ def create_span(self, name="test_span", kind=SpanKind.INTERNAL): trace_id=0x1234567890ABCDEF, span_id=0x9876543210, is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED) + trace_flags=TraceFlags(TraceFlags.SAMPLED), ), - kind=kind + kind=kind, ) return span def mock_add_auth(self, request): - request.headers._headers.extend([ - ('Authorization', self.expected_auth_header), - ('X-Amz-Date', self.expected_auth_x_amz_date), - ('X-Amz-Security-Token', self.expected_auth_security_token) - ]) \ No newline at end of file + request.headers._headers.extend( + [ + (AUTHORIZATION_HEADER, self.expected_auth_header), + (X_AMZ_DATE_HEADER, self.expected_auth_x_amz_date), + (X_AMZ_SECURITY_TOKEN_HEADER, self.expected_auth_security_token), + ] + ) From 5fc6cfbcd2f4d84234b978bec583a3fc9806b4af Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 05:40:06 +0000 Subject: [PATCH 06/39] fixing sanitation issue --- .../distro/aws_opentelemetry_configurator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index ff44a422c..c4b47092f 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -446,8 +446,17 @@ def _is_lambda_environment(): def _is_otlp_endpoint_cloudwatch(): + # Detects if it's the OTLP endpoint in CloudWatchs otlp_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) - return otlp_endpoint and "xray." in otlp_endpoint.lower() and ".amazonaws.com" in otlp_endpoint.lower() + if not otlp_endpoint: + return False + + endpoint_lower = otlp_endpoint.lower() + + has_xray = "xray." in endpoint_lower + has_amazonaws = ".amazonaws.com" in endpoint_lower + + return has_xray and has_amazonaws def _get_metric_export_interval(): From db6d384f2499a69b958f5918e13f5f2bac776fa5 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 05:41:57 +0000 Subject: [PATCH 07/39] formatting --- .../opentelemetry/distro/aws_opentelemetry_configurator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index c4b47092f..150ae43a4 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -450,12 +450,12 @@ def _is_otlp_endpoint_cloudwatch(): otlp_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) if not otlp_endpoint: return False - + endpoint_lower = otlp_endpoint.lower() - + has_xray = "xray." in endpoint_lower has_amazonaws = ".amazonaws.com" in endpoint_lower - + return has_xray and has_amazonaws From 579efb30d28b22840552c173c0abba6e7bde876e Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 05:45:44 +0000 Subject: [PATCH 08/39] fix arbitrary url error --- .../distro/aws_opentelemetry_configurator.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index 150ae43a4..852779792 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. import os +import re from logging import Logger, getLogger from typing import ClassVar, Dict, List, Type, Union @@ -450,13 +451,9 @@ def _is_otlp_endpoint_cloudwatch(): otlp_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) if not otlp_endpoint: return False + pattern = r"xray\.([a-z0-9-]+)\.amazonaws\.com" - endpoint_lower = otlp_endpoint.lower() - - has_xray = "xray." in endpoint_lower - has_amazonaws = ".amazonaws.com" in endpoint_lower - - return has_xray and has_amazonaws + return bool(re.match(pattern, otlp_endpoint.lower())) def _get_metric_export_interval(): From f97fd247cdec9ca8f95297cf8d7bbcb526d8a94a Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 06:27:38 +0000 Subject: [PATCH 09/39] linting imports --- .../opentelemetry/distro/test_otlp_sigv4_exporter.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index dd79a88e8..259cf527c 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -16,13 +16,8 @@ DEFAULT_TRACES_EXPORT_PATH, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, -) -from opentelemetry.sdk.trace import Resource, SpanContext, SpanLimits, Tracer, _Span -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.id_generator import RandomIdGenerator -from opentelemetry.sdk.trace.sampling import ALWAYS_ON +from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_TRACES_ENDPOINT +from opentelemetry.sdk.trace import SpanContext, _Span from opentelemetry.trace import SpanKind, TraceFlags OTLP_CW_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" From 451f19461cf5bbbf81d4727c0f88d91b0bdd6f5c Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 06:31:30 +0000 Subject: [PATCH 10/39] linting fix --- .../distro/test_otlp_sigv4_exporter.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index 259cf527c..410a8db70 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -1,7 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import os -import time from unittest import TestCase from unittest.mock import ANY, MagicMock, PropertyMock, patch @@ -71,7 +70,8 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): mock_session.get_available_regions.assert_called_once_with("xray") - # Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter + # Tests that the exporter constructor behavior + # is set by OTLP protobuf/http Span Exporter # if an invalid OTLP CloudWatch endpoint is set @patch("botocore.session.Session") def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): @@ -89,8 +89,9 @@ def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): self.assertIsNone(exporter._aws_region) - # Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, SigV4 authentication method is NOT called and - # is NOT injected into the existing Session headers. + # Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, + # SigV4 authentication method is NOT called and is + # NOT injected into the existing Session headers. @patch("botocore.session.Session.get_available_regions") @patch("requests.Session.post") @patch("botocore.auth.SigV4Auth.add_auth") @@ -153,8 +154,9 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( cert=ANY, ) - # Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, SigV4 authentication method is NOT called and - # is NOT injected into the existing Session headers. + # Tests that if the OTLP endpoint is a valid + # CW endpoint but no credentials are returned, + # SigV4 authentication method is NOT called and is NOT injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") @@ -198,7 +200,8 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - # Tests that if the OTLP endpoint is valid and credentials are valid, SigV4 authentication method is called and + # Tests that if the OTLP endpoint is valid + # and credentials are valid, SigV4 authentication method is called and # is injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") From 6ce4d68bbda18bae3775c2d8ee9af9b854356bfb Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 06:31:37 +0000 Subject: [PATCH 11/39] linting fix --- .../opentelemetry/distro/test_otlp_sigv4_exporter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index 410a8db70..eaf9e9e96 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -154,9 +154,9 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( cert=ANY, ) - # Tests that if the OTLP endpoint is a valid - # CW endpoint but no credentials are returned, - # SigV4 authentication method is NOT called and is NOT injected into the existing Session headers. + # Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, + # SigV4 authentication method is NOT called and is NOT + # injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") @@ -200,9 +200,9 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - # Tests that if the OTLP endpoint is valid - # and credentials are valid, SigV4 authentication method is called and - # is injected into the existing Session headers. + # Tests that if the OTLP endpoint is valid and credentials are valid, + # SigV4 authentication method is called and is + # injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") From 364f9de23518bf6374ae65e98866f5521de1ede7 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 06:33:57 +0000 Subject: [PATCH 12/39] linting fix --- .../distro/test_otlp_sigv4_exporter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index eaf9e9e96..4ca61f1a7 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -70,7 +70,7 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): mock_session.get_available_regions.assert_called_once_with("xray") - # Tests that the exporter constructor behavior + # Tests that the exporter constructor behavior # is set by OTLP protobuf/http Span Exporter # if an invalid OTLP CloudWatch endpoint is set @patch("botocore.session.Session") @@ -89,8 +89,8 @@ def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): self.assertIsNone(exporter._aws_region) - # Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, - # SigV4 authentication method is NOT called and is + # Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, + # SigV4 authentication method is NOT called and is # NOT injected into the existing Session headers. @patch("botocore.session.Session.get_available_regions") @patch("requests.Session.post") @@ -154,8 +154,8 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( cert=ANY, ) - # Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, - # SigV4 authentication method is NOT called and is NOT + # Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, + # SigV4 authentication method is NOT called and is NOT # injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") @@ -200,8 +200,8 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - # Tests that if the OTLP endpoint is valid and credentials are valid, - # SigV4 authentication method is called and is + # Tests that if the OTLP endpoint is valid and credentials are valid, + # SigV4 authentication method is called and is # injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") From f217ed18f8c30266bb17fb39be0be8e7454e9ae7 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 06:43:44 +0000 Subject: [PATCH 13/39] lint fix --- .../opentelemetry/distro/otlp_sigv4_exporter.py | 16 ++++++++-------- .../distro/test_otlp_sigv4_exporter.py | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index 332e16011..a83c1ec17 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -67,7 +67,8 @@ def _export(self, serialized_data: bytes): return super()._export(serialized_data) - def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: + @staticmethod + def _validate_exporter_endpoint(endpoint: str) -> Optional[str]: if not endpoint: return None @@ -83,12 +84,11 @@ def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: return None - else: - _logger.error( - f"Invalid XRay traces endpoint: {endpoint}." - "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " - "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " - "https://xray.us-west-2.amazonaws.com/v1/traces." - ) + _logger.error( + f"Invalid XRay traces endpoint: {endpoint}." + "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " + "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " + "https://xray.us-west-2.amazonaws.com/v1/traces." + ) return None diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index 4ca61f1a7..b309433ea 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -264,7 +264,8 @@ def validate_exporter_extends_http_span_exporter(self, exporter, endpoint): ) self.assertEqual(exporter._session.headers.get("User-Agent"), USER_AGENT) - def create_span(self, name="test_span", kind=SpanKind.INTERNAL): + @staticmethod + def create_span(name="test_span", kind=SpanKind.INTERNAL): span = _Span( name=name, context=SpanContext( From 56372781f04c05297b81dd06cd56f78518a03914 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 06:52:52 +0000 Subject: [PATCH 14/39] linting fix --- .../distro/otlp_sigv4_exporter.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index a83c1ec17..17d0f5a2d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 import logging import re from typing import Dict, Optional @@ -14,7 +16,6 @@ _logger = logging.getLogger(__name__) - class OTLPAwsSigV4Exporter(OTLPSpanExporter): def __init__( @@ -26,7 +27,7 @@ def __init__( headers: Optional[Dict[str, str]] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, - session: Optional[requests.Session] = None, + rsession: Optional[requests.Session] = None, ): self._aws_region = self._validate_exporter_endpoint(endpoint) @@ -38,7 +39,7 @@ def __init__( headers=headers, timeout=timeout, compression=compression, - session=session, + session=rsession, ) def _export(self, serialized_data: bytes): @@ -59,7 +60,7 @@ def _export(self, serialized_data: bytes): signer.add_auth(request) self._session.headers.update(dict(request.headers)) - except Exception as signing_error: + except (BotoCoreError, ClientError, ValueError) as signing_error: _logger.error(f"Failed to sign request: {signing_error}") else: @@ -71,24 +72,24 @@ def _export(self, serialized_data: bytes): def _validate_exporter_endpoint(endpoint: str) -> Optional[str]: if not endpoint: return None - - match = re.search(rf"{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com", endpoint) - + + match = re.search(f'{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com', endpoint) + if match: region = match.group(1) xray_regions = session.Session().get_available_regions(AWS_SERVICE) - if region in xray_regions: return region - _logger.error(f"Invalid AWS region: {region}. Valid regions are {xray_regions}.") - + + _logger.error("Invalid AWS region: %s. Valid regions are %s. Resolving to default endpoint.", + region, xray_regions) return None - - _logger.error( - f"Invalid XRay traces endpoint: {endpoint}." - "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " - "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " - "https://xray.us-west-2.amazonaws.com/v1/traces." - ) - + + _logger.error("Invalid XRay traces endpoint: %s. Resolving to default endpoint. " + "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " + "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " + "https://xray.us-west-2.amazonaws.com/v1/traces.", + endpoint) + return None + From 8e1d0eb8fb14f85f02fbc0219adb28d679f3dba8 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 06:54:10 +0000 Subject: [PATCH 15/39] lint fix --- .../distro/otlp_sigv4_exporter.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index 17d0f5a2d..a14f2662d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -16,6 +16,7 @@ _logger = logging.getLogger(__name__) + class OTLPAwsSigV4Exporter(OTLPSpanExporter): def __init__( @@ -72,24 +73,26 @@ def _export(self, serialized_data: bytes): def _validate_exporter_endpoint(endpoint: str) -> Optional[str]: if not endpoint: return None - - match = re.search(f'{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com', endpoint) - + + match = re.search(f"{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com", endpoint) + if match: region = match.group(1) xray_regions = session.Session().get_available_regions(AWS_SERVICE) if region in xray_regions: return region - - _logger.error("Invalid AWS region: %s. Valid regions are %s. Resolving to default endpoint.", - region, xray_regions) + + _logger.error( + "Invalid AWS region: %s. Valid regions are %s. Resolving to default endpoint.", region, xray_regions + ) return None - - _logger.error("Invalid XRay traces endpoint: %s. Resolving to default endpoint. " - "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " - "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " - "https://xray.us-west-2.amazonaws.com/v1/traces.", - endpoint) - - return None + _logger.error( + "Invalid XRay traces endpoint: %s. Resolving to default endpoint. " + "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " + "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " + "https://xray.us-west-2.amazonaws.com/v1/traces.", + endpoint, + ) + + return None From bb591a2c065e60df0d85b03b352ba28fa85a361b Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 07:02:09 +0000 Subject: [PATCH 16/39] linting fix --- .../src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index a14f2662d..61a12a3c8 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -6,7 +6,7 @@ import requests from botocore import session -from botocore.auth import SigV4Auth +from botocore.auth import NoCredentialsError, SigV4Auth from botocore.awsrequest import AWSRequest from grpc import Compression @@ -61,7 +61,7 @@ def _export(self, serialized_data: bytes): signer.add_auth(request) self._session.headers.update(dict(request.headers)) - except (BotoCoreError, ClientError, ValueError) as signing_error: + except NoCredentialsError as signing_error: _logger.error(f"Failed to sign request: {signing_error}") else: @@ -74,7 +74,7 @@ def _validate_exporter_endpoint(endpoint: str) -> Optional[str]: if not endpoint: return None - match = re.search(f"{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com", endpoint) + match = re.search(rf"{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com", endpoint) if match: region = match.group(1) From ad4c0a02a3705d645b000ab57489e17f30debec9 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 07:04:42 +0000 Subject: [PATCH 17/39] linting fix --- .../src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py | 2 +- .../amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index 61a12a3c8..b5b0bf899 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -62,7 +62,7 @@ def _export(self, serialized_data: bytes): self._session.headers.update(dict(request.headers)) except NoCredentialsError as signing_error: - _logger.error(f"Failed to sign request: {signing_error}") + _logger.error("Failed to sign request: %s", signing_error) else: _logger.error("Failed to get credentials to export span to OTLP CloudWatch endpoint") diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index b309433ea..f1e21e451 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -40,7 +40,8 @@ def setUp(self): self.invalid_cw_otlp_tracing_endpoints = [ "https://xray.bad-region-1.amazonaws.com/v1/traces", "https://xray.us-east-1.amaz.com/v1/traces", - "https://logs.us-east-1.amazonaws.com/v1/logs" "https://test-endpoint123.com/test", + "https://logs.us-east-1.amazonaws.com/v1/logs", + "https://test-endpoint123.com/test", ] self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request" From f943b073d763968783213bc416e34b92d9ea3d89 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 21:22:28 +0000 Subject: [PATCH 18/39] made botocore an optional dependency if not using otlp cw endpoint --- .../src/amazon/opentelemetry/distro/_utils.py | 14 ++++ .../distro/aws_opentelemetry_configurator.py | 15 +---- .../distro/otlp_sigv4_exporter.py | 66 +++++++++++-------- .../distro/test_otlp_sigv4_exporter.py | 46 ++++++++----- 4 files changed, 85 insertions(+), 56 deletions(-) create mode 100644 aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py new file mode 100644 index 000000000..568faff60 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -0,0 +1,14 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import re + + +def is_otlp_endpoint_cloudwatch(otlp_endpoint=None): + # Detects if it's the OTLP endpoint in CloudWatchs + if not otlp_endpoint: + return False + + pattern = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$" + + return bool(re.match(pattern, otlp_endpoint.lower())) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index 852779792..ab97cfc71 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -11,6 +11,7 @@ from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute +from amazon.opentelemetry.distro._utils import is_otlp_endpoint_cloudwatch from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import ( AttributePropagatingSpanProcessorBuilder, @@ -317,7 +318,7 @@ def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> Span traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000") span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint) - if _is_otlp_endpoint_cloudwatch(): + if is_otlp_endpoint_cloudwatch(os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)): span_exporter = OTLPAwsSigV4Exporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) if not _is_application_signals_enabled(): @@ -334,7 +335,7 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) -> provider.add_span_processor(AttributePropagatingSpanProcessorBuilder().build()) # Do not export metrics if it's CloudWatch OTLP endpoint - if _is_otlp_endpoint_cloudwatch(): + if is_otlp_endpoint_cloudwatch(): return # Export 100% spans and not export Application-Signals metrics if on Lambda. @@ -446,16 +447,6 @@ def _is_lambda_environment(): return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ -def _is_otlp_endpoint_cloudwatch(): - # Detects if it's the OTLP endpoint in CloudWatchs - otlp_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) - if not otlp_endpoint: - return False - pattern = r"xray\.([a-z0-9-]+)\.amazonaws\.com" - - return bool(re.match(pattern, otlp_endpoint.lower())) - - def _get_metric_export_interval(): export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL)) _logger.debug("Span Metrics export interval: %s", export_interval_millis) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index b5b0bf899..f3255b895 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -5,15 +5,12 @@ from typing import Dict, Optional import requests -from botocore import session -from botocore.auth import NoCredentialsError, SigV4Auth -from botocore.awsrequest import AWSRequest from grpc import Compression +from amazon.opentelemetry.distro._utils import is_otlp_endpoint_cloudwatch from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter AWS_SERVICE = "xray" - _logger = logging.getLogger(__name__) @@ -31,7 +28,33 @@ def __init__( rsession: Optional[requests.Session] = None, ): - self._aws_region = self._validate_exporter_endpoint(endpoint) + self._aws_region = None + + if endpoint and is_otlp_endpoint_cloudwatch(endpoint): + try: + from botocore import auth, awsrequest, session + + self.boto_auth = auth + self.boto_aws_request = awsrequest + self.boto_session = session.Session() + + self._aws_region = self._validate_exporter_endpoint(endpoint) + + except ImportError: + _logger.error( + "botocore is required to export traces to %s. " "Please install it using `pip install botocore`", + endpoint, + ) + + else: + _logger.error( + "Invalid XRay traces endpoint: %s. Resolving to OTLPSpanExporter to handle exporting. " + "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " + "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " + "https://xray.us-west-2.amazonaws.com/v1/traces.", + endpoint, + ) + super().__init__( endpoint=endpoint, certificate_file=certificate_file, @@ -45,23 +68,23 @@ def __init__( def _export(self, serialized_data: bytes): if self._aws_region: - request = AWSRequest( + request = self.boto_aws_request.AWSRequest( method="POST", url=self._endpoint, data=serialized_data, headers={"Content-Type": "application/x-protobuf"}, ) - botocore_session = session.Session() - credentials = botocore_session.get_credentials() + credentials = self.boto_session.get_credentials() + if credentials is not None: - signer = SigV4Auth(credentials, AWS_SERVICE, self._aws_region) + signer = self.boto_auth.SigV4Auth(credentials, AWS_SERVICE, self._aws_region) try: signer.add_auth(request) self._session.headers.update(dict(request.headers)) - except NoCredentialsError as signing_error: + except self.boto_auth.NoCredentialsError as signing_error: _logger.error("Failed to sign request: %s", signing_error) else: @@ -69,30 +92,19 @@ def _export(self, serialized_data: bytes): return super()._export(serialized_data) - @staticmethod - def _validate_exporter_endpoint(endpoint: str) -> Optional[str]: + def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: if not endpoint: return None - match = re.search(rf"{AWS_SERVICE}\.([a-z0-9-]+)\.amazonaws\.com", endpoint) + region = endpoint.split(".")[1] + xray_regions = self.boto_session.get_available_regions(AWS_SERVICE) - if match: - region = match.group(1) - xray_regions = session.Session().get_available_regions(AWS_SERVICE) - if region in xray_regions: - return region + if region not in xray_regions: _logger.error( "Invalid AWS region: %s. Valid regions are %s. Resolving to default endpoint.", region, xray_regions ) - return None - _logger.error( - "Invalid XRay traces endpoint: %s. Resolving to default endpoint. " - "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " - "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " - "https://xray.us-west-2.amazonaws.com/v1/traces.", - endpoint, - ) + return None - return None + return region diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index f1e21e451..5686603af 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -13,6 +13,7 @@ DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, DEFAULT_TRACES_EXPORT_PATH, + OTLPSpanExporter, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_TRACES_ENDPOINT @@ -42,24 +43,29 @@ def setUp(self): "https://xray.us-east-1.amaz.com/v1/traces", "https://logs.us-east-1.amazonaws.com/v1/logs", "https://test-endpoint123.com/test", + "xray.us-east-1.amazonaws.com/v1/traces", + "https://test-endpoint123.com/test https://xray.us-east-1.amazonaws.com/v1/traces", + "https://xray.us-east-1.amazonaws.com/v1/tracesssda", ] self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request" self.expected_auth_x_amz_date = "some_date" self.expected_auth_security_token = "test_token" - # Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set @patch.dict(os.environ, {}, clear=True) def test_sigv4_exporter_init_default(self): + """Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set""" + exporter = OTLPAwsSigV4Exporter() self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH) self.assertIsInstance(exporter._session, requests.Session) - # Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http - # Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}, clear=True) @patch("botocore.session.Session") def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): + """Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http + Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set.""" + mock_session = MagicMock() session_mock.return_value = mock_session @@ -71,12 +77,18 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): mock_session.get_available_regions.assert_called_once_with("xray") - # Tests that the exporter constructor behavior - # is set by OTLP protobuf/http Span Exporter - # if an invalid OTLP CloudWatch endpoint is set + @patch.dict("sys.modules", {"botocore": None}) + def test_no_botocore_valid_xray_endpoint(self): + """Test that exporter defaults when using OTLP CW endpoint without botocore""" + + exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + + self.assertIsNone(exporter._aws_region) + @patch("botocore.session.Session") def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): - + """Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter + if an invalid OTLP CloudWatch endpoint is set""" for bad_endpoint in self.invalid_cw_otlp_tracing_endpoints: with self.subTest(endpoint=bad_endpoint): 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): self.assertIsNone(exporter._aws_region) - # Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, - # SigV4 authentication method is NOT called and is - # NOT injected into the existing Session headers. @patch("botocore.session.Session.get_available_regions") @patch("requests.Session.post") @patch("botocore.auth.SigV4Auth.add_auth") def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( self, mock_sigv4_auth, requests_mock, botocore_mock ): + """Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, + SigV4 authentication method is NOT called and is NOT injected into the existing Session headers.""" + # Setting the exporter response mock_response = MagicMock() mock_response.status_code = 200 @@ -155,9 +167,6 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( cert=ANY, ) - # Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, - # SigV4 authentication method is NOT called and is NOT - # injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") @@ -165,7 +174,9 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( self, mock_sigv4_auth, requests_posts_mock, botocore_mock ): - + """Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, + SigV4 authentication method is NOT called and is NOT injected into the existing + Session headers.""" # Setting the exporter response mock_response = MagicMock() mock_response.status_code = 200 @@ -201,9 +212,6 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - # Tests that if the OTLP endpoint is valid and credentials are valid, - # SigV4 authentication method is called and is - # injected into the existing Session headers. @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") @@ -211,6 +219,9 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( self, mock_sigv4_auth, requests_posts_mock, botocore_mock ): + """Tests that if the OTLP endpoint is valid and credentials are valid, + SigV4 authentication method is called and is + injected into the existing Session headers.""" # Setting the exporter response mock_response = MagicMock() @@ -251,6 +262,7 @@ def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( self.assertEqual(actual_headers[X_AMZ_SECURITY_TOKEN_HEADER], self.expected_auth_security_token) def validate_exporter_extends_http_span_exporter(self, exporter, endpoint): + self.assertIsInstance(exporter, OTLPSpanExporter) self.assertEqual(exporter._endpoint, endpoint) self.assertEqual(exporter._certificate_file, True) self.assertEqual(exporter._client_certificate_file, None) From c796162dbd36570f7cffb1a141c48ec255e2960f Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 21:40:21 +0000 Subject: [PATCH 19/39] comments + linting fix --- .../distro/aws_opentelemetry_configurator.py | 1 - .../opentelemetry/distro/otlp_sigv4_exporter.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index ab97cfc71..b94736c58 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. import os -import re from logging import Logger, getLogger from typing import ClassVar, Dict, List, Type, Union diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index f3255b895..d3c73dc22 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -1,7 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import logging -import re from typing import Dict, Optional import requests @@ -13,6 +12,9 @@ AWS_SERVICE = "xray" _logger = logging.getLogger(__name__) +"""The OTLPAwsSigV4Exporter extends the functionality of the OTLPSpanExporter to allow SigV4 authentication if the + configured traces endpoint is a CloudWatch OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces""" + class OTLPAwsSigV4Exporter(OTLPSpanExporter): @@ -28,16 +30,21 @@ def __init__( rsession: Optional[requests.Session] = None, ): + # Represents the region of the CloudWatch OTLP endpoint to send the traces to. + # If the endpoint has been verified to be valid, this should not be None + self._aws_region = None if endpoint and is_otlp_endpoint_cloudwatch(endpoint): try: + # Defensive check to verify that the application being auto instrumented has + # botocore installed. + from botocore import auth, awsrequest, session self.boto_auth = auth self.boto_aws_request = awsrequest self.boto_session = session.Session() - self._aws_region = self._validate_exporter_endpoint(endpoint) except ImportError: From 0b656421423317c7075f3d1e5c26fdd4a4d55c43 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 21:58:49 +0000 Subject: [PATCH 20/39] linting fix --- .../src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index d3c73dc22..925bb26c2 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -36,10 +36,11 @@ def __init__( self._aws_region = None if endpoint and is_otlp_endpoint_cloudwatch(endpoint): - try: - # Defensive check to verify that the application being auto instrumented has - # botocore installed. + # Defensive check to verify that the application being auto instrumented has + # botocore installed. + try: + # pylint: disable=import-outside-toplevel from botocore import auth, awsrequest, session self.boto_auth = auth From 561fa01ce8d4b092de2e5a259026904b31616ab3 Mon Sep 17 00:00:00 2001 From: liustve Date: Tue, 11 Feb 2025 22:00:15 +0000 Subject: [PATCH 21/39] linting fix --- .../src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py index 925bb26c2..6b7743f56 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py @@ -50,7 +50,7 @@ def __init__( except ImportError: _logger.error( - "botocore is required to export traces to %s. " "Please install it using `pip install botocore`", + "botocore is required to export traces to %s. Please install it using `pip install botocore`", endpoint, ) From bba778d8bf55d5c13e8eb1081d27b3a542a11e65 Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:13:40 +0000 Subject: [PATCH 22/39] addressing comments --- .../src/amazon/opentelemetry/distro/_utils.py | 29 +++++- .../distro/aws_opentelemetry_configurator.py | 14 +-- ..._exporter.py => otlp_aws_span_exporter.py} | 50 +++-------- .../distro/patches/_instrumentation_patch.py | 17 +--- .../distro/test_otlp_sigv4_exporter.py | 90 ++++++++----------- 5 files changed, 87 insertions(+), 113 deletions(-) rename aws-opentelemetry-distro/src/amazon/opentelemetry/distro/{otlp_sigv4_exporter.py => otlp_aws_span_exporter.py} (62%) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index 568faff60..07cfb8270 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -2,13 +2,34 @@ # SPDX-License-Identifier: Apache-2.0 import re +import sys +from logging import Logger, getLogger +import pkg_resources + +_logger: Logger = getLogger(__name__) + +XRAY_OTLP_ENDPOINT_PATTERN = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$" + + +def is_xray_otlp_endpoint(otlp_endpoint: str = None) -> bool: + """Is the given endpoint the XRay OTLP endpoint?""" -def is_otlp_endpoint_cloudwatch(otlp_endpoint=None): - # Detects if it's the OTLP endpoint in CloudWatchs if not otlp_endpoint: return False - pattern = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$" + return bool(re.match(XRAY_OTLP_ENDPOINT_PATTERN, otlp_endpoint.lower())) - return bool(re.match(pattern, otlp_endpoint.lower())) + +def is_installed(req: str) -> bool: + """Is the given required package installed?""" + + if req in sys.modules and sys.modules.get(req) != None: + return True + + try: + pkg_resources.get_distribution(req) + except Exception as exc: # pylint: disable=broad-except + _logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc) + return False + return True diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index b94736c58..d6345d07c 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -10,7 +10,7 @@ from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute -from amazon.opentelemetry.distro._utils import is_otlp_endpoint_cloudwatch +from amazon.opentelemetry.distro._utils import is_xray_otlp_endpoint from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import ( AttributePropagatingSpanProcessorBuilder, @@ -20,7 +20,7 @@ AwsMetricAttributesSpanExporterBuilder, ) from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder -from amazon.opentelemetry.distro.otlp_sigv4_exporter import OTLPAwsSigV4Exporter +from amazon.opentelemetry.distro.otlp_aws_span_exporter import OTLPAwsSpanExporter from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader @@ -317,8 +317,10 @@ def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> Span traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000") span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint) - if is_otlp_endpoint_cloudwatch(os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)): - span_exporter = OTLPAwsSigV4Exporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) + if isinstance(span_exporter, OTLPSpanExporter) and is_xray_otlp_endpoint( + os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + ): + span_exporter = OTLPAwsSpanExporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) if not _is_application_signals_enabled(): return span_exporter @@ -333,8 +335,8 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) -> # Construct and set local and remote attributes span processor provider.add_span_processor(AttributePropagatingSpanProcessorBuilder().build()) - # Do not export metrics if it's CloudWatch OTLP endpoint - if is_otlp_endpoint_cloudwatch(): + # Do not export Application-Signals metrics if it's XRay OTLP endpoint + if is_xray_otlp_endpoint(): return # Export 100% spans and not export Application-Signals metrics if on Lambda. diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py similarity index 62% rename from aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py rename to aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py index 6b7743f56..e040a1d40 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py @@ -4,19 +4,23 @@ from typing import Dict, Optional import requests -from grpc import Compression -from amazon.opentelemetry.distro._utils import is_otlp_endpoint_cloudwatch +from amazon.opentelemetry.distro._utils import is_installed, is_xray_otlp_endpoint +from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter AWS_SERVICE = "xray" _logger = logging.getLogger(__name__) -"""The OTLPAwsSigV4Exporter extends the functionality of the OTLPSpanExporter to allow SigV4 authentication if the - configured traces endpoint is a CloudWatch OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces""" +class OTLPAwsSpanExporter(OTLPSpanExporter): + """ + This exporter extends the functionality of the OTLPSpanExporter to allow spans to be exported to the + XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the botocore + library to sign and directly inject SigV4 Authentication to the exported request's headers. -class OTLPAwsSigV4Exporter(OTLPSpanExporter): + https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html + """ def __init__( self, @@ -35,34 +39,23 @@ def __init__( self._aws_region = None - if endpoint and is_otlp_endpoint_cloudwatch(endpoint): + if endpoint and is_xray_otlp_endpoint(endpoint): - # Defensive check to verify that the application being auto instrumented has - # botocore installed. - try: + if is_installed("botocore"): # pylint: disable=import-outside-toplevel from botocore import auth, awsrequest, session self.boto_auth = auth self.boto_aws_request = awsrequest self.boto_session = session.Session() - self._aws_region = self._validate_exporter_endpoint(endpoint) + self._aws_region = endpoint.split(".")[1] - except ImportError: + else: _logger.error( "botocore is required to export traces to %s. Please install it using `pip install botocore`", endpoint, ) - else: - _logger.error( - "Invalid XRay traces endpoint: %s. Resolving to OTLPSpanExporter to handle exporting. " - "The traces endpoint follows the pattern https://xray.[AWSRegion].amazonaws.com/v1/traces. " - "For example, for the US West (Oregon) (us-west-2) Region, the endpoint will be " - "https://xray.us-west-2.amazonaws.com/v1/traces.", - endpoint, - ) - super().__init__( endpoint=endpoint, certificate_file=certificate_file, @@ -99,20 +92,3 @@ def _export(self, serialized_data: bytes): _logger.error("Failed to get credentials to export span to OTLP CloudWatch endpoint") return super()._export(serialized_data) - - def _validate_exporter_endpoint(self, endpoint: str) -> Optional[str]: - if not endpoint: - return None - - region = endpoint.split(".")[1] - xray_regions = self.boto_session.get_available_regions(AWS_SERVICE) - - if region not in xray_regions: - - _logger.error( - "Invalid AWS region: %s. Valid regions are %s. Resolving to default endpoint.", region, xray_regions - ) - - return None - - return region diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index fcda07e64..65b126f7d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -7,6 +7,7 @@ import pkg_resources +from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches # Env variable for determining whether we want to monkey patch gevent modules. Possible values are 'all', 'none', and @@ -25,7 +26,7 @@ def apply_instrumentation_patches() -> None: Where possible, automated testing should be run to catch upstream changes resulting in broken patches """ - if _is_installed("gevent"): + if is_installed("gevent"): try: gevent_patch_module = os.environ.get(AWS_GEVENT_PATCH_MODULES, "all") @@ -56,7 +57,7 @@ def apply_instrumentation_patches() -> None: except Exception as exc: # pylint: disable=broad-except _logger.info("Failed to monkey patch gevent, exception: %s", exc) - if _is_installed("botocore ~= 1.0"): + if is_installed("botocore ~= 1.0"): # pylint: disable=import-outside-toplevel # Delay import to only occur if patches is safe to apply (e.g. the instrumented library is installed). from amazon.opentelemetry.distro.patches._botocore_patches import _apply_botocore_instrumentation_patches @@ -66,15 +67,3 @@ def apply_instrumentation_patches() -> None: # No need to check if library is installed as this patches opentelemetry.sdk, # which must be installed for the distro to work at all. _apply_resource_detector_patches() - - -def _is_installed(req: str) -> bool: - if req in sys.modules: - return True - - try: - pkg_resources.get_distribution(req) - except Exception as exc: # pylint: disable=broad-except - _logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc) - return False - return True diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index 5686603af..b97516ee5 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -1,13 +1,14 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import os +import sys from unittest import TestCase from unittest.mock import ANY, MagicMock, PropertyMock, patch import requests from botocore.credentials import Credentials -from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSigV4Exporter +from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSpanExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_COMPRESSION, DEFAULT_ENDPOINT, @@ -20,7 +21,7 @@ from opentelemetry.sdk.trace import SpanContext, _Span from opentelemetry.trace import SpanKind, TraceFlags -OTLP_CW_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" +OTLP_XRAY_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ CONTENT_TYPE = "application/x-protobuf" AUTHORIZATION_HEADER = "Authorization" @@ -38,9 +39,8 @@ def setUp(self): self.create_span("test_span5", SpanKind.CONSUMER), ] - self.invalid_cw_otlp_tracing_endpoints = [ - "https://xray.bad-region-1.amazonaws.com/v1/traces", - "https://xray.us-east-1.amaz.com/v1/traces", + self.invalid_otlp_tracing_endpoints = [ + "https://xray.bad-region-1.amazonaws.com/v1/logs" "https://xray.us-east-1.amaz.com/v1/traces", "https://logs.us-east-1.amazonaws.com/v1/logs", "https://test-endpoint123.com/test", "xray.us-east-1.amazonaws.com/v1/traces", @@ -56,11 +56,26 @@ def setUp(self): def test_sigv4_exporter_init_default(self): """Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set""" - exporter = OTLPAwsSigV4Exporter() + exporter = OTLPAwsSpanExporter() self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH) + self.assertIsNone(exporter._aws_region) self.assertIsInstance(exporter._session, requests.Session) - @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}, clear=True) + @patch.dict("sys.modules", {"botocore": None}, clear=False) + @patch("pkg_resources.get_distribution") + def test_no_botocore_valid_xray_endpoint(self, mock_get_distribution): + """Test that exporter defaults when using OTLP CW endpoint without botocore""" + + def throw_exception(): + raise Exception() + + mock_get_distribution.side_effect = throw_exception + + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) + self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) + self.assertIsNone(exporter._aws_region) + + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}, clear=True) @patch("botocore.session.Session") def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): """Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http @@ -69,47 +84,28 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): mock_session = MagicMock() session_mock.return_value = mock_session - mock_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] - exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) self.assertEqual(exporter._aws_region, "us-east-1") - self.validate_exporter_extends_http_span_exporter(exporter, OTLP_CW_ENDPOINT) - - mock_session.get_available_regions.assert_called_once_with("xray") - - @patch.dict("sys.modules", {"botocore": None}) - def test_no_botocore_valid_xray_endpoint(self): - """Test that exporter defaults when using OTLP CW endpoint without botocore""" - - exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) - - self.assertIsNone(exporter._aws_region) + self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) @patch("botocore.session.Session") def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): """Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter if an invalid OTLP CloudWatch endpoint is set""" - for bad_endpoint in self.invalid_cw_otlp_tracing_endpoints: + for bad_endpoint in self.invalid_otlp_tracing_endpoints: with self.subTest(endpoint=bad_endpoint): with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): - - mock_session = MagicMock() - botocore_mock.return_value = mock_session - - mock_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] - exporter = OTLPAwsSigV4Exporter(endpoint=bad_endpoint) + exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) self.assertIsNone(exporter._aws_region) - @patch("botocore.session.Session.get_available_regions") @patch("requests.Session.post") @patch("botocore.auth.SigV4Auth.add_auth") - def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( - self, mock_sigv4_auth, requests_mock, botocore_mock - ): - """Tests that if the OTLP endpoint is not a valid CW endpoint but the credentials are valid, - SigV4 authentication method is NOT called and is NOT injected into the existing Session headers.""" + def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, mock_sigv4_auth, requests_mock): + """Tests that if the OTLP endpoint is not a valid XRay endpoint but the credentials are valid, + SigV4 authentication method is called but fails so NO headers are injected into the existing Session headers.""" # Setting the exporter response mock_response = MagicMock() @@ -122,27 +118,19 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( requests_mock.return_value = mock_session mock_session.post.return_value = mock_response - mock_botocore_session = MagicMock() - botocore_mock.return_value = mock_botocore_session - mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] - mock_botocore_session.get_credentials.return_value = Credentials( - access_key="test_key", secret_key="test_secret", token="test_token" - ) - # SigV4 mock authentication injection mock_sigv4_auth.side_effect = self.mock_add_auth # Initialize and call exporter - exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) exporter.export(self.testing_spans) - # For each invalid CW OTLP endpoint, vdalidate that the sigv4 - for bad_endpoint in self.invalid_cw_otlp_tracing_endpoints: + # For each invalid CW OTLP endpoint, vdalidate that SigV4 is not injected + for bad_endpoint in self.invalid_otlp_tracing_endpoints: with self.subTest(endpoint=bad_endpoint): with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): - botocore_mock.return_value = ["us-east-1", "us-west-2"] - exporter = OTLPAwsSigV4Exporter(endpoint=bad_endpoint) + exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) @@ -151,7 +139,7 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( exporter.export(self.testing_spans) - mock_sigv4_auth.assert_not_called() + mock_sigv4_auth.assert_called_once() # Verify that SigV4 request headers were not injected actual_headers = mock_session.headers @@ -170,7 +158,7 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") - @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}) + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}) def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( self, mock_sigv4_auth, requests_posts_mock, botocore_mock ): @@ -190,13 +178,12 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( mock_botocore_session = MagicMock() botocore_mock.return_value = mock_botocore_session - mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] # Test case, return None for get credentials mock_botocore_session.get_credentials.return_value = None # Initialize and call exporter - exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) # Validate that the region is valid self.assertEqual(exporter._aws_region, "us-east-1") @@ -215,7 +202,7 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") - @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_CW_ENDPOINT}) + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}) def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( self, mock_sigv4_auth, requests_posts_mock, botocore_mock ): @@ -236,7 +223,6 @@ def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( mock_botocore_session = MagicMock() botocore_mock.return_value = mock_botocore_session - mock_botocore_session.get_available_regions.return_value = ["us-east-1", "us-west-2"] mock_botocore_session.get_credentials.return_value = Credentials( access_key="test_key", secret_key="test_secret", token="test_token" ) @@ -245,7 +231,7 @@ def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( mock_sigv4_auth.side_effect = self.mock_add_auth # Initialize and call exporter - exporter = OTLPAwsSigV4Exporter(endpoint=OTLP_CW_ENDPOINT) + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) exporter.export(self.testing_spans) # Verify SigV4 auth was called From 1c8004c9a69b2e7703b67bb4d06b7dc080e2b1fc Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:21:22 +0000 Subject: [PATCH 23/39] linting fix --- .../opentelemetry/distro/patches/_instrumentation_patch.py | 3 --- .../amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py | 1 - 2 files changed, 4 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index 65b126f7d..9a5f4974b 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -2,11 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. import os -import sys from logging import Logger, getLogger -import pkg_resources - from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py index b97516ee5..157267519 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py @@ -1,7 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import os -import sys from unittest import TestCase from unittest.mock import ANY, MagicMock, PropertyMock, patch From de0e89fa3545c67fd31b658db06efc88eeddfbe4 Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:25:03 +0000 Subject: [PATCH 24/39] linting fix --- .../src/amazon/opentelemetry/distro/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index 07cfb8270..be55a8f3a 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -24,7 +24,7 @@ def is_xray_otlp_endpoint(otlp_endpoint: str = None) -> bool: def is_installed(req: str) -> bool: """Is the given required package installed?""" - if req in sys.modules and sys.modules.get(req) != None: + if req in sys.modules and sys.modules[req] is not None: return True try: From c207167e686ad8754358a2fc495eed492f67cf01 Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:43:14 +0000 Subject: [PATCH 25/39] tests + linting fix --- .../distro/patches/_instrumentation_patch.py | 3 + .../distro/test_otlp_aws_span_exporter.py | 282 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index 9a5f4974b..82baad48d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -2,8 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. import os +import sys from logging import Logger, getLogger +import pkg_resources # noqa: F401 + from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py new file mode 100644 index 000000000..7579b0e80 --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py @@ -0,0 +1,282 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import os +from unittest import TestCase +from unittest.mock import ANY, MagicMock, PropertyMock, patch + +import requests +from botocore.credentials import Credentials + +from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSpanExporter +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, + DEFAULT_TRACES_EXPORT_PATH, + OTLPSpanExporter, +) +from opentelemetry.exporter.otlp.proto.http.version import __version__ +from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_TRACES_ENDPOINT +from opentelemetry.sdk.trace import SpanContext, _Span +from opentelemetry.trace import SpanKind, TraceFlags + +OTLP_XRAY_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" +USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ +CONTENT_TYPE = "application/x-protobuf" +AUTHORIZATION_HEADER = "Authorization" +X_AMZ_DATE_HEADER = "X-Amz-Date" +X_AMZ_SECURITY_TOKEN_HEADER = "X-Amz-Security-Token" + + +class TestAwsSpanExporter(TestCase): + def setUp(self): + self.testing_spans = [ + self.create_span("test_span1", SpanKind.INTERNAL), + self.create_span("test_span2", SpanKind.SERVER), + self.create_span("test_span3", SpanKind.CLIENT), + self.create_span("test_span4", SpanKind.PRODUCER), + self.create_span("test_span5", SpanKind.CONSUMER), + ] + + self.invalid_otlp_tracing_endpoints = [ + "https://xray.bad-region-1.amazonaws.com/v1/traces", + "https://xray.us-east-1.amaz.com/v1/traces", + "https://logs.us-east-1.amazonaws.com/v1/logs", + "https://test-endpoint123.com/test", + "xray.us-east-1.amazonaws.com/v1/traces", + "https://test-endpoint123.com/test https://xray.us-east-1.amazonaws.com/v1/traces", + "https://xray.us-east-1.amazonaws.com/v1/tracesssda", + ] + + self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request" + self.expected_auth_x_amz_date = "some_date" + self.expected_auth_security_token = "test_token" + + @patch.dict(os.environ, {}, clear=True) + def test_sigv4_exporter_init_default(self): + """Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set""" + + exporter = OTLPAwsSpanExporter() + self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH) + self.assertIsNone(exporter._aws_region) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict("sys.modules", {"botocore": None}, clear=False) + @patch("pkg_resources.get_distribution") + def test_no_botocore_valid_xray_endpoint(self, mock_get_distribution): + """Test that exporter defaults when using OTLP CW endpoint without botocore""" + + def throw_exception(): + raise Exception() + + mock_get_distribution.side_effect = throw_exception + + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) + self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) + self.assertIsNone(exporter._aws_region) + + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}, clear=True) + @patch("botocore.session.Session") + def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): + """Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http + Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set.""" + + mock_session = MagicMock() + session_mock.return_value = mock_session + + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) + + self.assertEqual(exporter._aws_region, "us-east-1") + self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) + + @patch("botocore.session.Session") + def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): + """Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter + if an invalid OTLP CloudWatch endpoint is set""" + for bad_endpoint in self.invalid_otlp_tracing_endpoints: + with self.subTest(endpoint=bad_endpoint): + with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): + exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) + self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) + + self.assertIsNone(exporter._aws_region) + + @patch("requests.Session.post") + @patch("botocore.auth.SigV4Auth.add_auth") + def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, mock_sigv4_auth, requests_mock): + """Tests that if the OTLP endpoint is not a valid XRay endpoint but the credentials are valid, + SigV4 authentication method is called but fails so NO headers are injected into the existing Session headers.""" + + # Setting the exporter response + mock_response = MagicMock() + mock_response.status_code = 200 + type(mock_response).ok = PropertyMock(return_value=True) + + # Setting the request session headers to make the call to endpoint + mock_session = MagicMock() + mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} + requests_mock.return_value = mock_session + mock_session.post.return_value = mock_response + + # SigV4 mock authentication injection + mock_sigv4_auth.side_effect = self.mock_add_auth + + # Initialize and call exporter + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) + exporter.export(self.testing_spans) + + # For each invalid CW OTLP endpoint, vdalidate that SigV4 is not injected + for bad_endpoint in self.invalid_otlp_tracing_endpoints: + with self.subTest(endpoint=bad_endpoint): + with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): + + exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) + + self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) + + exporter.export(self.testing_spans) + + # Verify that SigV4 request headers were not injected + actual_headers = mock_session.headers + self.assertNotIn(AUTHORIZATION_HEADER, actual_headers) + self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) + self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) + + requests_mock.assert_called_with( + url=bad_endpoint, + data=ANY, + verify=ANY, + timeout=ANY, + cert=ANY, + ) + + @patch("botocore.session.Session") + @patch("requests.Session") + @patch("botocore.auth.SigV4Auth.add_auth") + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}) + def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( + self, mock_sigv4_auth, requests_posts_mock, botocore_mock + ): + """Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, + SigV4 authentication method is NOT called and is NOT injected into the existing + Session headers.""" + # Setting the exporter response + mock_response = MagicMock() + mock_response.status_code = 200 + type(mock_response).ok = PropertyMock(return_value=True) + + # Setting the request session headers to make the call to endpoint + mock_session = MagicMock() + mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} + requests_posts_mock.return_value = mock_session + mock_session.post.return_value = mock_response + + mock_botocore_session = MagicMock() + botocore_mock.return_value = mock_botocore_session + + # Test case, return None for get credentials + mock_botocore_session.get_credentials.return_value = None + + # Initialize and call exporter + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) + + # Validate that the region is valid + self.assertEqual(exporter._aws_region, "us-east-1") + + exporter.export(self.testing_spans) + + # Verify SigV4 auth was not called + mock_sigv4_auth.assert_not_called() + + # Verify that SigV4 request headers were properly injected + actual_headers = mock_session.headers + self.assertNotIn(AUTHORIZATION_HEADER, actual_headers) + self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) + self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) + + @patch("botocore.session.Session") + @patch("requests.Session") + @patch("botocore.auth.SigV4Auth.add_auth") + @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}) + def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( + self, mock_sigv4_auth, requests_posts_mock, botocore_mock + ): + """Tests that if the OTLP endpoint is valid and credentials are valid, + SigV4 authentication method is called and is + injected into the existing Session headers.""" + + # Setting the exporter response + mock_response = MagicMock() + mock_response.status_code = 200 + type(mock_response).ok = PropertyMock(return_value=True) + + # Setting the request session headers to make the call to endpoint + mock_session = MagicMock() + mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} + requests_posts_mock.return_value = mock_session + mock_session.post.return_value = mock_response + + mock_botocore_session = MagicMock() + botocore_mock.return_value = mock_botocore_session + mock_botocore_session.get_credentials.return_value = Credentials( + access_key="test_key", secret_key="test_secret", token="test_token" + ) + + # SigV4 mock authentication injection + mock_sigv4_auth.side_effect = self.mock_add_auth + + # Initialize and call exporter + exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) + exporter.export(self.testing_spans) + + # Verify SigV4 auth was called + mock_sigv4_auth.assert_called_once_with(ANY) + + # Verify that SigV4 request headers were properly injected + actual_headers = mock_session.headers + self.assertIn("Authorization", actual_headers) + self.assertIn("X-Amz-Date", actual_headers) + self.assertIn("X-Amz-Security-Token", actual_headers) + + self.assertEqual(actual_headers[AUTHORIZATION_HEADER], self.expected_auth_header) + self.assertEqual(actual_headers[X_AMZ_DATE_HEADER], self.expected_auth_x_amz_date) + self.assertEqual(actual_headers[X_AMZ_SECURITY_TOKEN_HEADER], self.expected_auth_security_token) + + def validate_exporter_extends_http_span_exporter(self, exporter, endpoint): + self.assertIsInstance(exporter, OTLPSpanExporter) + self.assertEqual(exporter._endpoint, endpoint) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._client_certificate_file, None) + self.assertEqual(exporter._client_key_file, None) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + self.assertIn("User-Agent", exporter._session.headers) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + CONTENT_TYPE, + ) + self.assertEqual(exporter._session.headers.get("User-Agent"), USER_AGENT) + + @staticmethod + def create_span(name="test_span", kind=SpanKind.INTERNAL): + span = _Span( + name=name, + context=SpanContext( + trace_id=0x1234567890ABCDEF, + span_id=0x9876543210, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + kind=kind, + ) + return span + + def mock_add_auth(self, request): + request.headers._headers.extend( + [ + (AUTHORIZATION_HEADER, self.expected_auth_header), + (X_AMZ_DATE_HEADER, self.expected_auth_x_amz_date), + (X_AMZ_SECURITY_TOKEN_HEADER, self.expected_auth_security_token), + ] + ) From 556b0374db88099f5c9ed7015bddafd90534d863 Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:43:41 +0000 Subject: [PATCH 26/39] renaming --- .../distro/test_otlp_sigv4_exporter.py | 286 ------------------ 1 file changed, 286 deletions(-) delete mode 100644 aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py deleted file mode 100644 index 157267519..000000000 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_sigv4_exporter.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -import os -from unittest import TestCase -from unittest.mock import ANY, MagicMock, PropertyMock, patch - -import requests -from botocore.credentials import Credentials - -from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSpanExporter -from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, - DEFAULT_TRACES_EXPORT_PATH, - OTLPSpanExporter, -) -from opentelemetry.exporter.otlp.proto.http.version import __version__ -from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_TRACES_ENDPOINT -from opentelemetry.sdk.trace import SpanContext, _Span -from opentelemetry.trace import SpanKind, TraceFlags - -OTLP_XRAY_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" -USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ -CONTENT_TYPE = "application/x-protobuf" -AUTHORIZATION_HEADER = "Authorization" -X_AMZ_DATE_HEADER = "X-Amz-Date" -X_AMZ_SECURITY_TOKEN_HEADER = "X-Amz-Security-Token" - - -class TestAwsSigV4Exporter(TestCase): - def setUp(self): - self.testing_spans = [ - self.create_span("test_span1", SpanKind.INTERNAL), - self.create_span("test_span2", SpanKind.SERVER), - self.create_span("test_span3", SpanKind.CLIENT), - self.create_span("test_span4", SpanKind.PRODUCER), - self.create_span("test_span5", SpanKind.CONSUMER), - ] - - self.invalid_otlp_tracing_endpoints = [ - "https://xray.bad-region-1.amazonaws.com/v1/logs" "https://xray.us-east-1.amaz.com/v1/traces", - "https://logs.us-east-1.amazonaws.com/v1/logs", - "https://test-endpoint123.com/test", - "xray.us-east-1.amazonaws.com/v1/traces", - "https://test-endpoint123.com/test https://xray.us-east-1.amazonaws.com/v1/traces", - "https://xray.us-east-1.amazonaws.com/v1/tracesssda", - ] - - self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request" - self.expected_auth_x_amz_date = "some_date" - self.expected_auth_security_token = "test_token" - - @patch.dict(os.environ, {}, clear=True) - def test_sigv4_exporter_init_default(self): - """Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set""" - - exporter = OTLPAwsSpanExporter() - self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH) - self.assertIsNone(exporter._aws_region) - self.assertIsInstance(exporter._session, requests.Session) - - @patch.dict("sys.modules", {"botocore": None}, clear=False) - @patch("pkg_resources.get_distribution") - def test_no_botocore_valid_xray_endpoint(self, mock_get_distribution): - """Test that exporter defaults when using OTLP CW endpoint without botocore""" - - def throw_exception(): - raise Exception() - - mock_get_distribution.side_effect = throw_exception - - exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) - self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) - self.assertIsNone(exporter._aws_region) - - @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}, clear=True) - @patch("botocore.session.Session") - def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): - """Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http - Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set.""" - - mock_session = MagicMock() - session_mock.return_value = mock_session - - exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) - - self.assertEqual(exporter._aws_region, "us-east-1") - self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) - - @patch("botocore.session.Session") - def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): - """Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter - if an invalid OTLP CloudWatch endpoint is set""" - for bad_endpoint in self.invalid_otlp_tracing_endpoints: - with self.subTest(endpoint=bad_endpoint): - with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): - exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) - self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) - - self.assertIsNone(exporter._aws_region) - - @patch("requests.Session.post") - @patch("botocore.auth.SigV4Auth.add_auth") - def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, mock_sigv4_auth, requests_mock): - """Tests that if the OTLP endpoint is not a valid XRay endpoint but the credentials are valid, - SigV4 authentication method is called but fails so NO headers are injected into the existing Session headers.""" - - # Setting the exporter response - mock_response = MagicMock() - mock_response.status_code = 200 - type(mock_response).ok = PropertyMock(return_value=True) - - # Setting the request session headers to make the call to endpoint - mock_session = MagicMock() - mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} - requests_mock.return_value = mock_session - mock_session.post.return_value = mock_response - - # SigV4 mock authentication injection - mock_sigv4_auth.side_effect = self.mock_add_auth - - # Initialize and call exporter - exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) - exporter.export(self.testing_spans) - - # For each invalid CW OTLP endpoint, vdalidate that SigV4 is not injected - for bad_endpoint in self.invalid_otlp_tracing_endpoints: - with self.subTest(endpoint=bad_endpoint): - with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): - - exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) - - self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) - - # Test case, should not have detected a valid region - self.assertIsNone(exporter._aws_region) - - exporter.export(self.testing_spans) - - mock_sigv4_auth.assert_called_once() - - # Verify that SigV4 request headers were not injected - actual_headers = mock_session.headers - self.assertNotIn(AUTHORIZATION_HEADER, actual_headers) - self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) - self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - - requests_mock.assert_called_with( - url=bad_endpoint, - data=ANY, - verify=ANY, - timeout=ANY, - cert=ANY, - ) - - @patch("botocore.session.Session") - @patch("requests.Session") - @patch("botocore.auth.SigV4Auth.add_auth") - @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}) - def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials( - self, mock_sigv4_auth, requests_posts_mock, botocore_mock - ): - """Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned, - SigV4 authentication method is NOT called and is NOT injected into the existing - Session headers.""" - # Setting the exporter response - mock_response = MagicMock() - mock_response.status_code = 200 - type(mock_response).ok = PropertyMock(return_value=True) - - # Setting the request session headers to make the call to endpoint - mock_session = MagicMock() - mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} - requests_posts_mock.return_value = mock_session - mock_session.post.return_value = mock_response - - mock_botocore_session = MagicMock() - botocore_mock.return_value = mock_botocore_session - - # Test case, return None for get credentials - mock_botocore_session.get_credentials.return_value = None - - # Initialize and call exporter - exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) - - # Validate that the region is valid - self.assertEqual(exporter._aws_region, "us-east-1") - - exporter.export(self.testing_spans) - - # Verify SigV4 auth was not called - mock_sigv4_auth.assert_not_called() - - # Verify that SigV4 request headers were properly injected - actual_headers = mock_session.headers - self.assertNotIn(AUTHORIZATION_HEADER, actual_headers) - self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) - self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - - @patch("botocore.session.Session") - @patch("requests.Session") - @patch("botocore.auth.SigV4Auth.add_auth") - @patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}) - def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint( - self, mock_sigv4_auth, requests_posts_mock, botocore_mock - ): - """Tests that if the OTLP endpoint is valid and credentials are valid, - SigV4 authentication method is called and is - injected into the existing Session headers.""" - - # Setting the exporter response - mock_response = MagicMock() - mock_response.status_code = 200 - type(mock_response).ok = PropertyMock(return_value=True) - - # Setting the request session headers to make the call to endpoint - mock_session = MagicMock() - mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} - requests_posts_mock.return_value = mock_session - mock_session.post.return_value = mock_response - - mock_botocore_session = MagicMock() - botocore_mock.return_value = mock_botocore_session - mock_botocore_session.get_credentials.return_value = Credentials( - access_key="test_key", secret_key="test_secret", token="test_token" - ) - - # SigV4 mock authentication injection - mock_sigv4_auth.side_effect = self.mock_add_auth - - # Initialize and call exporter - exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) - exporter.export(self.testing_spans) - - # Verify SigV4 auth was called - mock_sigv4_auth.assert_called_once_with(ANY) - - # Verify that SigV4 request headers were properly injected - actual_headers = mock_session.headers - self.assertIn("Authorization", actual_headers) - self.assertIn("X-Amz-Date", actual_headers) - self.assertIn("X-Amz-Security-Token", actual_headers) - - self.assertEqual(actual_headers[AUTHORIZATION_HEADER], self.expected_auth_header) - self.assertEqual(actual_headers[X_AMZ_DATE_HEADER], self.expected_auth_x_amz_date) - self.assertEqual(actual_headers[X_AMZ_SECURITY_TOKEN_HEADER], self.expected_auth_security_token) - - def validate_exporter_extends_http_span_exporter(self, exporter, endpoint): - self.assertIsInstance(exporter, OTLPSpanExporter) - self.assertEqual(exporter._endpoint, endpoint) - self.assertEqual(exporter._certificate_file, True) - self.assertEqual(exporter._client_certificate_file, None) - self.assertEqual(exporter._client_key_file, None) - self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) - self.assertIs(exporter._compression, DEFAULT_COMPRESSION) - self.assertEqual(exporter._headers, {}) - self.assertIn("User-Agent", exporter._session.headers) - self.assertEqual( - exporter._session.headers.get("Content-Type"), - CONTENT_TYPE, - ) - self.assertEqual(exporter._session.headers.get("User-Agent"), USER_AGENT) - - @staticmethod - def create_span(name="test_span", kind=SpanKind.INTERNAL): - span = _Span( - name=name, - context=SpanContext( - trace_id=0x1234567890ABCDEF, - span_id=0x9876543210, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ), - kind=kind, - ) - return span - - def mock_add_auth(self, request): - request.headers._headers.extend( - [ - (AUTHORIZATION_HEADER, self.expected_auth_header), - (X_AMZ_DATE_HEADER, self.expected_auth_x_amz_date), - (X_AMZ_SECURITY_TOKEN_HEADER, self.expected_auth_security_token), - ] - ) From b7749f8a41e3b04709e32ed1cfde57b4cabde5ec Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:45:45 +0000 Subject: [PATCH 27/39] lint --- .../opentelemetry/distro/patches/_instrumentation_patch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index 82baad48d..b3efcc109 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -2,10 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. import os -import sys from logging import Logger, getLogger -import pkg_resources # noqa: F401 +import pkg_resources # noqa: F401 from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches From e9bf1f1f393df866271fb766feeaa7770d5a3dd0 Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:52:48 +0000 Subject: [PATCH 28/39] linting + test fix --- .../opentelemetry/distro/patches/_instrumentation_patch.py | 2 +- .../opentelemetry/distro/test_otlp_aws_span_exporter.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index b3efcc109..21a8523f3 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -4,7 +4,7 @@ import os from logging import Logger, getLogger -import pkg_resources # noqa: F401 +import pkg_resources # noqa: F401 from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py index 7579b0e80..861938ccf 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py @@ -39,7 +39,6 @@ def setUp(self): ] self.invalid_otlp_tracing_endpoints = [ - "https://xray.bad-region-1.amazonaws.com/v1/traces", "https://xray.us-east-1.amaz.com/v1/traces", "https://logs.us-east-1.amazonaws.com/v1/logs", "https://test-endpoint123.com/test", @@ -89,8 +88,7 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): self.assertEqual(exporter._aws_region, "us-east-1") self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) - @patch("botocore.session.Session") - def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock): + def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self): """Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter if an invalid OTLP CloudWatch endpoint is set""" for bad_endpoint in self.invalid_otlp_tracing_endpoints: @@ -125,7 +123,8 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) exporter.export(self.testing_spans) - # For each invalid CW OTLP endpoint, vdalidate that SigV4 is not injected + # For each invalid CW OTLP endpoint, validate that SigV4 is not injected + self.invalid_otlp_tracing_endpoints.append("https://xray.bad-region-1.amazonaws.com/v1/traces") for bad_endpoint in self.invalid_otlp_tracing_endpoints: with self.subTest(endpoint=bad_endpoint): with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): From 56d747032a02a8da8afd7a9449923dc5e9cab01e Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:56:56 +0000 Subject: [PATCH 29/39] linting fix --- .../opentelemetry/distro/patches/_instrumentation_patch.py | 2 +- .../amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index 21a8523f3..7a623ad7e 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -4,7 +4,7 @@ import os from logging import Logger, getLogger -import pkg_resources # noqa: F401 +import pkg_resources # noqa: F401, W0611 from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py index 861938ccf..e657c9f57 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py @@ -66,7 +66,7 @@ def test_no_botocore_valid_xray_endpoint(self, mock_get_distribution): """Test that exporter defaults when using OTLP CW endpoint without botocore""" def throw_exception(): - raise Exception() + raise ImportError("test error") mock_get_distribution.side_effect = throw_exception From 532ab257d013ce9ae7eb66a0840fe19d0d8462b1 Mon Sep 17 00:00:00 2001 From: liustve Date: Wed, 12 Feb 2025 23:59:21 +0000 Subject: [PATCH 30/39] linting fix --- .../opentelemetry/distro/patches/_instrumentation_patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index 7a623ad7e..46c4e7cd2 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -4,7 +4,7 @@ import os from logging import Logger, getLogger -import pkg_resources # noqa: F401, W0611 +import pkg_resources # pylint: disable=unused-import from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches From 6cb0f55ed1c868f2e32833062fe614fdccc9c8ca Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 00:08:17 +0000 Subject: [PATCH 31/39] fixed test --- .../opentelemetry/distro/test_otlp_aws_span_exporter.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py index e657c9f57..78b424e92 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py @@ -119,10 +119,6 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, # SigV4 mock authentication injection mock_sigv4_auth.side_effect = self.mock_add_auth - # Initialize and call exporter - exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT) - exporter.export(self.testing_spans) - # For each invalid CW OTLP endpoint, validate that SigV4 is not injected self.invalid_otlp_tracing_endpoints.append("https://xray.bad-region-1.amazonaws.com/v1/traces") for bad_endpoint in self.invalid_otlp_tracing_endpoints: From 9a47ab37c2c9de028f37152ad3505d82dfb6d921 Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 00:24:38 +0000 Subject: [PATCH 32/39] lint fix + test fix --- .../distro/patches/_instrumentation_patch.py | 3 ++- .../distro/test_otlp_aws_span_exporter.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index 46c4e7cd2..fb8f087f7 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -4,7 +4,8 @@ import os from logging import Logger, getLogger -import pkg_resources # pylint: disable=unused-import +# pylint: disable=unused-import +import pkg_resources from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py index 78b424e92..29d429b84 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py @@ -101,7 +101,10 @@ def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self): @patch("requests.Session.post") @patch("botocore.auth.SigV4Auth.add_auth") - def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, mock_sigv4_auth, requests_mock): + @patch("botocore.session.Session") + def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( + self, botocore_mock, mock_sigv4_auth, requests_mock + ): """Tests that if the OTLP endpoint is not a valid XRay endpoint but the credentials are valid, SigV4 authentication method is called but fails so NO headers are injected into the existing Session headers.""" @@ -119,6 +122,13 @@ def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, # SigV4 mock authentication injection mock_sigv4_auth.side_effect = self.mock_add_auth + mock_botocore_session = MagicMock() + botocore_mock.return_value = mock_botocore_session + + mock_botocore_session.get_credentials.return_value = Credentials( + access_key="test_key", secret_key="test_secret", token="test_token" + ) + # For each invalid CW OTLP endpoint, validate that SigV4 is not injected self.invalid_otlp_tracing_endpoints.append("https://xray.bad-region-1.amazonaws.com/v1/traces") for bad_endpoint in self.invalid_otlp_tracing_endpoints: From 407bcdcf928f7fa58e5beaa4a9df3795a14eedbc Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 00:26:43 +0000 Subject: [PATCH 33/39] linting fix --- .../opentelemetry/distro/patches/_instrumentation_patch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index fb8f087f7..3a629440d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -5,6 +5,7 @@ from logging import Logger, getLogger # pylint: disable=unused-import +# flake8: noqa: F401 import pkg_resources from amazon.opentelemetry.distro._utils import is_installed From 46d75860b66ff3bffa80571155f572ba0f26296d Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 19:01:06 +0000 Subject: [PATCH 34/39] changed to broader exception --- .../opentelemetry/distro/otlp_aws_span_exporter.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py index e040a1d40..a24b30ab2 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py @@ -34,11 +34,12 @@ def __init__( rsession: Optional[requests.Session] = None, ): - # Represents the region of the CloudWatch OTLP endpoint to send the traces to. - # If the endpoint has been verified to be valid, this should not be None - self._aws_region = None + # Requires botocore to be installed to sign the headers. However, + # some users might not need to use this exporter. In order not conflict + # with existing behavior, we check for botocore before initializing this exporter. + if endpoint and is_xray_otlp_endpoint(endpoint): if is_installed("botocore"): @@ -67,7 +68,11 @@ def __init__( session=rsession, ) + # Overrides upstream's private implementation of _export. All behaviors are + # the same except if the endpoint is an XRay OTLP endpoint, we will sign the request + # with SigV4 in headers before sending it to the endpoint. Otherwise, we will skip signing. def _export(self, serialized_data: bytes): + if self._aws_region: request = self.boto_aws_request.AWSRequest( method="POST", @@ -85,7 +90,7 @@ def _export(self, serialized_data: bytes): signer.add_auth(request) self._session.headers.update(dict(request.headers)) - except self.boto_auth.NoCredentialsError as signing_error: + except Exception as signing_error: # pylint: disable=broad-except _logger.error("Failed to sign request: %s", signing_error) else: From 01e74f82557e4865482d20b785a5987a3209f6ab Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 19:31:42 +0000 Subject: [PATCH 35/39] linting fix --- .../src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py index a24b30ab2..3405c6b7f 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py @@ -90,7 +90,7 @@ def _export(self, serialized_data: bytes): signer.add_auth(request) self._session.headers.update(dict(request.headers)) - except Exception as signing_error: # pylint: disable=broad-except + except Exception as signing_error: # pylint: disable=broad-except _logger.error("Failed to sign request: %s", signing_error) else: From 667ac525f60dec34308414ca8415a26c84c7ab24 Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 20:31:28 +0000 Subject: [PATCH 36/39] removed is xray otlp endpoint validation in the span exporter --- .../src/amazon/opentelemetry/distro/_utils.py | 11 --- .../distro/aws_opentelemetry_configurator.py | 12 ++- .../distro/otlp_aws_span_exporter.py | 30 ++++---- .../distro/test_otlp_aws_span_exporter.py | 76 ------------------- 4 files changed, 27 insertions(+), 102 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index be55a8f3a..ea98635cd 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -9,17 +9,6 @@ _logger: Logger = getLogger(__name__) -XRAY_OTLP_ENDPOINT_PATTERN = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$" - - -def is_xray_otlp_endpoint(otlp_endpoint: str = None) -> bool: - """Is the given endpoint the XRay OTLP endpoint?""" - - if not otlp_endpoint: - return False - - return bool(re.match(XRAY_OTLP_ENDPOINT_PATTERN, otlp_endpoint.lower())) - def is_installed(req: str) -> bool: """Is the given required package installed?""" diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index d6345d07c..d5cea0989 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. import os +import re from logging import Logger, getLogger from typing import ClassVar, Dict, List, Type, Union @@ -10,7 +11,6 @@ from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute -from amazon.opentelemetry.distro._utils import is_xray_otlp_endpoint from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import ( AttributePropagatingSpanProcessorBuilder, @@ -83,6 +83,7 @@ OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED_CONFIG = "OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED" SYSTEM_METRICS_INSTRUMENTATION_SCOPE_NAME = "opentelemetry.instrumentation.system_metrics" OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" +XRAY_OTLP_ENDPOINT_PATTERN = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$" # UDP package size is not larger than 64KB LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10 @@ -448,6 +449,15 @@ def _is_lambda_environment(): return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ +def is_xray_otlp_endpoint(otlp_endpoint: str = None) -> bool: + """Is the given endpoint the XRay OTLP endpoint?""" + + if not otlp_endpoint: + return False + + return bool(re.match(XRAY_OTLP_ENDPOINT_PATTERN, otlp_endpoint.lower())) + + def _get_metric_export_interval(): export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL)) _logger.debug("Span Metrics export interval: %s", export_interval_millis) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py index 3405c6b7f..a43b56aab 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py @@ -5,7 +5,7 @@ import requests -from amazon.opentelemetry.distro._utils import is_installed, is_xray_otlp_endpoint +from amazon.opentelemetry.distro._utils import is_installed from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter @@ -40,22 +40,24 @@ def __init__( # some users might not need to use this exporter. In order not conflict # with existing behavior, we check for botocore before initializing this exporter. - if endpoint and is_xray_otlp_endpoint(endpoint): + if endpoint and is_installed("botocore"): + # pylint: disable=import-outside-toplevel + from botocore import auth, awsrequest, session - if is_installed("botocore"): - # pylint: disable=import-outside-toplevel - from botocore import auth, awsrequest, session + self.boto_auth = auth + self.boto_aws_request = awsrequest + self.boto_session = session.Session() - self.boto_auth = auth - self.boto_aws_request = awsrequest - self.boto_session = session.Session() - self._aws_region = endpoint.split(".")[1] + # Assumes only valid endpoints passed are of XRay OTLP format. + # The only usecase for this class would be for ADOT Python Auto Instrumentation and that already validates + # the endpoint to be an XRay OTLP endpoint. + self._aws_region = endpoint.split(".")[1] - else: - _logger.error( - "botocore is required to export traces to %s. Please install it using `pip install botocore`", - endpoint, - ) + else: + _logger.error( + "botocore is required to export traces to %s. Please install it using `pip install botocore`", + endpoint, + ) super().__init__( endpoint=endpoint, diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py index 29d429b84..b0222bb7c 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py @@ -38,15 +38,6 @@ def setUp(self): self.create_span("test_span5", SpanKind.CONSUMER), ] - self.invalid_otlp_tracing_endpoints = [ - "https://xray.us-east-1.amaz.com/v1/traces", - "https://logs.us-east-1.amazonaws.com/v1/logs", - "https://test-endpoint123.com/test", - "xray.us-east-1.amazonaws.com/v1/traces", - "https://test-endpoint123.com/test https://xray.us-east-1.amazonaws.com/v1/traces", - "https://xray.us-east-1.amazonaws.com/v1/tracesssda", - ] - self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request" self.expected_auth_x_amz_date = "some_date" self.expected_auth_security_token = "test_token" @@ -88,73 +79,6 @@ def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock): self.assertEqual(exporter._aws_region, "us-east-1") self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT) - def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self): - """Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter - if an invalid OTLP CloudWatch endpoint is set""" - for bad_endpoint in self.invalid_otlp_tracing_endpoints: - with self.subTest(endpoint=bad_endpoint): - with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): - exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) - self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) - - self.assertIsNone(exporter._aws_region) - - @patch("requests.Session.post") - @patch("botocore.auth.SigV4Auth.add_auth") - @patch("botocore.session.Session") - def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint( - self, botocore_mock, mock_sigv4_auth, requests_mock - ): - """Tests that if the OTLP endpoint is not a valid XRay endpoint but the credentials are valid, - SigV4 authentication method is called but fails so NO headers are injected into the existing Session headers.""" - - # Setting the exporter response - mock_response = MagicMock() - mock_response.status_code = 200 - type(mock_response).ok = PropertyMock(return_value=True) - - # Setting the request session headers to make the call to endpoint - mock_session = MagicMock() - mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE} - requests_mock.return_value = mock_session - mock_session.post.return_value = mock_response - - # SigV4 mock authentication injection - mock_sigv4_auth.side_effect = self.mock_add_auth - - mock_botocore_session = MagicMock() - botocore_mock.return_value = mock_botocore_session - - mock_botocore_session.get_credentials.return_value = Credentials( - access_key="test_key", secret_key="test_secret", token="test_token" - ) - - # For each invalid CW OTLP endpoint, validate that SigV4 is not injected - self.invalid_otlp_tracing_endpoints.append("https://xray.bad-region-1.amazonaws.com/v1/traces") - for bad_endpoint in self.invalid_otlp_tracing_endpoints: - with self.subTest(endpoint=bad_endpoint): - with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}): - - exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint) - - self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint) - - exporter.export(self.testing_spans) - - # Verify that SigV4 request headers were not injected - actual_headers = mock_session.headers - self.assertNotIn(AUTHORIZATION_HEADER, actual_headers) - self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers) - self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) - - requests_mock.assert_called_with( - url=bad_endpoint, - data=ANY, - verify=ANY, - timeout=ANY, - cert=ANY, - ) - @patch("botocore.session.Session") @patch("requests.Session") @patch("botocore.auth.SigV4Auth.add_auth") From 1b330127f1dbb114e5920c63fa0fb326987952d1 Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 22:26:32 +0000 Subject: [PATCH 37/39] linting fix --- .../src/amazon/opentelemetry/distro/_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index ea98635cd..847f50fb1 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -1,7 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import re import sys from logging import Logger, getLogger From c7d84105aded0d5d46eef246eb6bd11c98bed7cf Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 22:34:47 +0000 Subject: [PATCH 38/39] removed unused import --- .../opentelemetry/distro/patches/_instrumentation_patch.py | 4 ---- .../amazon/opentelemetry/distro/test_instrumentation_patch.py | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py index 3a629440d..9a5f4974b 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py @@ -4,10 +4,6 @@ import os from logging import Logger, getLogger -# pylint: disable=unused-import -# flake8: noqa: F401 -import pkg_resources - from amazon.opentelemetry.distro._utils import is_installed from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index 209c5d1bd..87e6c4810 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -38,9 +38,7 @@ _LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" # Patch names -GET_DISTRIBUTION_PATCH: str = ( - "amazon.opentelemetry.distro.patches._instrumentation_patch.pkg_resources.get_distribution" -) +GET_DISTRIBUTION_PATCH: str = "amazon.opentelemetry.distro._utils.pkg_resources.get_distribution" class TestInstrumentationPatch(TestCase): From 6cf44bdbf5713d8fb125bd45539fb6d0276632d5 Mon Sep 17 00:00:00 2001 From: liustve Date: Thu, 13 Feb 2025 22:45:27 +0000 Subject: [PATCH 39/39] removed validation for aws region --- .../distro/otlp_aws_span_exporter.py | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py index a43b56aab..215b99ded 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py @@ -74,28 +74,23 @@ def __init__( # the same except if the endpoint is an XRay OTLP endpoint, we will sign the request # with SigV4 in headers before sending it to the endpoint. Otherwise, we will skip signing. def _export(self, serialized_data: bytes): + request = self.boto_aws_request.AWSRequest( + method="POST", + url=self._endpoint, + data=serialized_data, + headers={"Content-Type": "application/x-protobuf"}, + ) - if self._aws_region: - request = self.boto_aws_request.AWSRequest( - method="POST", - url=self._endpoint, - data=serialized_data, - headers={"Content-Type": "application/x-protobuf"}, - ) - - credentials = self.boto_session.get_credentials() - - if credentials is not None: - signer = self.boto_auth.SigV4Auth(credentials, AWS_SERVICE, self._aws_region) + credentials = self.boto_session.get_credentials() - try: - signer.add_auth(request) - self._session.headers.update(dict(request.headers)) + if credentials is not None: + signer = self.boto_auth.SigV4Auth(credentials, AWS_SERVICE, self._aws_region) - except Exception as signing_error: # pylint: disable=broad-except - _logger.error("Failed to sign request: %s", signing_error) + try: + signer.add_auth(request) + self._session.headers.update(dict(request.headers)) - else: - _logger.error("Failed to get credentials to export span to OTLP CloudWatch endpoint") + except Exception as signing_error: # pylint: disable=broad-except + _logger.error("Failed to sign request: %s", signing_error) return super()._export(serialized_data)