From f266f86aadc3114d554aeec7558569e7e5ba6a45 Mon Sep 17 00:00:00 2001 From: Bar Harel Date: Sun, 10 Nov 2024 04:28:04 +0000 Subject: [PATCH] Use defaults argument (#26) closes #24 Uses `defaults` argument to prepopulate fields on records. ### Test Plan Unit tests --------- Co-authored-by: Nicholas Hairs --- docs/changelog.md | 1 + docs/quickstart.md | 12 +++++++++++ pyproject.toml | 2 +- src/pythonjsonlogger/core.py | 9 ++++++++- tests/test_formatters.py | 39 ++++++++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 144f370..5c23b27 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `pythonjsonlogger.[ORJSON,MSGSPEC]_AVAILABLE` no longer imports the respective package when determining availability. - `pythonjsonlogger.[orjson,msgspec]` now throws a `pythonjsonlogger.exception.MissingPackageError` when required libraries are not available. These contain more information about what is missing whilst still being an `ImportError`. +- `defaults` parameter is no longer ignored and now conforms to the standard library. Setting a defaults dictionary will add the specified keys if the those keys do not exist in a record or weren't passed by the `extra` parameter when logging a message. ## [3.1.0](https://github.com/nhairs/python-json-logger/compare/v3.0.1...v3.1.0) - 2023-05-28 diff --git a/docs/quickstart.md b/docs/quickstart.md index 3b78e5e..dc7a032 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -78,6 +78,18 @@ logger.info( Finally, any non-standard attributes added to a `LogRecord` will also be included in the logged data. See [Cookbook: Request / Trace IDs](cookbook.md#request-trace-ids) for an example. +#### Default Fields + +Default fields that are added to every log record prior to any other field can be set using the `default` argument. + +```python +formatter = JsonFormatter( + defaults={"environment": "dev"} +) +# ... +logger.info("this overwrites the environment field", extras={"environment": "dev"}) +``` + #### Static Fields Static fields that are added to every log record can be set using the `static_fields` argument. diff --git a/pyproject.toml b/pyproject.toml index 5055290..c05790a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-json-logger" -version = "3.1.0" +version = "3.2.0.dev1" description = "JSON Log Formatter for the Python Logging Package" authors = [ {name = "Zakaria Zajac", email = "zak@madzak.com"}, diff --git a/src/pythonjsonlogger/core.py b/src/pythonjsonlogger/core.py index 820aa94..27501b2 100644 --- a/src/pythonjsonlogger/core.py +++ b/src/pythonjsonlogger/core.py @@ -132,6 +132,8 @@ class BaseJsonFormatter(logging.Formatter): Must not be used directly. *New in 3.1* + + *Changed in 3.2*: `defaults` argument is no longer ignored. """ _style: Union[logging.PercentStyle, str] # type: ignore[assignment] @@ -161,7 +163,8 @@ def __init__( style: how to extract log fields from `fmt` validate: validate `fmt` against style, if implementing a custom `style` you must set this to `False`. - defaults: ignored - kept for compatibility with python 3.10+ + defaults: a dictionary containing default fields that are added before all other fields and + may be overridden. The supplied fields are still subject to `rename_fields`. prefix: an optional string prefix added at the beginning of the formatted string rename_fields: an optional dict, used to rename field names in the output. @@ -215,6 +218,7 @@ def __init__( self._required_fields = self.parse() self._skip_fields = set(self._required_fields) self._skip_fields.update(self.reserved_attrs) + self.defaults = defaults if defaults is not None else {} return def format(self, record: logging.LogRecord) -> str: @@ -310,6 +314,9 @@ def add_fields( message_dict: dictionary that was logged instead of a message. e.g `logger.info({"is_this_message_dict": True})` """ + for field in self.defaults: + log_record[self._get_rename(field)] = self.defaults[field] + for field in self._required_fields: log_record[self._get_rename(field)] = record.__dict__.get(field) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index cbaf886..2212429 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -174,6 +174,18 @@ def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatt return +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_defaults_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + env.set_formatter(class_(defaults={"first": 1, "second": 2})) + + env.logger.info("testing defaults field", extra={"first": 1234}) + log_json = env.load_json() + + assert log_json["first"] == 1234 + assert log_json["second"] == 2 + 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"})) @@ -186,6 +198,20 @@ def test_rename_base_field(env: LoggingEnvironment, class_: type[BaseJsonFormatt return +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_rename_with_defaults(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + """Make sure that the default fields are also renamed.""" + env.set_formatter(class_(rename_fields={"custom": "@custom"}, defaults={"custom": 1234})) + + msg = "testing rename with defaults" + env.logger.info(msg) + log_json = env.load_json() + + assert log_json["@custom"] == 1234 + assert "custom" not in log_json + return + + @pytest.mark.parametrize("class_", ALL_FORMATTERS) def test_rename_missing(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): env.set_formatter(class_(rename_fields={"missing_field": "new_field"})) @@ -321,6 +347,19 @@ def test_log_dict(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): return +@pytest.mark.parametrize("class_", ALL_FORMATTERS) +def test_log_dict_defaults(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): + env.set_formatter(class_(defaults={"d1": 1234, "d2": "hello"})) + + msg = {"d2": "world"} + env.logger.info(msg) + log_json = env.load_json() + + assert log_json["d1"] == 1234 + assert log_json["d2"] == "world" + return + + @pytest.mark.parametrize("class_", ALL_FORMATTERS) def test_log_extra(env: LoggingEnvironment, class_: type[BaseJsonFormatter]): env.set_formatter(class_())