From f8587bed6d61e8f2fcf09c818ce136f569a1c958 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Sat, 4 Jan 2025 10:12:03 +0900 Subject: [PATCH 01/12] Add feature to encode stack frames as an array --- src/pythonjsonlogger/core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 27501b2..5c94bbe 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -155,6 +155,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 +179,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 +223,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: @@ -247,11 +253,17 @@ def format(self, record: logging.LogRecord) -> str: if not message_dict.get("exc_info") and record.exc_text: message_dict["exc_info"] = record.exc_text + if self.exc_info_as_array and message_dict.get("exc_info"): + message_dict["exc_info"] = message_dict["exc_info"].splitlines() + # Display formatted record of stack frames # default format is a string returned from :func:`traceback.print_stack` if record.stack_info and not message_dict.get("stack_info"): message_dict["stack_info"] = self.formatStack(record.stack_info) + if self.stack_info_as_array and message_dict.get("stack_info"): + message_dict["stack_info"] = message_dict["stack_info"].splitlines() + log_record: LogRecord = {} self.add_fields(log_record, record, message_dict) log_record = self.process_log_record(log_record) From 1f381c7a8da3130084f5743ced146046ff38f673 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Sat, 4 Jan 2025 10:12:21 +0900 Subject: [PATCH 02/12] Add test cases --- tests/test_formatters.py | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index b15c911..315cdc2 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 type(log_json["exc_info"]) is 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 log_json.get("exc_info") is None + 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 type(log_json["stack_info"]) is 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 log_json.get("stack_info") is None + return + + ## JsonFormatter Specific ## ----------------------------------------------------------------------------- def test_json_ensure_ascii_true(env: LoggingEnvironment): From 7dfc5bb03b66043a21dd275b0d713069abb09011 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Mon, 6 Jan 2025 15:06:33 +0900 Subject: [PATCH 03/12] Override formatException and formatStack --- src/pythonjsonlogger/core.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 5c94bbe..326936e 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -253,17 +253,11 @@ def format(self, record: logging.LogRecord) -> str: if not message_dict.get("exc_info") and record.exc_text: message_dict["exc_info"] = record.exc_text - if self.exc_info_as_array and message_dict.get("exc_info"): - message_dict["exc_info"] = message_dict["exc_info"].splitlines() - # Display formatted record of stack frames # default format is a string returned from :func:`traceback.print_stack` if record.stack_info and not message_dict.get("stack_info"): message_dict["stack_info"] = self.formatStack(record.stack_info) - if self.stack_info_as_array and message_dict.get("stack_info"): - message_dict["stack_info"] = message_dict["stack_info"].splitlines() - log_record: LogRecord = {} self.add_fields(log_record, record, message_dict) log_record = self.process_log_record(log_record) @@ -380,3 +374,23 @@ def process_log_record(self, log_record: LogRecord) -> LogRecord: log_record: incoming data """ return log_record + + def formatException(self, ei) -> Union[str, list[str]]: + """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]]: + """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.formatStack(stack_info) + ) From ddfbafe1a13380ac991b608af31d4b17ddf3e28d Mon Sep 17 00:00:00 2001 From: 1hakusai1 <55519230+1hakusai1@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:51:45 +0900 Subject: [PATCH 04/12] Use isinstance for checking types Co-authored-by: Nicholas Hairs --- tests/test_formatters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 315cdc2..8dc1733 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -632,7 +632,7 @@ def test_exc_info_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatt env.logger.exception("Error occurs") log_json = env.load_json() - assert type(log_json["exc_info"]) is list + assert isinstance(log_json["exc_info"], list) return From 118a18b8ba1019db80b22129cfab76d04952fec2 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <55519230+1hakusai1@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:53:39 +0900 Subject: [PATCH 05/12] Use not in instead of dict.get Co-authored-by: Nicholas Hairs --- tests/test_formatters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 8dc1733..ff4ab4b 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -643,7 +643,7 @@ def test_exc_info_as_array_no_exc_info(env: LoggingEnvironment, class_: type[Bas env.logger.info("hello") log_json = env.load_json() - assert log_json.get("exc_info") is None + assert "exc_info" not in log_json return From c47513925f67a389e2ac0a0734c352cadd362c09 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <55519230+1hakusai1@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:53:57 +0900 Subject: [PATCH 06/12] Use not in instead of dict.get Co-authored-by: Nicholas Hairs --- tests/test_formatters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index ff4ab4b..b487461 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -667,7 +667,7 @@ def test_stack_info_as_array_no_stack_info( env.logger.info("hello", stack_info=False) log_json = env.load_json() - assert log_json.get("stack_info") is None + assert "stack_info" not in log_json return From 6d177597906a90ff8a63f7e5059db9d4603a0ace Mon Sep 17 00:00:00 2001 From: 1hakusai1 <55519230+1hakusai1@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:54:19 +0900 Subject: [PATCH 07/12] Use isinstance for type checking Co-authored-by: Nicholas Hairs --- tests/test_formatters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index b487461..050fc5e 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -654,7 +654,7 @@ def test_stack_info_as_array(env: LoggingEnvironment, class_: type[BaseJsonForma env.logger.info("hello", stack_info=True) log_json = env.load_json() - assert type(log_json["stack_info"]) is list + assert isinstance(log_json["stack_info"], list) return From ff8edd820ac0c9388e901670c82901360aae192f Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Sun, 2 Feb 2025 23:19:22 +0900 Subject: [PATCH 08/12] Ignore mypy rule "incompatible with return type in supertype" --- src/pythonjsonlogger/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 326936e..dfbad8e 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -375,7 +375,7 @@ def process_log_record(self, log_record: LogRecord) -> LogRecord: """ return log_record - def formatException(self, ei) -> Union[str, list[str]]: + 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. @@ -383,7 +383,7 @@ def formatException(self, ei) -> Union[str, list[str]]: 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]]: + 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. From 1d438d26e9ff595b49fc4e6995770e49404f7484 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Sun, 2 Feb 2025 23:20:09 +0900 Subject: [PATCH 09/12] stack_info_str is already formatted --- src/pythonjsonlogger/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index dfbad8e..390c804 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -392,5 +392,5 @@ def formatStack(self, stack_info) -> Union[str, list[str]]: #type: ignore return ( stack_info_str.splitlines() if self.stack_info_as_array - else stack_info_str.formatStack(stack_info) + else stack_info_str ) From ae98635964b0dc17f48a6f2a069e6efcd6f54837 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Sun, 2 Feb 2025 23:32:09 +0900 Subject: [PATCH 10/12] Update the change log --- docs/changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) 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 From 94c37bfcb360604f6b5a8cc3a63be758f12a084f Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Sun, 2 Feb 2025 23:34:32 +0900 Subject: [PATCH 11/12] Update the documentation for BaseJsonFormatter --- src/pythonjsonlogger/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 390c804..37087ee 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] From 9759985d0982e2cef5d8ed62cfea8202d99564c3 Mon Sep 17 00:00:00 2001 From: 1hakusai1 <1hakusai1@gmail.com> Date: Mon, 3 Feb 2025 23:09:46 +0900 Subject: [PATCH 12/12] Format --- src/pythonjsonlogger/core.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 37087ee..1a4dee3 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -377,7 +377,7 @@ def process_log_record(self, log_record: LogRecord) -> LogRecord: """ return log_record - def formatException(self, ei) -> Union[str, list[str]]: #type: ignore + 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. @@ -385,14 +385,10 @@ def formatException(self, ei) -> Union[str, list[str]]: #type: ignore 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 + 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 - ) + return stack_info_str.splitlines() if self.stack_info_as_array else stack_info_str