diff --git a/docs/changelog.md b/docs/changelog.md index 00fb3b3..34de13a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,8 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Logging a `dict` no longer modifies it. `exc_info` and `stack_info` were previously added to the caller's `dict`. [#66](https://github.com/nhairs/python-json-logger/pull/66) +- `$` style formats now support unbraced `$name` fields, not just `${name}`. [#18](https://github.com/nhairs/python-json-logger/issues/18) -Thanks @gaoflow, @prateek-dagar +Thanks @gaoflow, @prateek-dagar, @Sanjays2402 ## [4.1.0](https://github.com/nhairs/python-json-logger/compare/v4.0.0...v4.1.0) - 2026-03-29 diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 8807aed..c084027 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -61,7 +61,9 @@ RESERVED_ATTRS.sort() -STYLE_STRING_TEMPLATE_REGEX = re.compile(r"\$\{(.+?)\}", re.IGNORECASE) # $ style +STYLE_STRING_TEMPLATE_REGEX = re.compile( + r"\$(?:\$|\{(?P.+?)\}|(?P[_a-z][_a-z0-9]*))", re.IGNORECASE +) # $ style STYLE_STRING_FORMAT_REGEX = re.compile(r"\{(.+?)\}", re.IGNORECASE) # { style STYLE_PERCENT_REGEX = re.compile(r"%\((.+?)\)", re.IGNORECASE) # % style @@ -302,20 +304,22 @@ def parse(self) -> list[str]: raise ValueError(f"Style {self._style!r} is not supported") if isinstance(self._style, logging.StringTemplateStyle): - formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX + # String templates support both ${name} and $name, and $$ is an escaped literal + return [ + match.group("braced") or match.group("named") + for match in STYLE_STRING_TEMPLATE_REGEX.finditer(self._fmt) + if match.group("braced") or match.group("named") + ] - elif isinstance(self._style, logging.StrFormatStyle): - formatter_style_pattern = STYLE_STRING_FORMAT_REGEX + if isinstance(self._style, logging.StrFormatStyle): + return STYLE_STRING_FORMAT_REGEX.findall(self._fmt) - elif isinstance(self._style, logging.PercentStyle): + if isinstance(self._style, logging.PercentStyle): # PercentStyle is parent class of StringTemplateStyle and StrFormatStyle # so it must be checked last. - formatter_style_pattern = STYLE_PERCENT_REGEX + return STYLE_PERCENT_REGEX.findall(self._fmt) - else: - raise ValueError(f"Style {self._style!r} is not supported") - - return formatter_style_pattern.findall(self._fmt) + raise ValueError(f"Style {self._style!r} is not supported") 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 01e2cf5..bc234ec 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -167,6 +167,22 @@ def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatt return +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_string_template_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + # Note: string templates support both $name and ${name}, and $$ is an escaped literal $ + env.set_formatter( + class_("$$literal $levelname ${message} $filename ${lineno} $asctime", style="$") + ) + + 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]): # Note: we have double comma `,,` to test handling "empty" names