From a643c55f940a5b8680b23180ad9a234e978b8823 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 12 Nov 2025 12:06:46 +0100 Subject: [PATCH 1/6] opentelemetry-sdk: make it possible to customize processors configuration Make it easier for distributions to override the processors set up by the sdk configurator by specifying a span processor and a log record processor. --- .../sdk/_configuration/__init__.py | 21 +++++++-- opentelemetry-sdk/tests/test_configurator.py | 43 +++++++++++++++++-- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 7c0d0468f8..222167b0ac 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -37,7 +37,11 @@ OTEL_TRACES_EXPORTER, ) from opentelemetry.metrics import set_meter_provider -from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs import ( + LoggerProvider, + LoggingHandler, + LogRecordProcessor, +) from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, LogRecordExporter, @@ -58,7 +62,7 @@ PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import Attributes, Resource -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import SpanProcessor, TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.sdk.trace.sampling import Sampler @@ -210,6 +214,7 @@ def _init_tracing( sampler: Sampler | None = None, resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, + processor: Type[SpanProcessor] | None = None, ): provider = TracerProvider( id_generator=id_generator, @@ -219,10 +224,11 @@ def _init_tracing( set_tracer_provider(provider) exporter_args_map = exporter_args_map or {} + span_processor = processor or BatchSpanProcessor for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) provider.add_span_processor( - BatchSpanProcessor(exporter_class(**exporter_args)) + span_processor(exporter_class(**exporter_args)) ) @@ -256,15 +262,17 @@ def _init_logging( resource: Resource | None = None, setup_logging_handler: bool = True, exporter_args_map: ExporterArgsMap | None = None, + processor: Type[LogRecordProcessor] | None = None, ): provider = LoggerProvider(resource=resource) set_logger_provider(provider) exporter_args_map = exporter_args_map or {} + log_record_processor = processor or BatchLogRecordProcessor for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) provider.add_log_record_processor( - BatchLogRecordProcessor(exporter_class(**exporter_args)) + log_record_processor(exporter_class(**exporter_args)) ) # silence warnings from internal users until we drop the deprecated Events API @@ -429,7 +437,10 @@ def _initialize_components( id_generator: IdGenerator | None = None, setup_logging_handler: bool | None = None, exporter_args_map: ExporterArgsMap | None = None, + span_processor: Type[SpanProcessor] | None = None, + log_record_processor: Type[LogRecordProcessor] | None = None, ): + # pylint: disable=too-many-locals if trace_exporter_names is None: trace_exporter_names = [] if metric_exporter_names is None: @@ -464,6 +475,7 @@ def _initialize_components( sampler=sampler, resource=resource, exporter_args_map=exporter_args_map, + processor=span_processor, ) _init_metrics( metric_exporters, resource, exporter_args_map=exporter_args_map @@ -482,6 +494,7 @@ def _initialize_components( resource, setup_logging_handler, exporter_args_map=exporter_args_map, + processor=log_record_processor, ) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 8edc9190da..26ebe7b389 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -47,7 +47,10 @@ ) from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs._internal.export import LogRecordExporter -from opentelemetry.sdk._logs.export import ConsoleLogRecordExporter +from opentelemetry.sdk._logs.export import ( + ConsoleLogRecordExporter + SimpleLogRecordProcessor, +) from opentelemetry.sdk.environment_variables import ( OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, @@ -62,7 +65,10 @@ ) from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace.export import ConsoleSpanExporter +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, +) from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_ON, @@ -396,6 +402,16 @@ def test_trace_init_exporter_uses_exporter_args_map(self): exporter = provider.processor.exporter self.assertEqual(exporter.compression, "gzip") + def test_trace_init_custom_span_processor(self): + _init_tracing( + {"otlp": OTLPSpanExporter}, + id_generator=RandomIdGenerator(), + processor=SimpleSpanProcessor, + ) + + provider = self.set_provider_mock.call_args[0][0] + self.assertTrue(isinstance(provider.processor, SimpleSpanProcessor)) + @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) @patch("opentelemetry.sdk._configuration.entry_points") @@ -706,6 +722,17 @@ def test_logging_init_exporter_uses_exporter_args_map(self): provider = self.set_provider_mock.call_args[0][0] self.assertEqual(provider.processor.exporter.compression, "gzip") + def test_logging_init_custom_log_record_processor(self): + with ResetGlobalLoggingState(): + resource = Resource.create({}) + _init_logging( + {"otlp": DummyOTLPLogExporter}, + resource=resource, + processor=SimpleLogRecordProcessor, + ) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider.processor, SimpleLogRecordProcessor) + @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, @@ -742,7 +769,7 @@ 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, exporter_args_map=None + mock.ANY, mock.ANY, False, exporter_args_map=None, processor=None ) @patch.dict( @@ -758,7 +785,11 @@ 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, exporter_args_map=None + mock.ANY, + mock.ANY, + True, + exporter_args_map=None, + processor=None, ) self.assertEqual(tracing_mock.call_count, 1) @@ -843,6 +874,8 @@ def test_initialize_components_kwargs( "id_generator": "TEST_GENERATOR", "setup_logging_handler": True, "exporter_args_map": {1: {"compression": "gzip"}}, + "log_record_processor": SimpleLogRecordProcessor, + "span_processor": SimpleSpanProcessor, } _initialize_components(**kwargs) @@ -877,6 +910,7 @@ def test_initialize_components_kwargs( sampler="TEST_SAMPLER", resource="TEST_RESOURCE", exporter_args_map={1: {"compression": "gzip"}}, + processor=SimpleSpanProcessor, ) metrics_mock.assert_called_once_with( "TEST_METRICS_EXPORTERS_DICT", @@ -888,6 +922,7 @@ def test_initialize_components_kwargs( "TEST_RESOURCE", True, exporter_args_map={1: {"compression": "gzip"}}, + processor=SimpleLogRecordProcessor, ) def test_basicConfig_works_with_otel_handler(self): From 40d939fb3678a19a1a06ac7197fcade1f156f895 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 12 Nov 2025 12:10:59 +0100 Subject: [PATCH 2/6] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3091794e0..3a55630838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798)) - Silence events API warnings for internal users ([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847)) +- opentelemetry-sdk: make it possible to override the default processors in the SDK configurator + ([#4806](https://github.com/open-telemetry/opentelemetry-python/pull/4806)) ## Version 1.39.0/0.60b0 (2025-12-03) From 7ea39e38ddf4af6b82d58f12a1aeccff40f8d804 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Dec 2025 14:37:00 +0100 Subject: [PATCH 3/6] Fixes --- .../sdk/_configuration/__init__.py | 37 ++++++++++++++----- opentelemetry-sdk/tests/test_configurator.py | 32 ++++++++++------ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 222167b0ac..78ca5705bf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -214,7 +214,8 @@ def _init_tracing( sampler: Sampler | None = None, resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, - processor: Type[SpanProcessor] | None = None, + span_processors: Sequence[Type[SpanProcessor]] | None = None, + export_processor: Type[SpanProcessor] | None = None, ): provider = TracerProvider( id_generator=id_generator, @@ -224,11 +225,16 @@ def _init_tracing( set_tracer_provider(provider) exporter_args_map = exporter_args_map or {} - span_processor = processor or BatchSpanProcessor + span_processors = span_processors or [] + export_processor = export_processor or BatchSpanProcessor for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) + + for span_processor in span_processors: + provider.add_span_processor(span_processor) + provider.add_span_processor( - span_processor(exporter_class(**exporter_args)) + export_processor(exporter_class(**exporter_args)) ) @@ -262,17 +268,24 @@ def _init_logging( resource: Resource | None = None, setup_logging_handler: bool = True, exporter_args_map: ExporterArgsMap | None = None, - processor: Type[LogRecordProcessor] | None = None, + log_record_processors: Sequence[Type[LogRecordProcessor]] | None = None, + export_processor: Type[LogRecordProcessor] | None = None, ): provider = LoggerProvider(resource=resource) set_logger_provider(provider) exporter_args_map = exporter_args_map or {} - log_record_processor = processor or BatchLogRecordProcessor + + log_record_processors = log_record_processors or [] + export_processor = export_processor or BatchLogRecordProcessor for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) + + for log_record_processor in log_record_processors: + provider.add_log_record_processor(log_record_processor) + provider.add_log_record_processor( - log_record_processor(exporter_class(**exporter_args)) + export_processor(exporter_class(**exporter_args)) ) # silence warnings from internal users until we drop the deprecated Events API @@ -437,8 +450,10 @@ def _initialize_components( id_generator: IdGenerator | None = None, setup_logging_handler: bool | None = None, exporter_args_map: ExporterArgsMap | None = None, - span_processor: Type[SpanProcessor] | None = None, - log_record_processor: Type[LogRecordProcessor] | None = None, + span_processors: Sequence[Type[SpanProcessor]] | None = None, + trace_export_processor: Type[SpanProcessor] | None = None, + log_record_processors: Sequence[Type[LogRecordProcessor]] | None = None, + log_export_processor: Type[LogRecordProcessor] | None = None, ): # pylint: disable=too-many-locals if trace_exporter_names is None: @@ -475,7 +490,8 @@ def _initialize_components( sampler=sampler, resource=resource, exporter_args_map=exporter_args_map, - processor=span_processor, + span_processors=span_processors, + export_processor=trace_export_processor, ) _init_metrics( metric_exporters, resource, exporter_args_map=exporter_args_map @@ -494,7 +510,8 @@ def _initialize_components( resource, setup_logging_handler, exporter_args_map=exporter_args_map, - processor=log_record_processor, + log_record_processors=log_record_processors, + export_processor=log_export_processor, ) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 26ebe7b389..a6f94eb4ea 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -48,7 +48,7 @@ from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs._internal.export import LogRecordExporter from opentelemetry.sdk._logs.export import ( - ConsoleLogRecordExporter + ConsoleLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.environment_variables import ( @@ -402,11 +402,11 @@ def test_trace_init_exporter_uses_exporter_args_map(self): exporter = provider.processor.exporter self.assertEqual(exporter.compression, "gzip") - def test_trace_init_custom_span_processor(self): + def test_trace_init_custom_export_span_processor(self): _init_tracing( {"otlp": OTLPSpanExporter}, id_generator=RandomIdGenerator(), - processor=SimpleSpanProcessor, + export_processor=SimpleSpanProcessor, ) provider = self.set_provider_mock.call_args[0][0] @@ -722,13 +722,13 @@ def test_logging_init_exporter_uses_exporter_args_map(self): provider = self.set_provider_mock.call_args[0][0] self.assertEqual(provider.processor.exporter.compression, "gzip") - def test_logging_init_custom_log_record_processor(self): + def test_logging_init_custom_export_log_record_processor(self): with ResetGlobalLoggingState(): resource = Resource.create({}) _init_logging( {"otlp": DummyOTLPLogExporter}, resource=resource, - processor=SimpleLogRecordProcessor, + export_processor=SimpleLogRecordProcessor, ) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.processor, SimpleLogRecordProcessor) @@ -769,7 +769,12 @@ 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, exporter_args_map=None, processor=None + mock.ANY, + mock.ANY, + False, + exporter_args_map=None, + log_record_processors=None, + export_processor=None, ) @patch.dict( @@ -789,7 +794,8 @@ def test_logging_init_enable_env(self, logging_mock, tracing_mock): mock.ANY, True, exporter_args_map=None, - processor=None, + log_record_processors=None, + export_processor=None, ) self.assertEqual(tracing_mock.call_count, 1) @@ -874,8 +880,10 @@ def test_initialize_components_kwargs( "id_generator": "TEST_GENERATOR", "setup_logging_handler": True, "exporter_args_map": {1: {"compression": "gzip"}}, - "log_record_processor": SimpleLogRecordProcessor, - "span_processor": SimpleSpanProcessor, + "log_export_processor": SimpleLogRecordProcessor, + "trace_export_processor": SimpleSpanProcessor, + "log_record_processors": [], + "span_processors": [], } _initialize_components(**kwargs) @@ -910,7 +918,8 @@ def test_initialize_components_kwargs( sampler="TEST_SAMPLER", resource="TEST_RESOURCE", exporter_args_map={1: {"compression": "gzip"}}, - processor=SimpleSpanProcessor, + span_processors=[], + export_processor=SimpleSpanProcessor, ) metrics_mock.assert_called_once_with( "TEST_METRICS_EXPORTERS_DICT", @@ -922,7 +931,8 @@ def test_initialize_components_kwargs( "TEST_RESOURCE", True, exporter_args_map={1: {"compression": "gzip"}}, - processor=SimpleLogRecordProcessor, + log_record_processors=[], + export_processor=SimpleLogRecordProcessor, ) def test_basicConfig_works_with_otel_handler(self): From 7e27fc3c59f5d6c729b1e212290b34047e84b484 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Dec 2025 16:54:59 +0100 Subject: [PATCH 4/6] Please typechecking --- .../sdk/_configuration/__init__.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 78ca5705bf..a7cf3b350c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -109,6 +109,14 @@ ] +class ExporterSpanProcessor(SpanProcessor): + def __init__(self, span_exporter: SpanExporter, *args, **kwargs): ... + + +class ExporterLogRecordProcessor(LogRecordProcessor): + def __init__(self, exporter: LogRecordExporter, *args, **kwargs): ... + + def _import_config_components( selected_components: Sequence[str], entry_point_name: str ) -> list[tuple[str, Type]]: @@ -214,8 +222,8 @@ def _init_tracing( sampler: Sampler | None = None, resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, - span_processors: Sequence[Type[SpanProcessor]] | None = None, - export_processor: Type[SpanProcessor] | None = None, + span_processors: Sequence[SpanProcessor] | None = None, + export_span_processor: Type[ExporterSpanProcessor] | None = None, ): provider = TracerProvider( id_generator=id_generator, @@ -226,7 +234,7 @@ def _init_tracing( exporter_args_map = exporter_args_map or {} span_processors = span_processors or [] - export_processor = export_processor or BatchSpanProcessor + export_processor = export_span_processor or BatchSpanProcessor for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) @@ -268,8 +276,9 @@ def _init_logging( resource: Resource | None = None, setup_logging_handler: bool = True, exporter_args_map: ExporterArgsMap | None = None, - log_record_processors: Sequence[Type[LogRecordProcessor]] | None = None, - export_processor: Type[LogRecordProcessor] | None = None, + log_record_processors: Sequence[LogRecordProcessor] | None = None, + export_log_record_processor: Type[ExporterLogRecordProcessor] + | None = None, ): provider = LoggerProvider(resource=resource) set_logger_provider(provider) @@ -277,7 +286,7 @@ def _init_logging( exporter_args_map = exporter_args_map or {} log_record_processors = log_record_processors or [] - export_processor = export_processor or BatchLogRecordProcessor + export_processor = export_log_record_processor or BatchLogRecordProcessor for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) @@ -450,10 +459,11 @@ def _initialize_components( id_generator: IdGenerator | None = None, setup_logging_handler: bool | None = None, exporter_args_map: ExporterArgsMap | None = None, - span_processors: Sequence[Type[SpanProcessor]] | None = None, - trace_export_processor: Type[SpanProcessor] | None = None, - log_record_processors: Sequence[Type[LogRecordProcessor]] | None = None, - log_export_processor: Type[LogRecordProcessor] | None = None, + span_processors: Sequence[SpanProcessor] | None = None, + export_span_processor: Type[ExporterSpanProcessor] | None = None, + log_record_processors: Sequence[LogRecordProcessor] | None = None, + export_log_record_processor: Type[ExporterLogRecordProcessor] + | None = None, ): # pylint: disable=too-many-locals if trace_exporter_names is None: @@ -491,7 +501,7 @@ def _initialize_components( resource=resource, exporter_args_map=exporter_args_map, span_processors=span_processors, - export_processor=trace_export_processor, + export_span_processor=export_span_processor, ) _init_metrics( metric_exporters, resource, exporter_args_map=exporter_args_map @@ -511,7 +521,7 @@ def _initialize_components( setup_logging_handler, exporter_args_map=exporter_args_map, log_record_processors=log_record_processors, - export_processor=log_export_processor, + export_log_record_processor=export_log_record_processor, ) From d25e3678e998376a44f4c71cef18b3126fa524ab Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Dec 2025 11:56:14 +0100 Subject: [PATCH 5/6] Fix naming --- opentelemetry-sdk/tests/test_configurator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index a6f94eb4ea..1106add8ae 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -406,7 +406,7 @@ def test_trace_init_custom_export_span_processor(self): _init_tracing( {"otlp": OTLPSpanExporter}, id_generator=RandomIdGenerator(), - export_processor=SimpleSpanProcessor, + export_span_processor=SimpleSpanProcessor, ) provider = self.set_provider_mock.call_args[0][0] @@ -728,7 +728,7 @@ def test_logging_init_custom_export_log_record_processor(self): _init_logging( {"otlp": DummyOTLPLogExporter}, resource=resource, - export_processor=SimpleLogRecordProcessor, + export_log_record_processor=SimpleLogRecordProcessor, ) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.processor, SimpleLogRecordProcessor) @@ -774,7 +774,7 @@ def test_logging_init_disable_default(self, logging_mock, tracing_mock): False, exporter_args_map=None, log_record_processors=None, - export_processor=None, + export_log_record_processor=None, ) @patch.dict( @@ -795,7 +795,7 @@ def test_logging_init_enable_env(self, logging_mock, tracing_mock): True, exporter_args_map=None, log_record_processors=None, - export_processor=None, + export_log_record_processor=None, ) self.assertEqual(tracing_mock.call_count, 1) @@ -880,8 +880,8 @@ def test_initialize_components_kwargs( "id_generator": "TEST_GENERATOR", "setup_logging_handler": True, "exporter_args_map": {1: {"compression": "gzip"}}, - "log_export_processor": SimpleLogRecordProcessor, - "trace_export_processor": SimpleSpanProcessor, + "export_log_record_processor": SimpleLogRecordProcessor, + "export_span_processor": SimpleSpanProcessor, "log_record_processors": [], "span_processors": [], } @@ -919,7 +919,7 @@ def test_initialize_components_kwargs( resource="TEST_RESOURCE", exporter_args_map={1: {"compression": "gzip"}}, span_processors=[], - export_processor=SimpleSpanProcessor, + export_span_processor=SimpleSpanProcessor, ) metrics_mock.assert_called_once_with( "TEST_METRICS_EXPORTERS_DICT", @@ -932,7 +932,7 @@ def test_initialize_components_kwargs( True, exporter_args_map={1: {"compression": "gzip"}}, log_record_processors=[], - export_processor=SimpleLogRecordProcessor, + export_log_record_processor=SimpleLogRecordProcessor, ) def test_basicConfig_works_with_otel_handler(self): From ba2875999576c7c3c5b0a1d954c3cd374cfb9fa2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Dec 2025 17:15:17 +0100 Subject: [PATCH 6/6] Span processors should be added once --- .../sdk/_configuration/__init__.py | 19 +++-- opentelemetry-sdk/tests/test_configurator.py | 79 +++++++++++++------ 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index a7cf3b350c..9837fd474c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -233,14 +233,14 @@ def _init_tracing( set_tracer_provider(provider) exporter_args_map = exporter_args_map or {} - span_processors = span_processors or [] export_processor = export_span_processor or BatchSpanProcessor - for _, exporter_class in exporters.items(): - exporter_args = exporter_args_map.get(exporter_class, {}) - for span_processor in span_processors: - provider.add_span_processor(span_processor) + span_processors = span_processors or [] + for span_processor in span_processors: + provider.add_span_processor(span_processor) + for _, exporter_class in exporters.items(): + exporter_args = exporter_args_map.get(exporter_class, {}) provider.add_span_processor( export_processor(exporter_class(**exporter_args)) ) @@ -284,15 +284,14 @@ def _init_logging( set_logger_provider(provider) exporter_args_map = exporter_args_map or {} + export_processor = export_log_record_processor or BatchLogRecordProcessor log_record_processors = log_record_processors or [] - export_processor = export_log_record_processor or BatchLogRecordProcessor + for log_record_processor in log_record_processors: + provider.add_log_record_processor(log_record_processor) + for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) - - for log_record_processor in log_record_processors: - provider.add_log_record_processor(log_record_processor) - provider.add_log_record_processor( export_processor(exporter_class(**exporter_args)) ) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 1106add8ae..e443087343 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -45,7 +45,7 @@ _initialize_components, _OTelSDKConfigurator, ) -from opentelemetry.sdk._logs import LoggingHandler +from opentelemetry.sdk._logs import LoggingHandler, LogRecordProcessor from opentelemetry.sdk._logs._internal.export import LogRecordExporter from opentelemetry.sdk._logs.export import ( ConsoleLogRecordExporter, @@ -65,6 +65,7 @@ ) from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import SpanProcessor from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, @@ -88,23 +89,23 @@ class Provider: def __init__(self, resource=None, sampler=None, id_generator=None): self.sampler = sampler self.id_generator = id_generator - self.processor = None + self.processors = [] self.resource = resource or Resource.create({}) def add_span_processor(self, processor): - self.processor = processor + self.processors.append(processor) class DummyLoggerProvider: def __init__(self, resource=None): self.resource = resource - self.processor = DummyLogRecordProcessor(DummyOTLPLogExporter()) + self.processors = [] def add_log_record_processor(self, processor): - self.processor = processor + self.processors.append(processor) def get_logger(self, name, *args, **kwargs): - return DummyLogger(name, self.resource, self.processor) + return DummyLogger(name, self.resource, self.processors) def force_flush(self, *args, **kwargs): pass @@ -115,10 +116,10 @@ class DummyMeterProvider(MeterProvider): class DummyLogger: - def __init__(self, name, resource, processor): + def __init__(self, name, resource, processors): self.name = name self.resource = resource - self.processor = processor + self.processors = processors def emit( self, @@ -133,7 +134,8 @@ def emit( attributes=None, event_name=None, ): - self.processor.emit(record) + for processor in self.processors: + processor.emit(record) class DummyLogRecordProcessor: @@ -357,10 +359,11 @@ def test_trace_init_default(self): provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) self.assertIsInstance(provider.id_generator, RandomIdGenerator) - self.assertIsInstance(provider.processor, Processor) - self.assertIsInstance(provider.processor.exporter, Exporter) + self.assertEqual(len(provider.processors), 1) + self.assertIsInstance(provider.processors[0], Processor) + self.assertIsInstance(provider.processors[0].exporter, Exporter) self.assertEqual( - provider.processor.exporter.service_name, "my-test-service" + provider.processors[0].exporter.service_name, "my-test-service" ) self.assertEqual( provider.resource.attributes.get("telemetry.auto.version"), @@ -380,8 +383,11 @@ def test_trace_init_otlp(self): provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) self.assertIsInstance(provider.id_generator, RandomIdGenerator) - self.assertIsInstance(provider.processor, Processor) - self.assertIsInstance(provider.processor.exporter, OTLPSpanExporter) + self.assertEqual(len(provider.processors), 1) + self.assertIsInstance(provider.processors[0], Processor) + self.assertIsInstance( + provider.processors[0].exporter, OTLPSpanExporter + ) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("service.name"), @@ -399,18 +405,25 @@ def test_trace_init_exporter_uses_exporter_args_map(self): ) provider = self.set_provider_mock.call_args[0][0] - exporter = provider.processor.exporter + self.assertEqual(len(provider.processors), 1) + exporter = provider.processors[0].exporter self.assertEqual(exporter.compression, "gzip") - def test_trace_init_custom_export_span_processor(self): + def test_trace_init_custom_span_processors(self): + span_processor = mock.Mock(spec=SpanProcessor) _init_tracing( {"otlp": OTLPSpanExporter}, id_generator=RandomIdGenerator(), + span_processors=[span_processor], export_span_processor=SimpleSpanProcessor, ) provider = self.set_provider_mock.call_args[0][0] - self.assertTrue(isinstance(provider.processor, SimpleSpanProcessor)) + self.assertEqual(len(provider.processors), 2) + self.assertEqual(provider.processors[0], span_processor) + self.assertTrue( + isinstance(provider.processors[1], SimpleSpanProcessor) + ) @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) @@ -700,12 +713,16 @@ def test_logging_init_exporter(self): provider.resource.attributes.get("service.name"), "otlp-service", ) - self.assertIsInstance(provider.processor, DummyLogRecordProcessor) + self.assertEqual(len(provider.processors), 1) + self.assertIsInstance( + provider.processors[0], DummyLogRecordProcessor + ) self.assertIsInstance( - provider.processor.exporter, DummyOTLPLogExporter + provider.processors[0].exporter, DummyOTLPLogExporter ) getLogger(__name__).error("hello") - self.assertTrue(provider.processor.exporter.export_called) + self.assertEqual(len(provider.processors), 1) + self.assertTrue(provider.processors[0].exporter.export_called) def test_logging_init_exporter_uses_exporter_args_map(self): with ResetGlobalLoggingState(): @@ -720,18 +737,27 @@ def test_logging_init_exporter_uses_exporter_args_map(self): ) 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") + self.assertEqual(len(provider.processors), 1) + self.assertEqual( + provider.processors[0].exporter.compression, "gzip" + ) - def test_logging_init_custom_export_log_record_processor(self): + def test_logging_init_custom_log_record_processors(self): + log_record_processor = mock.Mock(spec=LogRecordProcessor) with ResetGlobalLoggingState(): resource = Resource.create({}) _init_logging( {"otlp": DummyOTLPLogExporter}, resource=resource, + log_record_processors=[log_record_processor], export_log_record_processor=SimpleLogRecordProcessor, ) provider = self.set_provider_mock.call_args[0][0] - self.assertIsInstance(provider.processor, SimpleLogRecordProcessor) + self.assertEqual(len(provider.processors), 2) + self.assertEqual(provider.processors[0], log_record_processor) + self.assertIsInstance( + provider.processors[1], SimpleLogRecordProcessor + ) @patch.dict( environ, @@ -752,12 +778,13 @@ def test_logging_init_exporter_without_handler_setup(self): provider.resource.attributes.get("service.name"), "otlp-service", ) - self.assertIsInstance(provider.processor, DummyLogRecordProcessor) + self.assertEqual(len(provider.processors), 1) + self.assertIsInstance(provider.processors[0], DummyLogRecordProcessor) self.assertIsInstance( - provider.processor.exporter, DummyOTLPLogExporter + provider.processors[0].exporter, DummyOTLPLogExporter ) getLogger(__name__).error("hello") - self.assertFalse(provider.processor.exporter.export_called) + self.assertFalse(provider.processors[0].exporter.export_called) @patch.dict( environ,