diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 745a83385f..cc202b460a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -23,7 +23,7 @@ import os from abc import ABC, abstractmethod from os import environ -from typing import Callable, Sequence, Type, Union +from typing import Any, Callable, Mapping, Sequence, Type, Union from typing_extensions import Literal @@ -91,10 +91,20 @@ _logger = logging.getLogger(__name__) +ExporterArgsMap = Mapping[ + Union[ + Type[SpanExporter], + Type[MetricExporter], + Type[MetricReader], + Type[LogExporter], + ], + Mapping[str, Any], +] + def _import_config_components( - selected_components: list[str], entry_point_name: str -) -> Sequence[tuple[str, object]]: + selected_components: Sequence[str], entry_point_name: str +) -> list[tuple[str, Type]]: component_implementations = [] for selected_component in selected_components: @@ -179,7 +189,7 @@ def _get_exporter_entry_point( def _get_exporter_names( signal_type: Literal["traces", "metrics", "logs"], -) -> Sequence[str]: +) -> list[str]: names = environ.get(_EXPORTER_ENV_BY_SIGNAL_TYPE.get(signal_type, "")) if not names or names.lower().strip() == "none": @@ -196,6 +206,7 @@ def _init_tracing( id_generator: IdGenerator | None = None, sampler: Sampler | None = None, resource: Resource | None = None, + exporter_args_map: ExporterArgsMap | None = None, ): provider = TracerProvider( id_generator=id_generator, @@ -204,8 +215,9 @@ def _init_tracing( ) set_tracer_provider(provider) + exporter_args_map = exporter_args_map or {} for _, exporter_class in exporters.items(): - exporter_args = {} + exporter_args = exporter_args_map.get(exporter_class, {}) provider.add_span_processor( BatchSpanProcessor(exporter_class(**exporter_args)) ) @@ -216,12 +228,13 @@ def _init_metrics( str, Union[Type[MetricExporter], Type[MetricReader]] ], resource: Resource | None = None, + exporter_args_map: ExporterArgsMap | None = None, ): metric_readers = [] + exporter_args_map = exporter_args_map or {} for _, exporter_or_reader_class in exporters_or_readers.items(): - exporter_args = {} - + exporter_args = exporter_args_map.get(exporter_or_reader_class, {}) if issubclass(exporter_or_reader_class, MetricReader): metric_readers.append(exporter_or_reader_class(**exporter_args)) else: @@ -239,12 +252,14 @@ def _init_logging( exporters: dict[str, Type[LogExporter]], resource: Resource | None = None, setup_logging_handler: bool = True, + exporter_args_map: ExporterArgsMap | None = None, ): provider = LoggerProvider(resource=resource) set_logger_provider(provider) + exporter_args_map = exporter_args_map or {} for _, exporter_class in exporters.items(): - exporter_args = {} + exporter_args = exporter_args_map.get(exporter_class, {}) provider.add_log_record_processor( BatchLogRecordProcessor(exporter_class(**exporter_args)) ) @@ -331,14 +346,16 @@ def _import_exporters( return trace_exporters, metric_exporters, log_exporters -def _import_sampler_factory(sampler_name: str) -> Callable[[str], Sampler]: +def _import_sampler_factory( + sampler_name: str, +) -> Callable[[float | str | None], Sampler]: _, sampler_impl = _import_config_components( [sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP )[0] return sampler_impl -def _import_sampler(sampler_name: str) -> Sampler | None: +def _import_sampler(sampler_name: str | None) -> Sampler | None: if not sampler_name: return None try: @@ -346,7 +363,7 @@ def _import_sampler(sampler_name: str) -> Sampler | None: arg = None if sampler_name in ("traceidratio", "parentbased_traceidratio"): try: - rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG)) + rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG, "")) except (ValueError, TypeError): _logger.warning( "Could not convert TRACES_SAMPLER_ARG to float. Using default value 1.0." @@ -391,6 +408,7 @@ def _initialize_components( resource_attributes: Attributes | None = None, id_generator: IdGenerator | None = None, setup_logging_handler: bool | None = None, + exporter_args_map: ExporterArgsMap | None = None, ): if trace_exporter_names is None: trace_exporter_names = [] @@ -413,7 +431,7 @@ def _initialize_components( resource_attributes = {} # populate version if using auto-instrumentation if auto_instrumentation_version: - resource_attributes[ResourceAttributes.TELEMETRY_AUTO_VERSION] = ( + resource_attributes[ResourceAttributes.TELEMETRY_AUTO_VERSION] = ( # type: ignore[reportIndexIssue] auto_instrumentation_version ) # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name @@ -425,8 +443,11 @@ def _initialize_components( id_generator=id_generator, sampler=sampler, resource=resource, + exporter_args_map=exporter_args_map, + ) + _init_metrics( + metric_exporters, resource, exporter_args_map=exporter_args_map ) - _init_metrics(metric_exporters, resource) if setup_logging_handler is None: setup_logging_handler = ( os.getenv( @@ -436,7 +457,12 @@ def _initialize_components( .lower() == "true" ) - _init_logging(log_exporters, resource, setup_logging_handler) + _init_logging( + log_exporters, + resource, + setup_logging_handler, + exporter_args_map=exporter_args_map, + ) class _BaseConfigurator(ABC): diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 9fda75b66f..4b9f364d95 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -178,8 +178,9 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: class DummyOTLPMetricExporter: - def __init__(self, *args, **kwargs): + def __init__(self, compression: str | None = None, *args, **kwargs): self.export_called = False + self.compression = compression def export(self, batch): self.export_called = True @@ -202,12 +203,14 @@ def shutdown(self): class OTLPSpanExporter: - pass + def __init__(self, compression: str | None = None, *args, **kwargs): + self.compression = compression class DummyOTLPLogExporter(LogExporter): - def __init__(self, *args, **kwargs): + def __init__(self, compression: str | None = None, *args, **kwargs): self.export_called = False + self.compression = compression def export(self, batch): self.export_called = True @@ -374,6 +377,20 @@ def test_trace_init_otlp(self): "my-otlp-test-service", ) + def test_trace_init_exporter_uses_exporter_args_map(self): + _init_tracing( + {"otlp": OTLPSpanExporter}, + id_generator=RandomIdGenerator(), + exporter_args_map={ + OTLPSpanExporter: {"compression": "gzip"}, + DummyMetricReaderPullExporter: {"compression": "no"}, + }, + ) + + provider = self.set_provider_mock.call_args[0][0] + exporter = provider.processor.exporter + self.assertEqual(exporter.compression, "gzip") + @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) @patch("opentelemetry.sdk._configuration.entry_points") @@ -667,6 +684,20 @@ def test_logging_init_exporter(self): getLogger(__name__).error("hello") self.assertTrue(provider.processor.exporter.export_called) + def test_logging_init_exporter_uses_exporter_args_map(self): + resource = Resource.create({}) + _init_logging( + {"otlp": DummyOTLPLogExporter}, + resource=resource, + exporter_args_map={ + DummyOTLPLogExporter: {"compression": "gzip"}, + DummyOTLPMetricExporter: {"compression": "no"}, + }, + ) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertEqual(provider.processor.exporter.compression, "gzip") + @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, @@ -702,7 +733,9 @@ def test_logging_init_exporter_without_handler_setup(self): def test_logging_init_disable_default(self, logging_mock, tracing_mock): _initialize_components(auto_instrumentation_version="auto-version") self.assertEqual(tracing_mock.call_count, 1) - logging_mock.assert_called_once_with(mock.ANY, mock.ANY, False) + logging_mock.assert_called_once_with( + mock.ANY, mock.ANY, False, exporter_args_map=None + ) @patch.dict( environ, @@ -716,7 +749,9 @@ def test_logging_init_disable_default(self, logging_mock, tracing_mock): def test_logging_init_enable_env(self, logging_mock, tracing_mock): with self.assertLogs(level=WARNING): _initialize_components(auto_instrumentation_version="auto-version") - logging_mock.assert_called_once_with(mock.ANY, mock.ANY, True) + logging_mock.assert_called_once_with( + mock.ANY, mock.ANY, True, exporter_args_map=None + ) self.assertEqual(tracing_mock.call_count, 1) @patch.dict( @@ -799,6 +834,7 @@ def test_initialize_components_kwargs( }, "id_generator": "TEST_GENERATOR", "setup_logging_handler": True, + "exporter_args_map": {1: {"compression": "gzip"}}, } _initialize_components(**kwargs) @@ -832,15 +868,18 @@ def test_initialize_components_kwargs( id_generator="TEST_GENERATOR", sampler="TEST_SAMPLER", resource="TEST_RESOURCE", + exporter_args_map={1: {"compression": "gzip"}}, ) metrics_mock.assert_called_once_with( "TEST_METRICS_EXPORTERS_DICT", "TEST_RESOURCE", + exporter_args_map={1: {"compression": "gzip"}}, ) logging_mock.assert_called_once_with( "TEST_LOG_EXPORTERS_DICT", "TEST_RESOURCE", True, + exporter_args_map={1: {"compression": "gzip"}}, ) def test_basicConfig_works_with_otel_handler(self): @@ -970,6 +1009,20 @@ def test_metrics_init_pull_exporter(self): reader = provider._sdk_config.metric_readers[0] self.assertIsInstance(reader, DummyMetricReaderPullExporter) + def test_metrics_init_exporter_uses_exporter_args_map(self): + resource = Resource.create({}) + _init_metrics( + {"otlp": DummyOTLPMetricExporter}, + resource=resource, + exporter_args_map={ + DummyOTLPMetricExporter: {"compression": "gzip"}, + DummyMetricReaderPullExporter: {"compression": "no"}, + }, + ) + provider = self.set_provider_mock.call_args[0][0] + reader = provider._sdk_config.metric_readers[0] + self.assertEqual(reader.exporter.compression, "gzip") + class TestExporterNames(TestCase): @patch.dict( diff --git a/pyproject.toml b/pyproject.toml index f7bab7fdcf..0f1cd93be4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,6 @@ include = [ exclude = [ "opentelemetry-sdk/tests", - "opentelemetry-sdk/src/opentelemetry/sdk/_configuration", "opentelemetry-sdk/src/opentelemetry/sdk/_events", "opentelemetry-sdk/src/opentelemetry/sdk/_logs", "opentelemetry-sdk/src/opentelemetry/sdk/error_handler",