diff --git a/docs/changelog.md b/docs/changelog.md index 82d1cc7..dede806 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [UNRELEASED] + +### Added +- `exc_info_as_array` and `stack_info_as_array` options are added to `pythonjsonlogger.core.BaseJsonFormatter`. + - If `exc_info_as_array` is True (Defualt: False), formatter encode exc_info into an array. + - If `stack_info_as_array` is True (Defualt: False), formatter encode stack_info into an array. + ## [3.2.1](https://github.com/nhairs/python-json-logger/compare/v3.2.0...v3.2.1) - 2024-12-16 ### Fixed diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 27501b2..1a4dee3 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -134,6 +134,8 @@ class BaseJsonFormatter(logging.Formatter): *New in 3.1* *Changed in 3.2*: `defaults` argument is no longer ignored. + + *Added in UNRELEASED*: `exc_info_as_array` and `stack_info_as_array` options are added. """ _style: Union[logging.PercentStyle, str] # type: ignore[assignment] @@ -155,6 +157,8 @@ def __init__( reserved_attrs: Optional[Sequence[str]] = None, timestamp: Union[bool, str] = False, defaults: Optional[Dict[str, Any]] = None, + exc_info_as_array: bool = False, + stack_info_as_array: bool = False, ) -> None: """ Args: @@ -177,6 +181,8 @@ def __init__( outputting the json log record. If string is passed, timestamp will be added to log record using string as key. If True boolean is passed, timestamp key will be "timestamp". Defaults to False/off. + exc_info_as_array: break the exc_info into a list of lines based on line breaks. + stack_info_as_array: break the stack_info into a list of lines based on line breaks. *Changed in 3.1*: @@ -219,6 +225,8 @@ def __init__( self._skip_fields = set(self._required_fields) self._skip_fields.update(self.reserved_attrs) self.defaults = defaults if defaults is not None else {} + self.exc_info_as_array = exc_info_as_array + self.stack_info_as_array = stack_info_as_array return def format(self, record: logging.LogRecord) -> str: @@ -368,3 +376,19 @@ def process_log_record(self, log_record: LogRecord) -> LogRecord: log_record: incoming data """ return log_record + + def formatException(self, ei) -> Union[str, list[str]]: # type: ignore + """Format and return the specified exception information. + + If exc_info_as_array is set to True, This method returns an array of strings. + """ + exception_info_str = super().formatException(ei) + return exception_info_str.splitlines() if self.exc_info_as_array else exception_info_str + + def formatStack(self, stack_info) -> Union[str, list[str]]: # type: ignore + """Format and return the specified stack information. + + If stack_info_as_array is set to True, This method returns an array of strings. + """ + stack_info_str = super().formatStack(stack_info) + return stack_info_str.splitlines() if self.stack_info_as_array else stack_info_str diff --git a/tests/test_formatters.py b/tests/test_formatters.py index b15c911..050fc5e 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -622,6 +622,55 @@ def custom_default(obj): return +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_exc_info_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + env.set_formatter(class_(exc_info_as_array=True)) + + try: + raise Exception("Error") + except BaseException: + env.logger.exception("Error occurs") + log_json = env.load_json() + + assert isinstance(log_json["exc_info"], list) + return + + +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_exc_info_as_array_no_exc_info(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + env.set_formatter(class_(exc_info_as_array=True)) + + env.logger.info("hello") + log_json = env.load_json() + + assert "exc_info" not in log_json + return + + +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_stack_info_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + env.set_formatter(class_(stack_info_as_array=True)) + + env.logger.info("hello", stack_info=True) + log_json = env.load_json() + + assert isinstance(log_json["stack_info"], list) + return + + +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_stack_info_as_array_no_stack_info( + env: LoggingEnvironment, class_: type[BaseJsonFormatter] +): + env.set_formatter(class_(stack_info_as_array=True)) + + env.logger.info("hello", stack_info=False) + log_json = env.load_json() + + assert "stack_info" not in log_json + return + + ## JsonFormatter Specific ## ----------------------------------------------------------------------------- def test_json_ensure_ascii_true(env: LoggingEnvironment):