Repository: Colin-b/logging_json (language: python)

Issue:
Add exception_field parameter
Hello! We like to use your module in our application but we need to reuse exception field for our own purposes.
I think we have 2 ways:
- do not set exception field if already exists
- or add exception_field parameter which disables exception field

I have done second way. Can your review it please?


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7230f89..fe30c3b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 21.12b0
+    rev: 22.10.0
     hooks:
       - id: black
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05a476d..b516c83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+## [0.3.0] - 2022-12-02
+### Added
+- Added `exception_field_name` parameter.
+
 ## [0.2.1] - 2022-01-26
 ### Fixed
 - `datetime`, `time` and `date` instances are now represented following [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) format instead of raising a `TypeError`.
@@ -23,7 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 - Public release.
 
-[Unreleased]: https://github.com/Colin-b/logging_json/compare/v0.2.1...HEAD
+[Unreleased]: https://github.com/Colin-b/logging_json/compare/v0.3.0...HEAD
+[0.3.0]: https://github.com/Colin-b/logging_json/compare/v0.2.1...v0.3.0
 [0.2.1]: https://github.com/Colin-b/logging_json/compare/v0.2.0...v0.2.1
 [0.2.0]: https://github.com/Colin-b/logging_json/compare/v0.1.0...v0.2.0
 [0.1.0]: https://github.com/Colin-b/logging_json/compare/v0.0.1...v0.1.0
diff --git a/README.md b/README.md
index a39cef8..2a9affb 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,9 @@ This dictionary will contain 3 keys:
 * `message`: The str representation of the exception (usually the provided error message).
 * `stack`: The stack trace, formatted as a string.
 
+You can rename the exception field key by setting the `exception_field_name` parameter with a new name for the key.
+It is also possible to disable this behaviour by setting the `exception_field_name` parameter to `None` or an empty string
+
 ### Logging with a dictionary
 
 This formatter allows you to log dictionary as in the following:
@@ -57,7 +60,7 @@ The resulting JSON dictionary will be the one you provided (with the [additional
 Anything not logged using a dictionary will be handled by the standard formatter and it can result in one of the 2 output:
 * A JSON dictionary, if [additional fields](#adding-additional-fields-and-values) are set or if `extra` parameter is used while logging, with the message available in the `msg` key of the resulting JSON dictionary.
   Default `msg` key name can be changed by `message_field_name` parameter of the `logging_json.JSONFormatter` instance.
-* The formatted record, if no [additional fields](#adding-additional-fields-and-values) are set. 
+* The formatted record, if no [additional fields](#adding-additional-fields-and-values) are set.
 
 This handles the usual string logging as in the following:
 
diff --git a/logging_json/_formatter.py b/logging_json/_formatter.py
index f0a2fe5..7b14a8e 100644
--- a/logging_json/_formatter.py
+++ b/logging_json/_formatter.py
@@ -2,7 +2,7 @@
 import datetime
 import json
 import logging
-from typing import Any, Dict
+from typing import Any, Dict, Optional
 
 standard_attributes = (
     "name",
@@ -61,6 +61,7 @@ def __init__(
         *args,
         fields: Dict[str, Any] = None,
         message_field_name: str = "msg",
+        exception_field_name: Optional[str] = "exception",
         **kwargs,
     ):
         # Allow to provide any formatter setting (useful to provide a custom date format)
@@ -68,6 +69,7 @@ def __init__(
         self.fields = fields or {}
         self.usesTime = lambda: "asctime" in self.fields.values()
         self.message_field_name = message_field_name
+        self.exception_field_name = exception_field_name
 
     def format(self, record: logging.LogRecord):
         # Let python set every additional record field
@@ -84,8 +86,8 @@ def format(self, record: logging.LogRecord):
 
         message.update(_extra_attributes(record))
 
-        if record.exc_info:
-            message["exception"] = {
+        if self.exception_field_name and record.exc_info:
+            message[self.exception_field_name] = {
                 "type": record.exc_info[0].__name__,
                 "message": str(record.exc_info[1]),
                 "stack": self.formatException(record.exc_info),
diff --git a/logging_json/version.py b/logging_json/version.py
index 8cb9886..e2e359f 100644
--- a/logging_json/version.py
+++ b/logging_json/version.py
@@ -3,4 +3,4 @@
 # Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
 # Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
 # Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
-__version__ = "0.2.1"
+__version__ = "0.3.0"


Test patch:
diff --git a/tests/test_formatter.py b/tests/test_formatter.py
index f8a6443..35656b0 100644
--- a/tests/test_formatter.py
+++ b/tests/test_formatter.py
@@ -2,6 +2,8 @@
 import json
 import time
 import datetime
+import pytest
+import sys
 
 import logging_json
 
@@ -114,6 +116,38 @@ def test_dict_message_at_exception_level(caplog):
     }
 
 
+def test_dict_message_at_exception_level_with_different_field_name(caplog):
+    caplog.set_level("INFO")
+    try:
+        raise MyException("this is the exception message")
+    except MyException:
+        logging.exception({"key 1": "value 1", "key 2": 2})
+    actual = dict_fmt(caplog, exception_field_name="info_about_exception")
+    actual["info_about_exception"].pop("stack")
+    assert actual == {
+        "key 1": "value 1",
+        "key 2": 2,
+        "info_about_exception": {
+            "message": "this is the exception message",
+            "type": "MyException",
+        },
+    }
+
+
+@pytest.mark.parametrize("value", [None, ""])
+def test_dict_message_at_exception_level_without_exception_field(caplog, value):
+    caplog.set_level("INFO")
+    try:
+        raise MyException("this is the exception message")
+    except MyException:
+        logging.exception({"key 1": "value 1", "key 2": 2})
+    actual = dict_fmt(caplog, exception_field_name=value)
+    assert actual == {
+        "key 1": "value 1",
+        "key 2": 2,
+    }
+
+
 def test_str_message_at_exception_level(caplog):
     caplog.set_level("INFO")
     try:
@@ -179,6 +213,7 @@ def test_documented_record_attributes(caplog, monkeypatch):
     actual.pop("process_id")
     actual.pop("relative_timestamp")
     actual.pop("line_number")
+    python310 = sys.version_info.minor >= 10
     assert actual == {
         "extra": "this is a value",
         "file_name": "test_formatter.py",
@@ -191,7 +226,7 @@ def test_documented_record_attributes(caplog, monkeypatch):
         "record_message": "{}",
         "thread_name": "MainThread",
         "timestamp": 1599736353.0076675,
-        "timestamp_milliseconds": 7.66754150390625,
+        "timestamp_milliseconds": 7.0 if python310 else 7.66754150390625,
     }