From b26ba4516056547d6d48e2f5d326b13e3ecd1b47 Mon Sep 17 00:00:00 2001 From: Jourdan Rodrigues Date: Fri, 25 Jul 2025 13:30:47 -0300 Subject: [PATCH 1/5] :ambulance: Handle "LogRecord.exc_info" being a string --- .../opentelemetry/sdk/_logs/_internal/__init__.py | 5 +++++ opentelemetry-sdk/tests/logs/test_handler.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 505904839b8..83153004e69 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -19,6 +19,7 @@ import concurrent.futures import json import logging +import sys import threading import traceback import warnings @@ -568,8 +569,12 @@ def _get_attributes(record: logging.LogRecord) -> _ExtendedAttributes: attributes[code_attributes.CODE_FUNCTION_NAME] = record.funcName attributes[code_attributes.CODE_LINE_NUMBER] = record.lineno + if isinstance(record.exc_info, str): + record.exc_info = sys.exc_info() + if record.exc_info: exctype, value, tb = record.exc_info + if exctype is not None: attributes[exception_attributes.EXCEPTION_TYPE] = ( exctype.__name__ diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 55526dc2b6a..fe14f041c46 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -14,6 +14,7 @@ import logging import os +import sys import unittest from unittest.mock import Mock, patch @@ -48,6 +49,20 @@ def test_handler_default_log_level(self): logger.warning("Warning message") self.assertEqual(processor.emit_count(), 1) + def test_handler_error_exc_info(self): + processor, logger = set_up_test_logging(logging.NOTSET) + exc_info_values = [ + # Don't know what caused it in my context, so I'm relying on mocks to replicate the behavior. + # First the `record.exc_info` becomes a string somehow, then `sys.exc_info` brings the tuple. + "Stringified exception", + (None, None, None), + ] + + with patch.object(sys, "exc_info", side_effect=exc_info_values): + logger.exception("Exception message") # Should not raise exception + + assert processor.emit_count() == 1 + def test_handler_custom_log_level(self): processor, logger = set_up_test_logging(logging.ERROR) From c967cef740476d671e815791a12cfb74e854645e Mon Sep 17 00:00:00 2001 From: Jourdan Rodrigues Date: Fri, 25 Jul 2025 15:11:10 -0300 Subject: [PATCH 2/5] :umbrella: Assert exception properties --- opentelemetry-sdk/tests/logs/test_handler.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index fe14f041c46..77adb05a3ef 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -15,6 +15,7 @@ import logging import os import sys +import traceback import unittest from unittest.mock import Mock, patch @@ -51,11 +52,21 @@ def test_handler_default_log_level(self): def test_handler_error_exc_info(self): processor, logger = set_up_test_logging(logging.NOTSET) + + class CustomException(Exception): + pass + + try: + raise CustomException("Custom exception") + except CustomException as exception: + exc_info = (type(exception), exception, exception.__traceback__) + traceback_str = "".join(traceback.format_exception(*exc_info)) + exc_info_values = [ # Don't know what caused it in my context, so I'm relying on mocks to replicate the behavior. # First the `record.exc_info` becomes a string somehow, then `sys.exc_info` brings the tuple. "Stringified exception", - (None, None, None), + exc_info, ] with patch.object(sys, "exc_info", side_effect=exc_info_values): @@ -63,6 +74,11 @@ def test_handler_error_exc_info(self): assert processor.emit_count() == 1 + attributes = processor.log_data_emitted[0].log_record.attributes._dict + assert attributes["exception.type"] == "CustomException" + assert attributes["exception.message"] == str(exc_info[1]) + assert attributes["exception.stacktrace"] == traceback_str + def test_handler_custom_log_level(self): processor, logger = set_up_test_logging(logging.ERROR) From 6c43c3296191dac0f3b08972ea5cfa2cc14ecb77 Mon Sep 17 00:00:00 2001 From: Jourdan Rodrigues Date: Fri, 25 Jul 2025 19:34:47 -0300 Subject: [PATCH 3/5] :pencil: Undo unnecessary change --- .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 83153004e69..3cee79f4388 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -574,7 +574,6 @@ def _get_attributes(record: logging.LogRecord) -> _ExtendedAttributes: if record.exc_info: exctype, value, tb = record.exc_info - if exctype is not None: attributes[exception_attributes.EXCEPTION_TYPE] = ( exctype.__name__ From b500d0f1c9b28ae83af998c7a0d8e7d1d3f91699 Mon Sep 17 00:00:00 2001 From: Jourdan Rodrigues Date: Tue, 29 Jul 2025 09:40:49 -0300 Subject: [PATCH 4/5] :sparkles: Address linting and documentation issues --- CHANGELOG.md | 1 + opentelemetry-sdk/tests/logs/test_handler.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b80ef9ec32..da2a2e1e3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Handle `LoggingHandler.exc_info` occasionally being a string ([#4699](https://github.com/open-telemetry/opentelemetry-python/pull/4699)) - Add missing Prometheus exporter documentation ([#4485](https://github.com/open-telemetry/opentelemetry-python/pull/4485)) - Overwrite logging.config.fileConfig and logging.config.dictConfig to ensure diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 77adb05a3ef..f2838d8964a 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -60,12 +60,13 @@ class CustomException(Exception): raise CustomException("Custom exception") except CustomException as exception: exc_info = (type(exception), exception, exception.__traceback__) - traceback_str = "".join(traceback.format_exception(*exc_info)) + else: + exc_info = "Second stringified exception" exc_info_values = [ # Don't know what caused it in my context, so I'm relying on mocks to replicate the behavior. # First the `record.exc_info` becomes a string somehow, then `sys.exc_info` brings the tuple. - "Stringified exception", + "First stringified exception", exc_info, ] @@ -77,7 +78,10 @@ class CustomException(Exception): attributes = processor.log_data_emitted[0].log_record.attributes._dict assert attributes["exception.type"] == "CustomException" assert attributes["exception.message"] == str(exc_info[1]) - assert attributes["exception.stacktrace"] == traceback_str + assert isinstance(exc_info, tuple) + assert attributes["exception.stacktrace"] == "".join( + traceback.format_exception(*exc_info) + ) def test_handler_custom_log_level(self): processor, logger = set_up_test_logging(logging.ERROR) From a26437e85ca602c4a7ba29eb96b3a43a818272e6 Mon Sep 17 00:00:00 2001 From: Jourdan Rodrigues Date: Tue, 29 Jul 2025 09:44:24 -0300 Subject: [PATCH 5/5] :books: Adhere to the changelog format --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da2a2e1e3b9..47725c7571f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -- Handle `LoggingHandler.exc_info` occasionally being a string ([#4699](https://github.com/open-telemetry/opentelemetry-python/pull/4699)) +- Fix logic to deal with `LoggingHandler.exc_info` occasionally being a string ([#4699](https://github.com/open-telemetry/opentelemetry-python/pull/4699)) - Add missing Prometheus exporter documentation ([#4485](https://github.com/open-telemetry/opentelemetry-python/pull/4485)) - Overwrite logging.config.fileConfig and logging.config.dictConfig to ensure