diff --git a/.changelog/5418.added b/.changelog/5418.added new file mode 100644 index 00000000000..0a957786517 --- /dev/null +++ b/.changelog/5418.added @@ -0,0 +1,8 @@ +`opentelemetry-configuration`: wire the experimental +`tracer_configurator/development`, `meter_configurator/development` and +`logger_configurator/development` fields into `create_tracer_provider`, +`create_meter_provider` and `create_logger_provider`, so per-instrumentation-scope +`enabled` overrides declared in the config file are applied to the provider +(previously these fields were parsed but silently discarded). The logger +`minimum_severity`/`trace_based` fields are not supported by the Python SDK and +are ignored with a warning. diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py index 4214b2f9b76..980bc90c4ec 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py @@ -19,6 +19,12 @@ from opentelemetry.configuration.models import ( BatchLogRecordProcessor as BatchLogRecordProcessorConfig, ) +from opentelemetry.configuration.models import ( + ExperimentalLoggerConfig as LoggerConfigConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalLoggerConfigurator as LoggerConfiguratorConfig, +) from opentelemetry.configuration.models import ( ExperimentalOtlpFileExporter as ExperimentalOtlpFileExporterConfig, ) @@ -41,6 +47,10 @@ SimpleLogRecordProcessor as SimpleLogRecordProcessorConfig, ) from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs._internal import ( + _LoggerConfig, + _RuleBasedLoggerConfigurator, +) from opentelemetry.sdk._logs._internal.export import ( BatchLogRecordProcessor, ConsoleLogRecordExporter, @@ -48,6 +58,7 @@ SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob _logger = logging.getLogger(__name__) @@ -216,6 +227,49 @@ def _create_log_record_processor( ) +def _to_logger_config(config: LoggerConfigConfig | None) -> _LoggerConfig: + """Map an experimental per-logger config to an SDK ``_LoggerConfig``. + + Only ``enabled`` is honored. ``minimum_severity`` and ``trace_based`` are + accepted by the config schema but not supported by the Python SDK + ``_LoggerConfig``; when set, they are ignored with a warning. An absent + ``enabled`` leaves the logger enabled. + """ + if config is None: + return _LoggerConfig.default() + if config.minimum_severity is not None or config.trace_based is not None: + _logger.warning( + "logger_configurator minimum_severity/trace_based are specified in " + "config but are not supported by the Python SDK LoggerProvider; " + "they will be ignored." + ) + if config.enabled is None: + return _LoggerConfig.default() + return _LoggerConfig(is_enabled=config.enabled) + + +def _create_logger_configurator( + config: LoggerConfiguratorConfig, +) -> _RuleBasedLoggerConfigurator: + """Build a rule-based logger configurator from experimental config. + + Each entry in ``loggers`` maps an instrumentation-scope name glob to a + per-logger config; ``default_config`` applies to scopes matching no glob. + Rules are evaluated in order, so earlier entries take precedence. + """ + rules = [ + ( + _scope_name_matches_glob(matcher.name), + _to_logger_config(matcher.config), + ) + for matcher in (config.loggers or []) + ] + return _RuleBasedLoggerConfigurator( + rules=rules, + default_config=_to_logger_config(config.default_config), + ) + + def create_logger_provider( config: LoggerProviderConfig | None, resource: Resource | None = None, @@ -232,7 +286,13 @@ def create_logger_provider( Returns: A configured LoggerProvider. """ - provider = LoggerProvider(resource=resource) + logger_configurator = ( + _create_logger_configurator(config.logger_configurator_development) + if config is not None and config.logger_configurator_development is not None + else None + ) + + provider = LoggerProvider(resource=resource, _logger_configurator=logger_configurator) if config is None: return provider diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py index 98eb1357835..27904c41b2c 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py @@ -25,6 +25,12 @@ from opentelemetry.configuration.models import ( ExemplarFilter as ExemplarFilterConfig, ) +from opentelemetry.configuration.models import ( + ExperimentalMeterConfig as MeterConfigConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalMeterConfigurator as MeterConfiguratorConfig, +) from opentelemetry.configuration.models import ( ExperimentalOtlpFileMetricExporter as ExperimentalOtlpFileMetricExporterConfig, ) @@ -76,6 +82,10 @@ UpDownCounter, _Gauge, ) +from opentelemetry.sdk.metrics._internal import ( + _MeterConfig, + _RuleBasedMeterConfigurator, +) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, ConsoleMetricExporter, @@ -94,6 +104,7 @@ View, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob _logger = logging.getLogger(__name__) @@ -511,6 +522,39 @@ def _create_exemplar_filter( ) +def _to_meter_config(config: MeterConfigConfig | None) -> _MeterConfig: + """Map an experimental per-meter config to an SDK ``_MeterConfig``. + + Only ``enabled`` is honored — it is the sole field the SDK + ``_MeterConfig`` exposes. An absent ``enabled`` leaves the meter enabled. + """ + if config is None or config.enabled is None: + return _MeterConfig.default() + return _MeterConfig(is_enabled=config.enabled) + + +def _create_meter_configurator( + config: MeterConfiguratorConfig, +) -> _RuleBasedMeterConfigurator: + """Build a rule-based meter configurator from experimental config. + + Each entry in ``meters`` maps an instrumentation-scope name glob to a + per-meter config; ``default_config`` applies to scopes matching no glob. + Rules are evaluated in order, so earlier entries take precedence. + """ + rules = [ + ( + _scope_name_matches_glob(matcher.name), + _to_meter_config(matcher.config), + ) + for matcher in (config.meters or []) + ] + return _RuleBasedMeterConfigurator( + rules=rules, + default_config=_to_meter_config(config.default_config), + ) + + def create_meter_provider( config: MeterProviderConfig | None, resource: Resource | None = None, @@ -544,11 +588,18 @@ def create_meter_provider( for view_config in config.views: views.append(_create_view(view_config)) + meter_configurator = ( + _create_meter_configurator(config.meter_configurator_development) + if config is not None and config.meter_configurator_development is not None + else None + ) + return MeterProvider( resource=resource, metric_readers=readers, exemplar_filter=exemplar_filter, # type: ignore[arg-type] views=views, + _meter_configurator=meter_configurator, ) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_tracer_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_tracer_provider.py index 733691dc67d..4168a64e36f 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_tracer_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_tracer_provider.py @@ -28,6 +28,12 @@ from opentelemetry.configuration.models import ( ExperimentalOtlpFileExporter as ExperimentalOtlpFileExporterConfig, ) +from opentelemetry.configuration.models import ( + ExperimentalTracerConfig as TracerConfigConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalTracerConfigurator as TracerConfiguratorConfig, +) from opentelemetry.configuration.models import ( IdGenerator as IdGeneratorConfig, ) @@ -64,6 +70,8 @@ _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT, SpanLimits, TracerProvider, + _RuleBasedTracerConfigurator, + _TracerConfig, ) from opentelemetry.sdk.trace._sampling_experimental import ( ComposableSampler, @@ -98,6 +106,7 @@ Sampler, TraceIdRatioBased, ) +from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob from opentelemetry.trace import SpanKind as TraceSpanKind _logger = logging.getLogger(__name__) @@ -389,6 +398,39 @@ def _create_span_limits(config: SpanLimitsConfig) -> SpanLimits: ) +def _to_tracer_config(config: TracerConfigConfig | None) -> _TracerConfig: + """Map an experimental per-tracer config to an SDK ``_TracerConfig``. + + Only ``enabled`` is honored — it is the sole field the SDK + ``_TracerConfig`` exposes. An absent ``enabled`` leaves the tracer enabled. + """ + if config is None or config.enabled is None: + return _TracerConfig.default() + return _TracerConfig(is_enabled=config.enabled) + + +def _create_tracer_configurator( + config: TracerConfiguratorConfig, +) -> _RuleBasedTracerConfigurator: + """Build a rule-based tracer configurator from experimental config. + + Each entry in ``tracers`` maps an instrumentation-scope name glob to a + per-tracer config; ``default_config`` applies to scopes matching no glob. + Rules are evaluated in order, so earlier entries take precedence. + """ + rules = [ + ( + _scope_name_matches_glob(matcher.name), + _to_tracer_config(matcher.config), + ) + for matcher in (config.tracers or []) + ] + return _RuleBasedTracerConfigurator( + rules=rules, + default_config=_to_tracer_config(config.default_config), + ) + + def create_tracer_provider( config: TracerProviderConfig | None, resource: Resource | None = None, @@ -422,11 +464,18 @@ def create_tracer_provider( ) ) + tracer_configurator = ( + _create_tracer_configurator(config.tracer_configurator_development) + if config is not None and config.tracer_configurator_development is not None + else None + ) + provider = TracerProvider( resource=resource, sampler=sampler, span_limits=span_limits, id_generator=id_generator, + _tracer_configurator=tracer_configurator, ) if config is not None: diff --git a/opentelemetry-configuration/tests/file/data/configurator_config.yaml b/opentelemetry-configuration/tests/file/data/configurator_config.yaml new file mode 100644 index 00000000000..8f6fce78c6d --- /dev/null +++ b/opentelemetry-configuration/tests/file/data/configurator_config.yaml @@ -0,0 +1,37 @@ +file_format: "1.0" +tracer_provider: + processors: + - batch: + exporter: + console: {} + tracer_configurator/development: + default_config: + enabled: true + tracers: + - name: "noisy.*" + config: + enabled: false +meter_provider: + readers: + - periodic: + exporter: + console: {} + meter_configurator/development: + default_config: + enabled: false + meters: + - name: "keep.*" + config: + enabled: true +logger_provider: + processors: + - batch: + exporter: + console: {} + logger_configurator/development: + default_config: + enabled: true + loggers: + - name: "noisy.*" + config: + enabled: false diff --git a/opentelemetry-configuration/tests/file/test_configurator_yaml.py b/opentelemetry-configuration/tests/file/test_configurator_yaml.py new file mode 100644 index 00000000000..b9a64a65b32 --- /dev/null +++ b/opentelemetry-configuration/tests/file/test_configurator_yaml.py @@ -0,0 +1,98 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# Tests access private members of SDK providers to assert wired configuration. +# pylint: disable=protected-access + +import unittest +from pathlib import Path + +from opentelemetry.configuration._logger_provider import ( + create_logger_provider, +) +from opentelemetry.configuration._meter_provider import ( + create_meter_provider, +) +from opentelemetry.configuration._tracer_provider import ( + create_tracer_provider, +) +from opentelemetry.configuration.file import load_config_file +from opentelemetry.configuration.models import ( + ExperimentalLoggerConfig, + ExperimentalLoggerConfigurator, + ExperimentalLoggerMatcherAndConfig, + ExperimentalMeterConfig, + ExperimentalMeterConfigurator, + ExperimentalMeterMatcherAndConfig, + ExperimentalTracerConfig, + ExperimentalTracerConfigurator, + ExperimentalTracerMatcherAndConfig, +) +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + + +class TestConfiguratorYaml(unittest.TestCase): + """Verify the tracer/meter/logger configurator nodes are parsed from YAML + and wired into the providers they configure.""" + + @classmethod + def setUpClass(cls): + config_path = Path(__file__).parent / "data" / "configurator_config.yaml" + cls.config = load_config_file(str(config_path)) + + def test_tracer_configurator_parsed_from_yaml(self): + self.assertEqual( + self.config.tracer_provider.tracer_configurator_development, + ExperimentalTracerConfigurator( + default_config=ExperimentalTracerConfig(enabled=True), + tracers=[ + ExperimentalTracerMatcherAndConfig( + name="noisy.*", + config=ExperimentalTracerConfig(enabled=False), + ) + ], + ), + ) + + def test_meter_configurator_parsed_from_yaml(self): + self.assertEqual( + self.config.meter_provider.meter_configurator_development, + ExperimentalMeterConfigurator( + default_config=ExperimentalMeterConfig(enabled=False), + meters=[ + ExperimentalMeterMatcherAndConfig( + name="keep.*", + config=ExperimentalMeterConfig(enabled=True), + ) + ], + ), + ) + + def test_logger_configurator_parsed_from_yaml(self): + self.assertEqual( + self.config.logger_provider.logger_configurator_development, + ExperimentalLoggerConfigurator( + default_config=ExperimentalLoggerConfig(enabled=True), + loggers=[ + ExperimentalLoggerMatcherAndConfig( + name="noisy.*", + config=ExperimentalLoggerConfig(enabled=False), + ) + ], + ), + ) + + def test_tracer_configurator_from_yaml_is_wired(self): + provider = create_tracer_provider(self.config.tracer_provider) + self.assertFalse(provider._apply_tracer_configurator(InstrumentationScope("noisy.http")).is_enabled) + self.assertTrue(provider._apply_tracer_configurator(InstrumentationScope("app.service")).is_enabled) + + def test_meter_configurator_from_yaml_is_wired(self): + provider = create_meter_provider(self.config.meter_provider) + self.assertTrue(provider._apply_meter_configurator(InstrumentationScope("keep.me")).is_enabled) + self.assertFalse(provider._apply_meter_configurator(InstrumentationScope("other")).is_enabled) + + def test_logger_configurator_from_yaml_is_wired(self): + provider = create_logger_provider(self.config.logger_provider) + self.assertFalse(provider._apply_logger_configurator(InstrumentationScope("noisy.http")).is_enabled) + self.assertTrue(provider._apply_logger_configurator(InstrumentationScope("app.service")).is_enabled) diff --git a/opentelemetry-configuration/tests/test_logger_provider.py b/opentelemetry-configuration/tests/test_logger_provider.py index 9965e4b6bfe..299a643d235 100644 --- a/opentelemetry-configuration/tests/test_logger_provider.py +++ b/opentelemetry-configuration/tests/test_logger_provider.py @@ -25,6 +25,15 @@ from opentelemetry.configuration.models import ( BatchLogRecordProcessor as BatchLogRecordProcessorConfig, ) +from opentelemetry.configuration.models import ( + ExperimentalLoggerConfig as LoggerConfigConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalLoggerConfigurator as LoggerConfiguratorConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalLoggerMatcherAndConfig as LoggerMatcherAndConfig, +) from opentelemetry.configuration.models import ( ExperimentalOtlpFileExporter as ExperimentalOtlpFileExporterConfig, ) @@ -42,6 +51,7 @@ ) from opentelemetry.configuration.models import ( NameStringValuePair, + SeverityNumber, ) from opentelemetry.configuration.models import ( OtlpGrpcExporter as OtlpGrpcExporterConfig, @@ -59,6 +69,7 @@ SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestCreateLoggerProviderBasic(unittest.TestCase): @@ -432,5 +443,121 @@ def test_no_limits_no_warning(): mock_logger.warning.assert_not_called() +# Configurator tests access the SDK LoggerProvider private +# _apply_logger_configurator to assert the wired config takes effect. +# pylint: disable=protected-access +class TestLoggerConfigurator(unittest.TestCase): + @staticmethod + def _enabled(provider, name): + return provider._apply_logger_configurator(InstrumentationScope(name)).is_enabled + + def test_no_configurator_leaves_loggers_enabled(self): + provider = create_logger_provider(LoggerProviderConfig(processors=[])) + self.assertTrue(self._enabled(provider, "any.scope")) + + def test_matching_glob_disables_logger(self): + config = LoggerProviderConfig( + processors=[], + logger_configurator_development=LoggerConfiguratorConfig( + default_config=LoggerConfigConfig(enabled=True), + loggers=[ + LoggerMatcherAndConfig( + name="noisy.*", + config=LoggerConfigConfig(enabled=False), + ) + ], + ), + ) + provider = create_logger_provider(config) + self.assertFalse(self._enabled(provider, "noisy.http")) + self.assertTrue(self._enabled(provider, "app.service")) + + def test_default_config_applies_to_unmatched_scopes(self): + config = LoggerProviderConfig( + processors=[], + logger_configurator_development=LoggerConfiguratorConfig( + default_config=LoggerConfigConfig(enabled=False), + loggers=[ + LoggerMatcherAndConfig( + name="keep.*", + config=LoggerConfigConfig(enabled=True), + ) + ], + ), + ) + provider = create_logger_provider(config) + self.assertTrue(self._enabled(provider, "keep.me")) + self.assertFalse(self._enabled(provider, "other")) + + def test_first_matching_rule_wins(self): + config = LoggerProviderConfig( + processors=[], + logger_configurator_development=LoggerConfiguratorConfig( + loggers=[ + LoggerMatcherAndConfig( + name="a.*", + config=LoggerConfigConfig(enabled=False), + ), + LoggerMatcherAndConfig( + name="a.b", + config=LoggerConfigConfig(enabled=True), + ), + ], + ), + ) + provider = create_logger_provider(config) + self.assertFalse(self._enabled(provider, "a.b")) + + def test_absent_enabled_defaults_to_enabled(self): + config = LoggerProviderConfig( + processors=[], + logger_configurator_development=LoggerConfiguratorConfig( + default_config=LoggerConfigConfig(), + ), + ) + provider = create_logger_provider(config) + self.assertTrue(self._enabled(provider, "any.scope")) + + def test_unsupported_minimum_severity_logs_warning(self): + config = LoggerProviderConfig( + processors=[], + logger_configurator_development=LoggerConfiguratorConfig( + default_config=LoggerConfigConfig( + enabled=True, + minimum_severity=SeverityNumber.warn, + ), + ), + ) + with self.assertLogs( + "opentelemetry.configuration._logger_provider", + level="WARNING", + ) as cm: + create_logger_provider(config) + self.assertTrue( + any("minimum_severity" in msg for msg in cm.output), + "Expected warning about unsupported minimum_severity", + ) + + def test_unsupported_trace_based_logs_warning(self): + config = LoggerProviderConfig( + processors=[], + logger_configurator_development=LoggerConfiguratorConfig( + default_config=LoggerConfigConfig( + enabled=True, + trace_based=True, + ), + ), + ) + with self.assertLogs( + "opentelemetry.configuration._logger_provider", + level="WARNING", + ) as cm: + create_logger_provider(config) + self.assertTrue( + any("trace_based" in msg for msg in cm.output), + "Expected warning about unsupported trace_based", + ) + + if __name__ == "__main__": unittest.main() diff --git a/opentelemetry-configuration/tests/test_meter_provider.py b/opentelemetry-configuration/tests/test_meter_provider.py index 83c9a8868c1..6dd8d054d6f 100644 --- a/opentelemetry-configuration/tests/test_meter_provider.py +++ b/opentelemetry-configuration/tests/test_meter_provider.py @@ -3,6 +3,7 @@ # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access +# pylint: disable=too-many-lines import os import sys @@ -24,6 +25,15 @@ from opentelemetry.configuration.models import ( ConsoleMetricExporter as ConsoleMetricExporterConfig, ) +from opentelemetry.configuration.models import ( + ExperimentalMeterConfig as MeterConfigConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalMeterConfigurator as MeterConfiguratorConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalMeterMatcherAndConfig as MeterMatcherAndConfig, +) from opentelemetry.configuration.models import ( ExperimentalOtlpFileMetricExporter as ExperimentalOtlpFileMetricExporterConfig, ) @@ -93,6 +103,7 @@ View, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestCreateMeterProviderBasic(unittest.TestCase): @@ -857,3 +868,76 @@ def test_stream_aggregation_sum(self): def test_stream_aggregation_default(self): view = self._get_view(self._make_view_config(stream_kwargs={"aggregation": AggregationConfig(default={})})) self.assertIsInstance(view._aggregation, DefaultAggregation) + + +class TestMeterConfigurator(unittest.TestCase): + @staticmethod + def _enabled(provider, name): + return provider._apply_meter_configurator(InstrumentationScope(name)).is_enabled + + def test_no_configurator_leaves_meters_enabled(self): + provider = create_meter_provider(MeterProviderConfig(readers=[])) + self.assertTrue(self._enabled(provider, "any.scope")) + + def test_matching_glob_disables_meter(self): + config = MeterProviderConfig( + readers=[], + meter_configurator_development=MeterConfiguratorConfig( + default_config=MeterConfigConfig(enabled=True), + meters=[ + MeterMatcherAndConfig( + name="noisy.*", + config=MeterConfigConfig(enabled=False), + ) + ], + ), + ) + provider = create_meter_provider(config) + self.assertFalse(self._enabled(provider, "noisy.http")) + self.assertTrue(self._enabled(provider, "app.service")) + + def test_default_config_applies_to_unmatched_scopes(self): + config = MeterProviderConfig( + readers=[], + meter_configurator_development=MeterConfiguratorConfig( + default_config=MeterConfigConfig(enabled=False), + meters=[ + MeterMatcherAndConfig( + name="keep.*", + config=MeterConfigConfig(enabled=True), + ) + ], + ), + ) + provider = create_meter_provider(config) + self.assertTrue(self._enabled(provider, "keep.me")) + self.assertFalse(self._enabled(provider, "other")) + + def test_first_matching_rule_wins(self): + config = MeterProviderConfig( + readers=[], + meter_configurator_development=MeterConfiguratorConfig( + meters=[ + MeterMatcherAndConfig( + name="a.*", + config=MeterConfigConfig(enabled=False), + ), + MeterMatcherAndConfig( + name="a.b", + config=MeterConfigConfig(enabled=True), + ), + ], + ), + ) + provider = create_meter_provider(config) + self.assertFalse(self._enabled(provider, "a.b")) + + def test_absent_enabled_defaults_to_enabled(self): + config = MeterProviderConfig( + readers=[], + meter_configurator_development=MeterConfiguratorConfig( + default_config=MeterConfigConfig(), + ), + ) + provider = create_meter_provider(config) + self.assertTrue(self._enabled(provider, "any.scope")) diff --git a/opentelemetry-configuration/tests/test_tracer_provider.py b/opentelemetry-configuration/tests/test_tracer_provider.py index 9c950a0e049..bdcc359a127 100644 --- a/opentelemetry-configuration/tests/test_tracer_provider.py +++ b/opentelemetry-configuration/tests/test_tracer_provider.py @@ -3,6 +3,7 @@ # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access +# pylint: disable=too-many-lines import os import sys @@ -43,6 +44,15 @@ from opentelemetry.configuration.models import ( ExperimentalSpanParent as SpanParentConfig, ) +from opentelemetry.configuration.models import ( + ExperimentalTracerConfig as TracerConfigConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalTracerConfigurator as TracerConfiguratorConfig, +) +from opentelemetry.configuration.models import ( + ExperimentalTracerMatcherAndConfig as TracerMatcherAndConfig, +) from opentelemetry.configuration.models import ( IdGenerator as IdGeneratorConfig, ) @@ -95,6 +105,7 @@ Sampler, TraceIdRatioBased, ) +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import SpanContext, TraceFlags from opentelemetry.trace import SpanKind as TraceSpanKind @@ -830,3 +841,78 @@ def test_empty_id_generator_raises_configuration_error(self): """Empty IdGenerator config (no type specified) raises ConfigurationError.""" with self.assertRaises(ConfigurationError): self._make_provider(IdGeneratorConfig()) + + +# Configurator tests access the SDK TracerProvider private +# _apply_tracer_configurator to assert the wired config takes effect. +class TestTracerConfigurator(unittest.TestCase): + @staticmethod + def _enabled(provider, name): + return provider._apply_tracer_configurator(InstrumentationScope(name)).is_enabled + + def test_no_configurator_leaves_tracers_enabled(self): + provider = create_tracer_provider(TracerProviderConfig(processors=[])) + self.assertTrue(self._enabled(provider, "any.scope")) + + def test_matching_glob_disables_tracer(self): + config = TracerProviderConfig( + processors=[], + tracer_configurator_development=TracerConfiguratorConfig( + default_config=TracerConfigConfig(enabled=True), + tracers=[ + TracerMatcherAndConfig( + name="noisy.*", + config=TracerConfigConfig(enabled=False), + ) + ], + ), + ) + provider = create_tracer_provider(config) + self.assertFalse(self._enabled(provider, "noisy.http")) + self.assertTrue(self._enabled(provider, "app.service")) + + def test_default_config_applies_to_unmatched_scopes(self): + config = TracerProviderConfig( + processors=[], + tracer_configurator_development=TracerConfiguratorConfig( + default_config=TracerConfigConfig(enabled=False), + tracers=[ + TracerMatcherAndConfig( + name="keep.*", + config=TracerConfigConfig(enabled=True), + ) + ], + ), + ) + provider = create_tracer_provider(config) + self.assertTrue(self._enabled(provider, "keep.me")) + self.assertFalse(self._enabled(provider, "other")) + + def test_first_matching_rule_wins(self): + config = TracerProviderConfig( + processors=[], + tracer_configurator_development=TracerConfiguratorConfig( + tracers=[ + TracerMatcherAndConfig( + name="a.*", + config=TracerConfigConfig(enabled=False), + ), + TracerMatcherAndConfig( + name="a.b", + config=TracerConfigConfig(enabled=True), + ), + ], + ), + ) + provider = create_tracer_provider(config) + self.assertFalse(self._enabled(provider, "a.b")) + + def test_absent_enabled_defaults_to_enabled(self): + config = TracerProviderConfig( + processors=[], + tracer_configurator_development=TracerConfiguratorConfig( + default_config=TracerConfigConfig(), + ), + ) + provider = create_tracer_provider(config) + self.assertTrue(self._enabled(provider, "any.scope"))