Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to encode stack information into an array #39

Merged
merged 12 commits into from
Feb 4, 2025
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/pythonjsonlogger/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand All @@ -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*:

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
49 changes: 49 additions & 0 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down