Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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)
- `defaults` is copied on construction like `rename_fields` and `static_fields`, so DictConfigurator `ext://` prefixes are converted and mutating the caller's dict cannot change formatter state.
- A string passed as `reserved_attrs` is treated as a single field name instead of being iterated as characters.

Thanks @dylanpulver

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.1.dev1"
version = "4.2.1.dev2"
description = "JSON Log Formatter for the Python Logging Package"
authors = [
{name = "Zakaria Zajac", email = "zak@madzak.com"},
Expand Down
22 changes: 17 additions & 5 deletions src/pythonjsonlogger/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ def __init__(
rename_fields_keep_missing: When renaming fields, include missing fields in the output.
static_fields: an optional dict, used to add fields with static values to all logs
reserved_attrs: an optional list of fields that will be skipped when
outputting json log record. Defaults to [all log record attributes][pythonjsonlogger.core.RESERVED_ATTRS].
outputting json log record. A single field name may be passed as a
string. Defaults to [all log record attributes][pythonjsonlogger.core.RESERVED_ATTRS].
timestamp: an optional string/boolean field to add a timestamp when
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
Expand All @@ -180,6 +181,11 @@ def __init__(
to `python-json-logger` and thus care should be taken to not to pass this format to other
logging Formatter implementations.
- `fmt` now supports sequences of strings (e.g. lists and tuples) of field names.

*Changed in 4.2.1*:

- `defaults` is copied on construction, matching `rename_fields` and `static_fields`.
- A string `reserved_attrs` value is treated as a single field name.
"""
## logging.Formatter compatibility
## ---------------------------------------------------------------------
Expand Down Expand Up @@ -213,23 +219,29 @@ def __init__(
## ---------------------------------------------------------------------
self.prefix = prefix

# We recreate the dict in rename_fields and static_fields to support internal/external
# references which require getting the item to do the conversion.
# Recreate these dicts so DictConfigurator `ext://` / `cfg://` prefixes
# are converted (ConvertingDict only converts on `__getitem__`) and so
# later mutation of the caller's dict cannot change formatter state.
# For more details see: https://github.com/nhairs/python-json-logger/pull/45
self.rename_fields = (
{key: rename_fields[key] for key in rename_fields} if rename_fields is not None else {}
)
self.static_fields = (
{key: static_fields[key] for key in static_fields} if static_fields is not None else {}
)
self.defaults = {key: defaults[key] for key in defaults} if defaults is not None else {}

self.rename_fields_keep_missing = rename_fields_keep_missing
self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
# `str` is a Sequence[str] of characters; treat a string as one field name.
if reserved_attrs is None:
reserved_attrs = RESERVED_ATTRS
elif isinstance(reserved_attrs, str):
reserved_attrs = (reserved_attrs,)
self.reserved_attrs = set(reserved_attrs)
self.timestamp = timestamp

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
Expand Down
30 changes: 30 additions & 0 deletions tests/test_dictconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,33 @@ def test_external_reference_support(env: LoggingEnvironment):
assert log_json["ext-val"] == EXT_VAL
assert log_json["dummy"] == "DUMMY"
return


def test_defaults_external_reference_support() -> None:
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "pythonjsonlogger.json.JsonFormatter",
"defaults": {"ext-val": "ext://tests.test_dictconfig.EXT_VAL"},
}
},
"handlers": {
"default": {
"level": "DEBUG",
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"": {"handlers": ["default"], "level": "WARNING", "propagate": False},
},
}
)
formatter = logging.root.handlers[0].formatter
assert formatter.defaults["ext-val"] == EXT_VAL # type: ignore[union-attr]
assert type(formatter.defaults) is dict # type: ignore[union-attr]
return
29 changes: 29 additions & 0 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,35 @@ def test_defaults_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_defaults_not_shared_with_caller(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
defaults = {"environment": "dev"}
formatter = class_(defaults=defaults)
assert formatter.defaults is not defaults
defaults["environment"] = "prod"
env.set_formatter(formatter)

env.logger.info("testing defaults isolation")
log_json = env.load_json()

assert log_json["environment"] == "dev"
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_reserved_attrs_string(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
env.set_formatter(class_(reserved_attrs="filename"))

env.logger.info("hello", extra={"user": "x"})
log_json = env.load_json()

assert log_json["message"] == "hello"
assert log_json["user"] == "x"
assert "filename" not in log_json
assert "levelname" in log_json
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_rename_base_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
env.set_formatter(class_(rename_fields={"message": "@message"}))
Expand Down
Loading