diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index fa5acf42c..be241885d 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -35,3 +35,32 @@ def is_installed(req: str) -> bool: def is_agent_observability_enabled() -> bool: """Is the Agentic AI monitoring flag set to true?""" return os.environ.get(AGENT_OBSERVABILITY_ENABLED, "false").lower() == "true" + + +def get_aws_region() -> str: + """Get AWS region using botocore session. + + botocore automatically checks in the following priority order: + 1. AWS_REGION environment variable + 2. AWS_DEFAULT_REGION environment variable + 3. AWS CLI config file (~/.aws/config) + 4. EC2 instance metadata service + + Returns: + The AWS region if found, None otherwise. + """ + if is_installed("botocore"): + try: + from botocore import session # pylint: disable=import-outside-toplevel + + botocore_session = session.Session() + if botocore_session.region_name: + return botocore_session.region_name + except (ImportError, AttributeError): + # botocore failed to determine region + pass + + _logger.warning( + "AWS region not found. Please set AWS_REGION environment variable or configure AWS CLI with 'aws configure'." + ) + return None 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 a08374bbe..af90d15a7 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 @@ -102,6 +102,14 @@ # UDP package size is not larger than 64KB LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10 +OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER" +OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER" +OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" +OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" +OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER" +OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" +OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED" + _logger: Logger = getLogger(__name__) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_distro.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_distro.py index 9bca8acd1..cf8109780 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_distro.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_distro.py @@ -4,6 +4,19 @@ import sys from logging import Logger, getLogger +from amazon.opentelemetry.distro._utils import get_aws_region, is_agent_observability_enabled +from amazon.opentelemetry.distro.aws_opentelemetry_configurator import ( + APPLICATION_SIGNALS_ENABLED_CONFIG, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, + OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, + OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, + OTEL_TRACES_EXPORTER, + OTEL_TRACES_SAMPLER, +) from amazon.opentelemetry.distro.patches._instrumentation_patch import apply_instrumentation_patches from opentelemetry.distro import OpenTelemetryDistro from opentelemetry.environment_variables import OTEL_PROPAGATORS, OTEL_PYTHON_ID_GENERATOR @@ -65,5 +78,44 @@ def _configure(self, **kwargs): OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, "base2_exponential_bucket_histogram" ) + if is_agent_observability_enabled(): + # "otlp" is already native OTel default, but we set them here to be explicit + # about intended configuration for agent observability + os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp") + os.environ.setdefault(OTEL_LOGS_EXPORTER, "otlp") + os.environ.setdefault(OTEL_METRICS_EXPORTER, "awsemf") + + # Set GenAI capture content default + os.environ.setdefault(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "true") + + # Set OTLP endpoints with AWS region if not already set + region = get_aws_region() + if region: + os.environ.setdefault( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, f"https://xray.{region}.amazonaws.com/v1/traces" + ) + os.environ.setdefault(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, f"https://logs.{region}.amazonaws.com/v1/logs") + else: + _logger.warning( + "AWS region could not be determined. OTLP endpoints will not be automatically configured. " + "Please set AWS_REGION environment variable or configure OTLP endpoints manually." + ) + + # Set sampler default + os.environ.setdefault(OTEL_TRACES_SAMPLER, "parentbased_always_on") + + # Set disabled instrumentations default + os.environ.setdefault( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, + "http,sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector," + "botocore,boto3,urllib3,requests,starlette", + ) + + # Set logging auto instrumentation default + os.environ.setdefault(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "true") + + # Disable AWS Application Signals by default + os.environ.setdefault(APPLICATION_SIGNALS_ENABLED_CONFIG, "false") + if kwargs.get("apply_patches", True): apply_instrumentation_patches() diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py index 7368a04c8..5a044c5eb 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py @@ -1,12 +1,226 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import os from importlib.metadata import PackageNotFoundError, version from unittest import TestCase +from unittest.mock import patch + +from amazon.opentelemetry.distro.aws_opentelemetry_distro import AwsOpenTelemetryDistro class TestAwsOpenTelemetryDistro(TestCase): + def setUp(self): + # Store original env vars if they exist + self.env_vars_to_restore = {} + self.env_vars_to_check = [ + "OTEL_EXPORTER_OTLP_PROTOCOL", + "OTEL_PROPAGATORS", + "OTEL_PYTHON_ID_GENERATOR", + "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", + "AGENT_OBSERVABILITY_ENABLED", + "OTEL_TRACES_EXPORTER", + "OTEL_LOGS_EXPORTER", + "OTEL_METRICS_EXPORTER", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + "OTEL_TRACES_SAMPLER", + "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", + "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", + "OTEL_AWS_APPLICATION_SIGNALS_ENABLED", + ] + + # First, save all current values + for var in self.env_vars_to_check: + if var in os.environ: + self.env_vars_to_restore[var] = os.environ[var] + + # Then clear ALL of them to ensure clean state + for var in self.env_vars_to_check: + if var in os.environ: + del os.environ[var] + + def tearDown(self): + # Clear all env vars first + for var in self.env_vars_to_check: + if var in os.environ: + del os.environ[var] + + # Then restore original values + for var, value in self.env_vars_to_restore.items(): + os.environ[var] = value + def test_package_available(self): try: version("aws-opentelemetry-distro") except PackageNotFoundError: self.fail("aws-opentelemetry-distro not installed") + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + def test_configure_sets_default_values(self, mock_super_configure, mock_apply_patches): + """Test that _configure sets default environment variables""" + distro = AwsOpenTelemetryDistro() + distro._configure(apply_patches=True) + + # Check that default values are set + self.assertEqual(os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL"), "http/protobuf") + self.assertEqual(os.environ.get("OTEL_PROPAGATORS"), "xray,tracecontext,b3,b3multi") + self.assertEqual(os.environ.get("OTEL_PYTHON_ID_GENERATOR"), "xray") + self.assertEqual( + os.environ.get("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION"), + "base2_exponential_bucket_histogram", + ) + + # Verify super()._configure() was called + mock_super_configure.assert_called_once() + + # Verify patches were applied + mock_apply_patches.assert_called_once() + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + def test_configure_without_patches(self, mock_super_configure, mock_apply_patches): # pylint: disable=no-self-use + """Test that _configure can skip applying patches""" + distro = AwsOpenTelemetryDistro() + distro._configure(apply_patches=False) + + # Verify patches were NOT applied + mock_apply_patches.assert_not_called() + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.get_aws_region") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.is_agent_observability_enabled") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + def test_configure_with_agent_observability_enabled( + self, mock_super_configure, mock_apply_patches, mock_is_agent_observability, mock_get_aws_region + ): + """Test that _configure sets agent observability defaults when enabled""" + mock_is_agent_observability.return_value = True + mock_get_aws_region.return_value = "us-west-2" + + distro = AwsOpenTelemetryDistro() + distro._configure() + + # Check agent observability defaults + self.assertEqual(os.environ.get("OTEL_TRACES_EXPORTER"), "otlp") + self.assertEqual(os.environ.get("OTEL_LOGS_EXPORTER"), "otlp") + self.assertEqual(os.environ.get("OTEL_METRICS_EXPORTER"), "awsemf") + self.assertEqual(os.environ.get("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"), "true") + self.assertEqual( + os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"), "https://xray.us-west-2.amazonaws.com/v1/traces" + ) + self.assertEqual( + os.environ.get("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"), "https://logs.us-west-2.amazonaws.com/v1/logs" + ) + self.assertEqual(os.environ.get("OTEL_TRACES_SAMPLER"), "parentbased_always_on") + self.assertEqual( + os.environ.get("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"), + "http,sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector," + "botocore,boto3,urllib3,requests,starlette", + ) + self.assertEqual(os.environ.get("OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED"), "true") + self.assertEqual(os.environ.get("OTEL_AWS_APPLICATION_SIGNALS_ENABLED"), "false") + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.get_aws_region") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.is_agent_observability_enabled") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + def test_configure_with_agent_observability_no_region( + self, mock_super_configure, mock_apply_patches, mock_is_agent_observability, mock_get_aws_region + ): + """Test that _configure handles missing AWS region gracefully""" + mock_is_agent_observability.return_value = True + mock_get_aws_region.return_value = None # No region found + + distro = AwsOpenTelemetryDistro() + distro._configure() + + # Check that OTLP endpoints are not set when region is not available + self.assertNotIn("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", os.environ) + self.assertNotIn("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", os.environ) + + # But verify that the exporters are still set to otlp (will use default endpoints) + self.assertEqual(os.environ.get("OTEL_TRACES_EXPORTER"), "otlp") + self.assertEqual(os.environ.get("OTEL_LOGS_EXPORTER"), "otlp") + self.assertEqual(os.environ.get("OTEL_METRICS_EXPORTER"), "awsemf") + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.is_agent_observability_enabled") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + def test_configure_with_agent_observability_disabled( + self, mock_super_configure, mock_apply_patches, mock_is_agent_observability + ): + """Test that _configure doesn't set agent observability defaults when disabled""" + mock_is_agent_observability.return_value = False + + distro = AwsOpenTelemetryDistro() + distro._configure() + + # Check that agent observability defaults are not set + self.assertNotIn("OTEL_TRACES_EXPORTER", os.environ) + self.assertNotIn("OTEL_LOGS_EXPORTER", os.environ) + self.assertNotIn("OTEL_METRICS_EXPORTER", os.environ) + self.assertNotIn("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", os.environ) + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.get_aws_region") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.is_agent_observability_enabled") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + def test_configure_preserves_existing_env_vars( + self, mock_super_configure, mock_apply_patches, mock_is_agent_observability, mock_get_aws_region + ): + """Test that _configure doesn't override existing environment variables""" + mock_is_agent_observability.return_value = True + mock_get_aws_region.return_value = "us-east-1" + + # Set existing values + os.environ["OTEL_TRACES_EXPORTER"] = "custom_exporter" + os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = "https://custom.endpoint.com" + + distro = AwsOpenTelemetryDistro() + distro._configure() + + # Check that existing values are preserved + self.assertEqual(os.environ.get("OTEL_TRACES_EXPORTER"), "custom_exporter") + self.assertEqual(os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"), "https://custom.endpoint.com") + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + @patch("os.getcwd") + @patch("sys.path", new_callable=list) + def test_configure_adds_cwd_to_sys_path(self, mock_sys_path, mock_getcwd, mock_super_configure, mock_apply_patches): + """Test that _configure adds current working directory to sys.path""" + mock_getcwd.return_value = "/test/working/directory" + + distro = AwsOpenTelemetryDistro() + distro._configure() + + # Check that cwd was added to sys.path + self.assertIn("/test/working/directory", mock_sys_path) + + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.get_aws_region") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.is_agent_observability_enabled") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.apply_instrumentation_patches") + @patch("amazon.opentelemetry.distro.aws_opentelemetry_distro.OpenTelemetryDistro._configure") + def test_configure_with_agent_observability_endpoints_already_set( + self, mock_super_configure, mock_apply_patches, mock_is_agent_observability, mock_get_aws_region + ): + """Test that user-provided OTLP endpoints are preserved even when region detection fails""" + mock_is_agent_observability.return_value = True + mock_get_aws_region.return_value = None # No region found + + # User has already set custom endpoints + os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = "https://my-custom-traces.example.com" + os.environ["OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"] = "https://my-custom-logs.example.com" + + distro = AwsOpenTelemetryDistro() + distro._configure() + + # Verify that user-provided endpoints are preserved + self.assertEqual(os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"), "https://my-custom-traces.example.com") + self.assertEqual(os.environ.get("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"), "https://my-custom-logs.example.com") + + # And exporters are still set to otlp + self.assertEqual(os.environ.get("OTEL_TRACES_EXPORTER"), "otlp") + self.assertEqual(os.environ.get("OTEL_LOGS_EXPORTER"), "otlp") diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py index 0839aec98..adb690359 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py @@ -4,22 +4,31 @@ import os from importlib.metadata import PackageNotFoundError from unittest import TestCase -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from amazon.opentelemetry.distro._utils import AGENT_OBSERVABILITY_ENABLED, is_agent_observability_enabled, is_installed +from amazon.opentelemetry.distro._utils import ( + AGENT_OBSERVABILITY_ENABLED, + get_aws_region, + is_agent_observability_enabled, + is_installed, +) class TestUtils(TestCase): def setUp(self): # Store original env var if it exists self.original_env = os.environ.get(AGENT_OBSERVABILITY_ENABLED) + # Clear it to ensure clean state + if AGENT_OBSERVABILITY_ENABLED in os.environ: + del os.environ[AGENT_OBSERVABILITY_ENABLED] def tearDown(self): - # Restore original env var + # First clear the env var + if AGENT_OBSERVABILITY_ENABLED in os.environ: + del os.environ[AGENT_OBSERVABILITY_ENABLED] + # Then restore original if it existed if self.original_env is not None: os.environ[AGENT_OBSERVABILITY_ENABLED] = self.original_env - elif AGENT_OBSERVABILITY_ENABLED in os.environ: - del os.environ[AGENT_OBSERVABILITY_ENABLED] def test_is_installed_package_not_found(self): """Test is_installed returns False when package is not found""" @@ -94,3 +103,68 @@ def test_is_agent_observability_enabled_various_values(self): if AGENT_OBSERVABILITY_ENABLED in os.environ: del os.environ[AGENT_OBSERVABILITY_ENABLED] self.assertFalse(is_agent_observability_enabled()) + + def test_get_aws_region_with_botocore(self): + """Test get_aws_region when botocore is available and returns a region""" + with patch("amazon.opentelemetry.distro._utils.is_installed") as mock_is_installed: + mock_is_installed.return_value = True + + # Create a mock botocore session + mock_session_class = MagicMock() + mock_session_instance = MagicMock() + mock_session_instance.region_name = "us-east-1" + mock_session_class.Session.return_value = mock_session_instance + + # Patch the import statement directly in the function + with patch.dict("sys.modules", {"botocore": MagicMock(session=mock_session_class)}): + region = get_aws_region() + self.assertEqual(region, "us-east-1") + + def test_get_aws_region_without_botocore(self): + """Test get_aws_region when botocore is not installed""" + with patch("amazon.opentelemetry.distro._utils.is_installed") as mock_is_installed: + mock_is_installed.return_value = False + + region = get_aws_region() + self.assertIsNone(region) + + def test_get_aws_region_botocore_no_region(self): + """Test get_aws_region when botocore is available but returns no region""" + with patch("amazon.opentelemetry.distro._utils.is_installed") as mock_is_installed: + mock_is_installed.return_value = True + + # Create a mock botocore session with no region + mock_session_class = MagicMock() + mock_session_instance = MagicMock() + mock_session_instance.region_name = None + mock_session_class.Session.return_value = mock_session_instance + + # Patch the import statement directly in the function + with patch.dict("sys.modules", {"botocore": MagicMock(session=mock_session_class)}): + region = get_aws_region() + self.assertIsNone(region) + + def test_get_aws_region_botocore_import_error(self): + """Test get_aws_region when botocore import fails""" + with patch("amazon.opentelemetry.distro._utils.is_installed") as mock_is_installed: + mock_is_installed.return_value = True + + # Mock ImportError when trying to import botocore + with patch.dict("sys.modules", {"botocore": None}): + with patch("builtins.__import__", side_effect=ImportError("Botocore not found")): + region = get_aws_region() + self.assertIsNone(region) + + def test_get_aws_region_botocore_attribute_error(self): + """Test get_aws_region when botocore has attribute errors""" + with patch("amazon.opentelemetry.distro._utils.is_installed") as mock_is_installed: + mock_is_installed.return_value = True + + # Mock the botocore import with AttributeError on Session + mock_session_module = MagicMock() + mock_session_module.Session.side_effect = AttributeError("Session class not found") + + # Patch the import statement directly in the function + with patch.dict("sys.modules", {"botocore": MagicMock(session=mock_session_module)}): + region = get_aws_region() + self.assertIsNone(region)