From ebbd70a386cd28a3ea7d28a151b0751d49bfe6f6 Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Mon, 26 May 2025 19:14:10 +1000 Subject: [PATCH 1/4] [core] Add support for comma format Fixes #15 --- docs/changelog.md | 2 ++ src/pythonjsonlogger/core.py | 25 ++++++++++++++++++++----- tests/test_formatters.py | 21 +++++++++++++++------ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index ccec8cb..749a3b5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support `DictConfigurator` prefixes for `rename_fields` and `static_fields`. [#45](https://github.com/nhairs/python-json-logger/pull/45) - Allows using values like `ext://sys.stderr` in `fileConfig`/`dictConfig` value fields. +- Support comma seperated lists for Formatter `fmt` (`style=","`) e.g. `"asctime,message,levelname"` [#15](https://github.com/nhairs/python-json-logger/issues/15) + - Note that this style is specific to `python-json-logger` and thus care should be taken not to pass this format to other logging Formatter implementations. ### Changed - Rename `pythonjsonlogger.core.LogRecord` and `log_record` arguments to avoid confusion / overlapping with `logging.LogRecord`. [#38](https://github.com/nhairs/python-json-logger/issues/38) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 6fd7285..6629cbf 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -175,6 +175,12 @@ def __init__( - Renaming fields now preserves the order that fields were added in and avoids adding missing fields. The original behaviour, missing fields have a value of `None`, is still available by setting `rename_fields_keep_missing` to `True`. + + *Added in 4.0*: + + - `fmt` now supports comma seperated lists (`style=","`). Note that this style is specific + to `python-json-logger` and thus care should be taken to not to pass this format to other + logging Formatter implementations. """ ## logging.Formatter compatibility ## --------------------------------------------------------------------- @@ -186,7 +192,7 @@ def __init__( self._style = _style self._fmt = _style._fmt - elif not validate: + elif style == "," or not validate: self._style = style self._fmt = fmt @@ -271,6 +277,18 @@ def parse(self) -> List[str]: Returns: list of fields to be extracted and serialized """ + if self._fmt is None: + # TODO: does it matter that we do this before checking if the style is valid? + # (we already (mostly) check for valid style names in __init__ + return [] + + if isinstance(self._style, str) and self._style == ",": + # TODO: should we check that there are no empty fields? + # If yes we should do this in __init__ where we validate other styles? + # Do we drop empty fields? + # etc + return [field.strip() for field in self._fmt.split(",") if field.strip()] + if isinstance(self._style, logging.StringTemplateStyle): formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX @@ -285,10 +303,7 @@ def parse(self) -> List[str]: else: raise ValueError(f"Style {self._style!r} is not supported") - if self._fmt: - return formatter_style_pattern.findall(self._fmt) - - return [] + return formatter_style_pattern.findall(self._fmt) def serialize_log_record(self, log_data: LogData) -> str: """Returns the final representation of the data to be logged diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 35ebe5e..332eae3 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -158,12 +158,21 @@ def test_default_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter] @pytest.mark.parametrize("class_", ALL_FORMATTERS) def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): - env.set_formatter( - class_( - # All kind of different styles to check the regex - "[%(levelname)8s] %(message)s %(filename)s:%(lineno)d %(asctime)" - ) - ) + # Note: All kind of different %s styles to check the regex + env.set_formatter(class_("[%(levelname)8s] %(message)s %(filename)s:%(lineno)d %(asctime)")) + + msg = "testing logging format" + env.logger.info(msg) + log_json = env.load_json() + + assert log_json["message"] == msg + assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"} + return + + +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + env.set_formatter(class_("levelname,message,filename,lineno,asctime", style=",")) msg = "testing logging format" env.logger.info(msg) From d1d8c58592e63fd7e94f33592a95cb3b27fdf48a Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Mon, 26 May 2025 19:18:54 +1000 Subject: [PATCH 2/4] Update tests --- tests/test_formatters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 332eae3..0a0f458 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -172,7 +172,8 @@ def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatt @pytest.mark.parametrize("class_", ALL_FORMATTERS) def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): - env.set_formatter(class_("levelname,message,filename,lineno,asctime", style=",")) + # Note: we have double comma `,,` to test handling "empty" names + env.set_formatter(class_("levelname,,message,filename,lineno,asctime,", style=",")) msg = "testing logging format" env.logger.info(msg) From 9270d7315fb706483c72725a49462579ed3c7e63 Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Sat, 6 Sep 2025 19:31:01 +1000 Subject: [PATCH 3/4] Address todos / review comments --- src/pythonjsonlogger/core.py | 10 +++------- tests/test_formatters.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 6629cbf..2b61624 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -184,7 +184,7 @@ def __init__( """ ## logging.Formatter compatibility ## --------------------------------------------------------------------- - # Note: validate added in 3.8, defaults added in 3.10 + # Note: validate added in python 3.8, defaults added in 3.10 if style in logging._STYLES: _style = logging._STYLES[style][0](fmt) # type: ignore[operator] if validate: @@ -196,6 +196,8 @@ def __init__( self._style = style self._fmt = fmt + # TODO: Validate comma format + else: raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}") @@ -278,15 +280,9 @@ def parse(self) -> List[str]: list of fields to be extracted and serialized """ if self._fmt is None: - # TODO: does it matter that we do this before checking if the style is valid? - # (we already (mostly) check for valid style names in __init__ return [] if isinstance(self._style, str) and self._style == ",": - # TODO: should we check that there are no empty fields? - # If yes we should do this in __init__ where we validate other styles? - # Do we drop empty fields? - # etc return [field.strip() for field in self._fmt.split(",") if field.strip()] if isinstance(self._style, logging.StringTemplateStyle): diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 0a0f458..6f8bd17 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -158,7 +158,7 @@ def test_default_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter] @pytest.mark.parametrize("class_", ALL_FORMATTERS) def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): - # Note: All kind of different %s styles to check the regex + # Note: We use different %s styles in the format to check the regex correctly collects them env.set_formatter(class_("[%(levelname)8s] %(message)s %(filename)s:%(lineno)d %(asctime)")) msg = "testing logging format" From 7b719ad280127c11e9e546be352fd0cfd21c0eff Mon Sep 17 00:00:00 2001 From: Nicholas Hairs Date: Sat, 6 Sep 2025 19:35:51 +1000 Subject: [PATCH 4/4] Update __init__ invalid style error --- 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 2b61624..10ac6dc 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -199,7 +199,7 @@ def __init__( # TODO: Validate comma format else: - raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}") + raise ValueError("Style must be one of: '%{$,'") self.datefmt = datefmt