Repository: nhairs/python-json-logger

Issue:
Defaults parameter is ignored
The defaults parameter that Logging.Formatter takes is ignored. We can make it work 😄

I can submit a patch :-)

Discussion:
nhairs: Hi @bharel,

I plan to do a new point release as soon as #23 is ready (it is currently waiting on a new release of msgspec).

It would be good if this was ready to land in the same release.
bharel: Haven't run black on the last change. Should be good now.
nhairs: > I'm receiving import errors on mypy, but those are unrelated (running on py3.13) where msgspec doesn't exist yet...?

Yep that's fine - on CI we run it using 3.12 which I can see is passing :tada:

bharel: Oh you managed to push it I see :-)

Patch:
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 @@ class BaseJsonFormatter(logging.Formatter):
             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 @@ class BaseJsonFormatter(logging.Formatter):
         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 @@ class BaseJsonFormatter(logging.Formatter):
             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)
 


Test patch:
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_())