diff --git a/Dockerfile b/Dockerfile index d2f4ed6c0..d946b3695 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ RUN sed -i "/opentelemetry-exporter-otlp-proto-grpc/d" ./aws-opentelemetry-distr RUN mkdir workspace && pip install setuptools==75.2.0 urllib3==2.2.3 --target workspace ./aws-opentelemetry-distro # Stage 2: Build the cp-utility binary -FROM public.ecr.aws/docker/library/rust:1.81 as builder +FROM public.ecr.aws/docker/library/rust:1.82 as builder WORKDIR /usr/src/cp-utility COPY ./tools/cp-utility . 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 16820721c..e4be93d99 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 @@ -3,7 +3,7 @@ # Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. import os import re -from logging import Logger, getLogger +from logging import NOTSET, Logger, getLogger from typing import ClassVar, Dict, List, Type, Union from importlib_metadata import version @@ -21,11 +21,14 @@ AwsMetricAttributesSpanExporterBuilder, ) from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder -from amazon.opentelemetry.distro.otlp_aws_span_exporter import OTLPAwsSpanExporter +from amazon.opentelemetry.distro.exporter.otlp.aws.logs.otlp_aws_logs_exporter import OTLPAwsLogExporter +from amazon.opentelemetry.distro.exporter.otlp.aws.traces.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 from amazon.opentelemetry.distro.scope_based_filtering_view import ScopeBasedRetainingView +from opentelemetry._logs import set_logger_provider +from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.metrics import set_meter_provider @@ -36,9 +39,10 @@ _import_exporters, _import_id_generator, _import_sampler, - _init_logging, _OTelSDKConfigurator, ) +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, @@ -84,7 +88,15 @@ 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$" +OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" +OTEL_EXPORTER_OTLP_LOGS_HEADERS = "OTEL_EXPORTER_OTLP_LOGS_HEADERS" + +AWS_TRACES_OTLP_ENDPOINT_PATTERN = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$" +AWS_LOGS_OTLP_ENDPOINT_PATTERN = r"https://logs\.([a-z0-9-]+)\.amazonaws\.com/v1/logs$" + +AWS_OTLP_LOGS_GROUP_HEADER = "x-aws-log-group" +AWS_OTLP_LOGS_STREAM_HEADER = "x-aws-log-stream" + # UDP package size is not larger than 64KB LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10 @@ -160,6 +172,29 @@ def _initialize_components(): _init_logging(log_exporters, resource) +def _init_logging( + exporters: Dict[str, Type[LogExporter]], + resource: Resource = None, +): + + # Provides a default OTLP log exporter when none is specified. + # This is the behavior for the logs exporters for other languages. + if not exporters: + exporters = {"otlp": OTLPLogExporter} + + provider = LoggerProvider(resource=resource) + set_logger_provider(provider) + + for _, exporter_class in exporters.items(): + exporter_args: Dict[str, any] = {} + log_exporter = _customize_logs_exporter(exporter_class(**exporter_args), resource) + provider.add_log_record_processor(BatchLogRecordProcessor(exporter=log_exporter)) + + handler = LoggingHandler(level=NOTSET, logger_provider=provider) + + getLogger().addHandler(handler) + + def _init_tracing( exporters: Dict[str, Type[SpanExporter]], id_generator: IdGenerator = None, @@ -177,7 +212,7 @@ def _init_tracing( for _, exporter_class in exporters.items(): exporter_args: Dict[str, any] = {} span_exporter: SpanExporter = exporter_class(**exporter_args) - span_exporter = _customize_exporter(span_exporter, resource) + span_exporter = _customize_span_exporter(span_exporter, resource) trace_provider.add_span_processor( BatchSpanProcessor(span_exporter=span_exporter, max_export_batch_size=_span_export_batch_size()) ) @@ -312,19 +347,19 @@ def _customize_sampler(sampler: Sampler) -> Sampler: return AlwaysRecordSampler(sampler) -def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter: +def _customize_span_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter: + traces_endpoint = os.environ.get(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 is_xray_otlp_endpoint(os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)): - # TODO: Change this url once doc writer has added a section for using SigV4 without collector - _logger.info("Detected using AWS OTLP XRay Endpoint.") + if _is_aws_otlp_endpoint(traces_endpoint, "xray"): + _logger.info("Detected using AWS OTLP Traces Endpoint.") if isinstance(span_exporter, OTLPSpanExporter): - span_exporter = OTLPAwsSpanExporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) + span_exporter = OTLPAwsSpanExporter(endpoint=traces_endpoint) else: _logger.warning( @@ -338,6 +373,26 @@ def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> Span return AwsMetricAttributesSpanExporterBuilder(span_exporter, resource).build() +def _customize_logs_exporter(log_exporter: LogExporter, resource: Resource) -> LogExporter: + logs_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) + + if _is_aws_otlp_endpoint(logs_endpoint, "logs"): + _logger.info("Detected using AWS OTLP Logs Endpoint.") + + if isinstance(log_exporter, OTLPLogExporter) and _validate_logs_headers(): + # Setting default compression mode to Gzip as this is the behavior in upstream's + # collector otlp http exporter: + # https://github.com/open-telemetry/opentelemetry-collector/tree/main/exporter/otlphttpexporter + return OTLPAwsLogExporter(endpoint=logs_endpoint) + + _logger.warning( + "Improper configuration see: please export/set " + "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf and OTEL_LOGS_EXPORTER=otlp" + ) + + return log_exporter + + def _customize_span_processors(provider: TracerProvider, resource: Resource) -> None: # Add LambdaSpanProcessor to list of processors regardless of application signals. if _is_lambda_environment(): @@ -458,12 +513,48 @@ 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?""" +def _is_aws_otlp_endpoint(otlp_endpoint: str = None, service: str = "xray") -> bool: + """Is the given endpoint an AWS OTLP endpoint?""" + + pattern = AWS_TRACES_OTLP_ENDPOINT_PATTERN if service == "xray" else AWS_LOGS_OTLP_ENDPOINT_PATTERN + if not otlp_endpoint: return False - return bool(re.match(XRAY_OTLP_ENDPOINT_PATTERN, otlp_endpoint.lower())) + return bool(re.match(pattern, otlp_endpoint.lower())) + + +def _validate_logs_headers() -> bool: + """Checks if x-aws-log-group and x-aws-log-stream are present in the headers in order to send logs to + AWS OTLP Logs endpoint.""" + + logs_headers = os.environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS) + + if not logs_headers: + _logger.warning( + "Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS " + "to include x-aws-log-group and x-aws-log-stream" + ) + return False + + filtered_log_headers_count = 0 + + for pair in logs_headers.split(","): + if "=" in pair: + split = pair.split("=", 1) + key = split[0] + value = split[1] + if key in (AWS_OTLP_LOGS_GROUP_HEADER, AWS_OTLP_LOGS_STREAM_HEADER) and value: + filtered_log_headers_count += 1 + + if filtered_log_headers_count != 2: + _logger.warning( + "Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS " + "to have values for x-aws-log-group and x-aws-log-stream" + ) + return False + + return True def _get_metric_export_interval(): diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/common/aws_auth_session.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/common/aws_auth_session.py new file mode 100644 index 000000000..2c383592b --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/common/aws_auth_session.py @@ -0,0 +1,89 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging + +import requests + +from amazon.opentelemetry.distro._utils import is_installed + +_logger = logging.getLogger(__name__) + + +class AwsAuthSession(requests.Session): + """ + A custom requests Session that adds AWS SigV4 authentication to HTTP requests. + + This class extends the standard requests.Session to automatically sign requests + with AWS Signature Version 4 (SigV4) authentication. It's specifically designed + for use with the OpenTelemetry Logs and Traces exporters that send data to AWS OTLP endpoints: + X-Ray (traces) and CloudWatch Logs. + + The session requires botocore to be installed for signing headers. If botocore + is not available, the session will fall back to standard unauthenticated requests + and log an error message. + + Usage: + session = AwsAuthSession(aws_region="us-west-2", service="logs") + response = session.request("POST", "https://logs.us-west-2.amazonaws.com/v1/logs", + data=payload, headers=headers) + + Args: + aws_region (str): The AWS region to use for signing (e.g., "us-east-1") + service (str): The AWS service name for signing (e.g., "logs" or "xray") + """ + + def __init__(self, aws_region, service): + + self._has_required_dependencies = False + + # Requires botocore to be installed to sign the headers. However, + # some users might not need to use this authenticator. In order not conflict + # with existing behavior, we check for botocore before initializing this exporter. + + if aws_region and service and 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 = aws_region + self._service = service + self._has_required_dependencies = True + + else: + _logger.error( + "botocore is required to enable SigV4 Authentication. Please install it using `pip install botocore`", + ) + + super().__init__() + + def request(self, method, url, *args, data=None, headers=None, **kwargs): + if self._has_required_dependencies: + + credentials = self._boto_session.get_credentials() + + if credentials is not None: + signer = self._boto_auth.SigV4Auth(credentials, self._service, self._aws_region) + + request = self._boto_aws_request.AWSRequest( + method="POST", + url=url, + data=data, + headers={"Content-Type": "application/x-protobuf"}, + ) + + try: + signer.add_auth(request) + + if headers is None: + headers = {} + + headers.update(dict(request.headers)) + + except Exception as signing_error: # pylint: disable=broad-except + _logger.error("Failed to sign request: %s", signing_error) + + return super().request(method=method, url=url, *args, data=data, headers=headers, **kwargs) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/logs/otlp_aws_logs_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/logs/otlp_aws_logs_exporter.py new file mode 100644 index 000000000..048632c06 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/logs/otlp_aws_logs_exporter.py @@ -0,0 +1,36 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, Optional + +from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter + + +class OTLPAwsLogExporter(OTLPLogExporter): + 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, + ): + self._aws_region = None + + if endpoint: + self._aws_region = endpoint.split(".")[1] + + OTLPLogExporter.__init__( + self, + endpoint, + certificate_file, + client_key_file, + client_certificate_file, + headers, + timeout, + compression=Compression.Gzip, + session=AwsAuthSession(aws_region=self._aws_region, service="logs"), + ) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/traces/otlp_aws_span_exporter.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/traces/otlp_aws_span_exporter.py new file mode 100644 index 000000000..5fd5d744d --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/traces/otlp_aws_span_exporter.py @@ -0,0 +1,37 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, Optional + +from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + + +class OTLPAwsSpanExporter(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, + ): + self._aws_region = None + + if endpoint: + self._aws_region = endpoint.split(".")[1] + + OTLPSpanExporter.__init__( + self, + endpoint, + certificate_file, + client_key_file, + client_certificate_file, + headers, + timeout, + compression, + session=AwsAuthSession(aws_region=self._aws_region, service="xray"), + ) 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 deleted file mode 100644 index 7c00b51a8..000000000 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/otlp_aws_span_exporter.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -import logging -from typing import Dict, Optional - -import requests - -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 - -AWS_SERVICE = "xray" -_logger = logging.getLogger(__name__) - - -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. - - https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html - """ - - 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, - rsession: Optional[requests.Session] = None, - ): - - self._aws_region = None - self._has_required_dependencies = False - # 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_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() - - # 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] - self._has_required_dependencies = True - - else: - _logger.error( - "botocore is required to export traces to %s. Please install it using `pip install botocore`", - 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=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._has_required_dependencies: - 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) - - try: - signer.add_auth(request) - self._session.headers.update(dict(request.headers)) - - except Exception as signing_error: # pylint: disable=broad-except - _logger.error("Failed to sign request: %s", signing_error) - else: - _logger.debug("botocore is not installed. Failed to sign request to export traces to: %s", self._endpoint) - - return super()._export(serialized_data) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py new file mode 100644 index 000000000..e0c62b89d --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py @@ -0,0 +1,63 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from unittest import TestCase +from unittest.mock import patch + +import requests +from botocore.credentials import Credentials + +from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession + +AWS_OTLP_TRACES_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces" +AWS_OTLP_LOGS_ENDPOINT = "https://logs.us-east-1.amazonaws.com/v1/logs" + +AUTHORIZATION_HEADER = "Authorization" +X_AMZ_DATE_HEADER = "X-Amz-Date" +X_AMZ_SECURITY_TOKEN_HEADER = "X-Amz-Security-Token" + +mock_credentials = Credentials(access_key="test_access_key", secret_key="test_secret_key", token="test_session_token") + + +class TestAwsAuthSession(TestCase): + @patch("pkg_resources.get_distribution", side_effect=ImportError("test error")) + @patch.dict("sys.modules", {"botocore": None}, clear=False) + @patch("requests.Session.request", return_value=requests.Response()) + def test_aws_auth_session_no_botocore(self, _, __): + """Tests that aws_auth_session will not inject SigV4 Headers if botocore is not installed.""" + + session = AwsAuthSession("us-east-1", "xray") + actual_headers = {"test": "test"} + + session.request("POST", AWS_OTLP_TRACES_ENDPOINT, data="", headers=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) + + @patch("requests.Session.request", return_value=requests.Response()) + @patch("botocore.session.Session.get_credentials", return_value=None) + def test_aws_auth_session_no_credentials(self, _, __): + """Tests that aws_auth_session will not inject SigV4 Headers if retrieving credentials returns None.""" + + session = AwsAuthSession("us-east-1", "xray") + actual_headers = {"test": "test"} + + session.request("POST", AWS_OTLP_TRACES_ENDPOINT, data="", headers=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) + + @patch("requests.Session.request", return_value=requests.Response()) + @patch("botocore.session.Session.get_credentials", return_value=mock_credentials) + def test_aws_auth_session(self, _, __): + """Tests that aws_auth_session will inject SigV4 Headers if botocore is installed.""" + + session = AwsAuthSession("us-east-1", "xray") + actual_headers = {"test": "test"} + + session.request("POST", AWS_OTLP_TRACES_ENDPOINT, data="", headers=actual_headers) + + self.assertIn(AUTHORIZATION_HEADER, actual_headers) + self.assertIn(X_AMZ_DATE_HEADER, actual_headers) + self.assertIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py index 432bdde09..9df1b81ff 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py @@ -5,6 +5,8 @@ from unittest import TestCase from unittest.mock import MagicMock, patch +from requests import Session + from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler from amazon.opentelemetry.distro.attribute_propagating_span_processor import AttributePropagatingSpanProcessor from amazon.opentelemetry.distro.aws_batch_unsampled_span_processor import BatchUnsampledSpanProcessor @@ -12,14 +14,19 @@ from amazon.opentelemetry.distro.aws_metric_attributes_span_exporter import AwsMetricAttributesSpanExporter from amazon.opentelemetry.distro.aws_opentelemetry_configurator import ( LAMBDA_SPAN_EXPORT_BATCH_SIZE, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, ApplicationSignalsExporterProvider, AwsOpenTelemetryConfigurator, _custom_import_sampler, - _customize_exporter, + _customize_logs_exporter, _customize_metric_exporters, _customize_sampler, + _customize_span_exporter, _customize_span_processors, _export_unsampled_span_for_lambda, + _init_logging, _is_application_signals_enabled, _is_application_signals_runtime_enabled, _is_defer_to_workers_enabled, @@ -27,13 +34,20 @@ ) from amazon.opentelemetry.distro.aws_opentelemetry_distro import AwsOpenTelemetryDistro from amazon.opentelemetry.distro.aws_span_metrics_processor import AwsSpanMetricsProcessor +from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession +from amazon.opentelemetry.distro.exporter.otlp.aws.logs.otlp_aws_logs_exporter import OTLPAwsLogExporter +from amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter import OTLPAwsSpanExporter from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter from amazon.opentelemetry.distro.sampler._aws_xray_sampling_client import _AwsXRaySamplingClient from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import _AwsXRayRemoteSampler from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader from opentelemetry.environment_variables import OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import OTLPMetricExporterMixin +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter as OTLPGrpcLogExporter from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter as OTLPGrpcOTLPMetricExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as OTLPGrpcSpanExporter +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG @@ -275,14 +289,14 @@ def test_customize_sampler(self): self.assertIsInstance(customized_sampler, AlwaysRecordSampler) self.assertEqual(mock_sampler, customized_sampler._root_sampler) - def test_customize_exporter(self): + def test_customize_span_exporter(self): mock_exporter: SpanExporter = MagicMock(spec=OTLPSpanExporter) - customized_exporter: SpanExporter = _customize_exporter(mock_exporter, Resource.get_empty()) + customized_exporter: SpanExporter = _customize_span_exporter(mock_exporter, Resource.get_empty()) self.assertEqual(mock_exporter, customized_exporter) os.environ.setdefault("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "True") os.environ.setdefault("OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", "False") - customized_exporter = _customize_exporter(mock_exporter, Resource.get_empty()) + customized_exporter = _customize_span_exporter(mock_exporter, Resource.get_empty()) self.assertNotEqual(mock_exporter, customized_exporter) self.assertIsInstance(customized_exporter, AwsMetricAttributesSpanExporter) self.assertEqual(mock_exporter, customized_exporter._delegate) @@ -291,12 +305,244 @@ def test_customize_exporter(self): os.environ.setdefault("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "True") os.environ.setdefault("OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", "False") os.environ.setdefault("AWS_LAMBDA_FUNCTION_NAME", "myLambdaFunc") - customized_exporter = _customize_exporter(mock_exporter, Resource.get_empty()) + customized_exporter = _customize_span_exporter(mock_exporter, Resource.get_empty()) self.assertNotEqual(mock_exporter, customized_exporter) self.assertIsInstance(customized_exporter, AwsMetricAttributesSpanExporter) self.assertIsInstance(customized_exporter._delegate, OTLPUdpSpanExporter) os.environ.pop("AWS_LAMBDA_FUNCTION_NAME", None) + def test_customize_span_exporter_sigv4(self): + + traces_good_endpoints = [ + "https://xray.us-east-1.amazonaws.com/v1/traces", + "https://XRAY.US-EAST-1.AMAZONAWS.COM/V1/TRACES", + "https://xray.us-east-1.amazonaws.com/v1/traces", + "https://XRAY.US-EAST-1.amazonaws.com/v1/traces", + "https://xray.US-EAST-1.AMAZONAWS.com/v1/traces", + "https://Xray.Us-East-1.amazonaws.com/v1/traces", + "https://xRAY.us-EAST-1.amazonaws.com/v1/traces", + "https://XRAY.us-EAST-1.AMAZONAWS.com/v1/TRACES", + "https://xray.US-EAST-1.amazonaws.com/V1/Traces", + "https://xray.us-east-1.AMAZONAWS.COM/v1/traces", + "https://XrAy.Us-EaSt-1.AmAzOnAwS.cOm/V1/TrAcEs", + "https://xray.US-EAST-1.amazonaws.com/v1/traces", + "https://xray.us-east-1.amazonaws.com/V1/TRACES", + "https://XRAY.US-EAST-1.AMAZONAWS.COM/v1/traces", + "https://xray.us-east-1.AMAZONAWS.COM/V1/traces", + ] + + traces_bad_endpoints = [ + "http://localhost:4318/v1/traces", + "http://xray.us-east-1.amazonaws.com/v1/traces", + "ftp://xray.us-east-1.amazonaws.com/v1/traces", + "https://ray.us-east-1.amazonaws.com/v1/traces", + "https://xra.us-east-1.amazonaws.com/v1/traces", + "https://x-ray.us-east-1.amazonaws.com/v1/traces", + "https://xray.amazonaws.com/v1/traces", + "https://xray.us-east-1.amazon.com/v1/traces", + "https://xray.us-east-1.aws.com/v1/traces", + "https://xray.us_east_1.amazonaws.com/v1/traces", + "https://xray.us.east.1.amazonaws.com/v1/traces", + "https://xray..amazonaws.com/v1/traces", + "https://xray.us-east-1.amazonaws.com/traces", + "https://xray.us-east-1.amazonaws.com/v2/traces", + "https://xray.us-east-1.amazonaws.com/v1/trace", + "https://xray.us-east-1.amazonaws.com/v1/traces/", + "https://xray.us-east-1.amazonaws.com//v1/traces", + "https://xray.us-east-1.amazonaws.com/v1//traces", + "https://xray.us-east-1.amazonaws.com/v1/traces?param=value", + "https://xray.us-east-1.amazonaws.com/v1/traces#fragment", + "https://xray.us-east-1.amazonaws.com:443/v1/traces", + "https:/xray.us-east-1.amazonaws.com/v1/traces", + "https:://xray.us-east-1.amazonaws.com/v1/traces", + ] + + good_configs = [] + bad_configs = [] + + for endpoint in traces_good_endpoints: + config = { + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: endpoint, + } + + good_configs.append(config) + + for endpoint in traces_bad_endpoints: + config = { + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: endpoint, + } + + bad_configs.append(config) + + for config in good_configs: + self.customize_exporter_test( + config, + _customize_span_exporter, + OTLPSpanExporter(), + OTLPAwsSpanExporter, + AwsAuthSession, + Compression.NoCompression, + ) + + for config in bad_configs: + self.customize_exporter_test( + config, + _customize_span_exporter, + OTLPSpanExporter(), + OTLPSpanExporter, + Session, + Compression.NoCompression, + ) + + self.assertIsInstance( + _customize_span_exporter(OTLPGrpcSpanExporter(), Resource.get_empty()), OTLPGrpcSpanExporter + ) + + def test_customize_logs_exporter_sigv4(self): + logs_good_endpoints = [ + "https://logs.us-east-1.amazonaws.com/v1/logs", + "https://LOGS.US-EAST-1.AMAZONAWS.COM/V1/LOGS", + "https://logs.us-east-1.amazonaws.com/v1/logs", + "https://LOGS.US-EAST-1.amazonaws.com/v1/logs", + "https://logs.US-EAST-1.AMAZONAWS.com/v1/logs", + "https://Logs.Us-East-1.amazonaws.com/v1/logs", + "https://lOGS.us-EAST-1.amazonaws.com/v1/logs", + "https://LOGS.us-EAST-1.AMAZONAWS.com/v1/LOGS", + "https://logs.US-EAST-1.amazonaws.com/V1/Logs", + "https://logs.us-east-1.AMAZONAWS.COM/v1/logs", + "https://LoGs.Us-EaSt-1.AmAzOnAwS.cOm/V1/LoGs", + "https://logs.US-EAST-1.amazonaws.com/v1/logs", + "https://logs.us-east-1.amazonaws.com/V1/LOGS", + "https://LOGS.US-EAST-1.AMAZONAWS.COM/v1/logs", + "https://logs.us-east-1.AMAZONAWS.COM/V1/logs", + ] + + logs_bad_endpoints = [ + "http://localhost:4318/v1/logs", + "http://logs.us-east-1.amazonaws.com/v1/logs", + "ftp://logs.us-east-1.amazonaws.com/v1/logs", + "https://log.us-east-1.amazonaws.com/v1/logs", + "https://logging.us-east-1.amazonaws.com/v1/logs", + "https://cloud-logs.us-east-1.amazonaws.com/v1/logs", + "https://logs.amazonaws.com/v1/logs", + "https://logs.us-east-1.amazon.com/v1/logs", + "https://logs.us-east-1.aws.com/v1/logs", + "https://logs.us_east_1.amazonaws.com/v1/logs", + "https://logs.us.east.1.amazonaws.com/v1/logs", + "https://logs..amazonaws.com/v1/logs", + "https://logs.us-east-1.amazonaws.com/logs", + "https://logs.us-east-1.amazonaws.com/v2/logs", + "https://logs.us-east-1.amazonaws.com/v1/log", + "https://logs.us-east-1.amazonaws.com/v1/logs/", + "https://logs.us-east-1.amazonaws.com//v1/logs", + "https://logs.us-east-1.amazonaws.com/v1//logs", + "https://logs.us-east-1.amazonaws.com/v1/logs?param=value", + "https://logs.us-east-1.amazonaws.com/v1/logs#fragment", + "https://logs.us-east-1.amazonaws.com:443/v1/logs", + "https:/logs.us-east-1.amazonaws.com/v1/logs", + "https:://logs.us-east-1.amazonaws.com/v1/logs", + "https://logs.us-east-1.amazonaws.com/v1/logging", + "https://logs.us-east-1.amazonaws.com/v1/cloudwatchlogs", + "https://logs.us-east-1.amazonaws.com/v1/cwlogs", + ] + + logs_bad_headers = [ + "x-aws-log-group=,x-aws-log-stream=test", + "x-aws-log-stream=test", + "x-aws-log-group=test", + "", + ] + + good_configs = [] + bad_configs = [] + + for endpoint in logs_good_endpoints: + config = { + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: endpoint, + OTEL_EXPORTER_OTLP_LOGS_HEADERS: "x-aws-log-group=test,x-aws-log-stream=test", + } + + good_configs.append(config) + + for endpoint in logs_bad_endpoints: + config = { + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: endpoint, + OTEL_EXPORTER_OTLP_LOGS_HEADERS: "x-aws-log-group=test,x-aws-log-stream=test", + } + + bad_configs.append(config) + + for headers in logs_bad_headers: + config = { + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "https://logs.us-east-1.amazonaws.com/v1/logs", + OTEL_EXPORTER_OTLP_LOGS_HEADERS: headers, + } + + bad_configs.append(config) + + for config in good_configs: + self.customize_exporter_test( + config, + _customize_logs_exporter, + OTLPLogExporter(), + OTLPAwsLogExporter, + AwsAuthSession, + Compression.Gzip, + ) + + for config in bad_configs: + self.customize_exporter_test( + config, _customize_logs_exporter, OTLPLogExporter(), OTLPLogExporter, Session, Compression.NoCompression + ) + + self.assertIsInstance( + _customize_logs_exporter(OTLPGrpcLogExporter(), Resource.get_empty()), OTLPGrpcLogExporter + ) + + # Need to patch all of these to prevent some weird multi-threading error with the LogProvider + @patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator.LoggingHandler", return_value=MagicMock()) + @patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator.getLogger", return_value=MagicMock()) + @patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator._customize_logs_exporter") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator.LoggerProvider", return_value=MagicMock()) + @patch( + "amazon.opentelemetry.distro.aws_opentelemetry_configurator.BatchLogRecordProcessor", return_value=MagicMock() + ) + def test_init_logging( + self, + mock_batch_processor, + mock_logger_provider, + mock_customize_logs_exporter, + mock_get_logger, + mock_logging_handler, + ): + + captured_exporter = None + + def capture_exporter(*args, **kwargs): + nonlocal captured_exporter + result = _customize_logs_exporter(*args, **kwargs) + captured_exporter = result + return result + + mock_customize_logs_exporter.side_effect = capture_exporter + + test_cases = [ + [{"otlp": OTLPLogExporter}, OTLPLogExporter], + [{}, OTLPLogExporter], + [{"grpc": OTLPGrpcLogExporter}, OTLPGrpcLogExporter], + ] + + os.environ[OTEL_EXPORTER_OTLP_LOGS_ENDPOINT] = "https://logs.us-east-1.amazonaws.com/v1/logs" + + for tc in test_cases: + exporter_dict = tc[0] + expected_exporter = tc[1] + _init_logging(exporter_dict, Resource.get_empty()) + + self.assertIsInstance(captured_exporter, expected_exporter) + + os.environ.pop(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) + def test_customize_span_processors(self): mock_tracer_provider: TracerProvider = MagicMock() _customize_span_processors(mock_tracer_provider, Resource.get_empty()) @@ -436,6 +682,27 @@ def test_customize_metric_exporter(self): os.environ.pop("OTEL_METRIC_EXPORT_INTERVAL", None) + def customize_exporter_test( + self, + config, + executor, + default_exporter, + expected_exporter_type, + expected_session, + expected_compression, + ): + for key, value in config.items(): + os.environ[key] = value + + try: + result = executor(default_exporter, Resource.get_empty()) + self.assertIsInstance(result, expected_exporter_type) + self.assertIsInstance(result._session, expected_session) + self.assertEqual(result._compression, expected_compression) + finally: + for key in config.keys(): + os.environ.pop(key, None) + def validate_distro_environ(): tc: TestCase = TestCase() 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 deleted file mode 100644 index b0222bb7c..000000000 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_otlp_aws_span_exporter.py +++ /dev/null @@ -1,211 +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 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.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 ImportError("test error") - - 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") - @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), - ] - )