From d69d95e2aa3cbedaa8955679af8f5d584e723a1d Mon Sep 17 00:00:00 2001 From: Henry Su Date: Mon, 20 Jul 2026 14:51:37 -0500 Subject: [PATCH] fix(structlog): preserve existing processors Assisted-by: OpenAI Codex --- .../instrumentation/structlog/__init__.py | 20 +++++---- .../tests/test_structlog.py | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog/__init__.py b/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog/__init__.py index c280938ec3..50117b3fdd 100644 --- a/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog/__init__.py @@ -345,13 +345,17 @@ def _instrument(self, **kwargs): config = structlog.get_config() current_processors = list(config.get("processors", [])) - # Insert before the last processor, assumed to be the renderer. - if current_processors: - insert_position = len(current_processors) - 1 - else: - insert_position = 0 + # Insert before the last processor, assumed to be the renderer, unless + # the app has already configured an OTel processor. + if not any( + isinstance(p, StructlogProcessor) for p in current_processors + ): + if current_processors: + insert_position = len(current_processors) - 1 + else: + insert_position = 0 - current_processors.insert(insert_position, processor) + current_processors.insert(insert_position, processor) # Reconfigure structlog with the new processor chain StructlogInstrumentor._original_configure( @@ -418,11 +422,11 @@ def _uninstrument(self, **kwargs): config = structlog.get_config() current_processors = list(config.get("processors", [])) - # Remove all StructlogProcessor instances + # Remove only the processor added by this instrumentor. new_processors = [ p for p in current_processors - if not isinstance(p, StructlogProcessor) + if p is not StructlogInstrumentor._processor ] configured_by_app = StructlogInstrumentor._is_configured_by_app diff --git a/instrumentation/opentelemetry-instrumentation-structlog/tests/test_structlog.py b/instrumentation/opentelemetry-instrumentation-structlog/tests/test_structlog.py index d42e3f97b0..5bdda99d03 100644 --- a/instrumentation/opentelemetry-instrumentation-structlog/tests/test_structlog.py +++ b/instrumentation/opentelemetry-instrumentation-structlog/tests/test_structlog.py @@ -470,6 +470,48 @@ def test_instrument_adds_processor(self): ) self.assertTrue(has_otel_processor) + def test_instrument_does_not_duplicate_existing_processor(self): + """Test instrument() reuses an existing OTel processor.""" + existing_processor = StructlogProcessor( + logger_provider=self.logger_provider + ) + structlog.configure( + processors=[ + existing_processor, + structlog.dev.ConsoleRenderer(), + ] + ) + + StructlogInstrumentor().instrument( + logger_provider=self.logger_provider + ) + + processors = structlog.get_config()["processors"] + otel_processors = [ + p for p in processors if isinstance(p, StructlogProcessor) + ] + self.assertEqual(otel_processors, [existing_processor]) + + def test_uninstrument_preserves_existing_processor(self): + """Test uninstrument() preserves an app-configured OTel processor.""" + existing_processor = StructlogProcessor( + logger_provider=self.logger_provider + ) + structlog.configure( + processors=[ + existing_processor, + structlog.dev.ConsoleRenderer(), + ] + ) + + StructlogInstrumentor().instrument( + logger_provider=self.logger_provider + ) + StructlogInstrumentor().uninstrument() + + processors = structlog.get_config()["processors"] + self.assertIn(existing_processor, processors) + def test_instrument_without_prior_structlog_configuration_emits_logs(self): """Test instrument() emits logs before the app configures structlog.""" structlog.reset_defaults()