Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ 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).

## [4.2.1](https://github.com/nhairs/python-json-logger/compare/v4.2.0...v4.2.1) - UNRELEASED

### Removed
- `STYLE_STRING_FORMAT_REGEX`, which is no longer used to find `{` style fields. [#75](https://github.com/nhairs/python-json-logger/pull/75)

### Fixed
- `%` style formats no longer treat the escaped literal `%%` as the start of a field, so `"%%(notafield)s"` is correctly read as literal text. [#75](https://github.com/nhairs/python-json-logger/pull/75)
- `{` style formats now use `string.Formatter` (as `logging.StrFormatStyle.validate` does) to find fields, so escaped literal braces (`{{`/`}}`) are skipped and a conversion (`{message!r}`) or format spec (`{levelname:>8}`) is no longer treated as part of the field name. [#75](https://github.com/nhairs/python-json-logger/pull/75)

Thanks @dylanpulver

Comment thread
nhairs marked this conversation as resolved.
## [4.2.0](https://github.com/nhairs/python-json-logger/compare/v4.1.0...v4.2.0) - 2026-08-15

### Changed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "python-json-logger"
version = "4.2.0"
version = "4.2.1.dev1"
description = "JSON Log Formatter for the Python Logging Package"
authors = [
{name = "Zakaria Zajac", email = "zak@madzak.com"},
Expand Down
16 changes: 12 additions & 4 deletions src/pythonjsonlogger/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from datetime import datetime, timezone
import logging
import re
import string
import sys
from typing import TypeAlias, Any
from collections.abc import Container, Sequence
Expand Down Expand Up @@ -64,8 +65,7 @@
STYLE_STRING_TEMPLATE_REGEX = re.compile(
r"\$(?:\$|\{(?P<braced>.+?)\}|(?P<named>[_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
STYLE_PERCENT_REGEX = re.compile(r"%(?:%|\((?P<named>.+?)\))", re.IGNORECASE) # % style

## Type Aliases
## -----------------------------------------------------------------------------
Expand Down Expand Up @@ -311,12 +311,20 @@ def parse(self) -> list[str]:
]

if isinstance(self._style, logging.StrFormatStyle):
return STYLE_STRING_FORMAT_REGEX.findall(self._fmt)
return [
field_name
for _, field_name, _, _ in string.Formatter().parse(self._fmt)
if field_name
]

if isinstance(self._style, logging.PercentStyle):
# PercentStyle is parent class of StringTemplateStyle and StrFormatStyle
# so it must be checked last.
return STYLE_PERCENT_REGEX.findall(self._fmt)
return [
match.group("named")
for match in STYLE_PERCENT_REGEX.finditer(self._fmt)
if match.group("named")
]

raise ValueError(f"Style {self._style!r} is not supported")

Expand Down
29 changes: 27 additions & 2 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,14 @@ 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: 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)"))
# Note: We use different %s styles in the format to check the regex correctly collects them,
# and %% is an escaped literal percent so %%(notafield)s is not a field
env.set_formatter(
class_(
"[%(levelname)8s] %(message)s %(filename)s:%(lineno)d"
" 100%% %%(notafield)s %(asctime)"
)
)

msg = "testing logging format"
env.logger.info(msg)
Expand All @@ -183,6 +189,25 @@ def test_string_template_format(env: LoggingEnvironment, class_: type[BaseJsonFo
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_str_format_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
# Note: {{ }} is an escaped literal brace, and !r / :>{width} are not part of a field name
env.set_formatter(
class_(
"{{literal}} {levelname:>{width}} {message!r} {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
Expand Down
Loading