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
3 changes: 2 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [4.2.1](https://github.com/nhairs/python-json-logger/compare/v4.2.0...v4.2.1) - UNRELEASED

### Added
- Documentation: Quickstart examples are tested to ensure the sample output matches reality.
- Add support for Python 3.15, PyPy 3.12

### Removed
Expand All @@ -20,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `%` 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
Thanks @dylanpulver, @Gonghan-Princess

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

Expand Down
61 changes: 58 additions & 3 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import logging
from pythonjsonlogger.json import JsonFormatter

logger = logging.getLogger()
logger.setLevel(logging.INFO)

logHandler = logging.StreamHandler()
formatter = JsonFormatter()
Expand Down Expand Up @@ -98,25 +99,79 @@ Finally, any non-standard attributes added to a `LogRecord` will also be include

#### Default Fields

Default fields that are added to every log record prior to any other field can be set using the `default` argument.
Default fields that are added to every log record prior to any other field can be set using the `defaults` argument.

```python
formatter = JsonFormatter(
defaults={"environment": "dev"}
)
# ...
logHandler.setFormatter(formatter)
logger.info("this message will have environment=dev by default")
logger.info("this overwrites the environment field", extra={"environment": "prod"})
```

Output:

```json
{"environment": "dev", "message": "this message will have environment=dev by default"}
{"environment": "prod", "message": "this overwrites the environment field"}
```

#### Static Fields

Static fields that are added to every log record can be set using the `static_fields` argument.

```python
formatter = JsonFormatter(
static_fields={"True gets logged on every record?": True}
static_fields={"service": "billing"}
)
logHandler.setFormatter(formatter)
logger.info("processing a request")
```

Output:

```json
{"message": "processing a request", "service": "billing"}
```

Despite the name, static fields can be overridden by message fields and `extra` fields, as described below.

#### Field Precedence

These field sources are added in the following order. A later source replaces an earlier value for the same output key:

1. `defaults`
2. Fields selected by `fmt`
3. `static_fields`
4. Fields from a dictionary message
5. Non-reserved `LogRecord` attributes, including those supplied through `extra`

Fields selected by `fmt` are handled in step 2 and are not merged again in step 5. This is why `static_fields` can override a field selected by `fmt`, even when that field was supplied through `extra`. All five sources are subject to `rename_fields`.

The following example shows a static value overriding both a default and the record's `levelname`, while dictionary messages and `extra` can override the static `environment`:

```python
formatter = JsonFormatter(
["message", "levelname"],
defaults={"environment": "dev", "levelname": "DEFAULT"},
static_fields={"environment": "test", "levelname": "STATIC"},
)
logHandler.setFormatter(formatter)
logger.info("static fields win over defaults and fmt")
logger.info({"message": "dictionary message wins", "environment": "staging"})
logger.info(
{"message": "extra wins", "environment": "staging"},
extra={"environment": "prod"},
)
```

Output:

```json
{"environment": "test", "levelname": "STATIC", "message": "static fields win over defaults and fmt"}
{"environment": "staging", "levelname": "STATIC", "message": "dictionary message wins"}
{"environment": "prod", "levelname": "STATIC", "message": "extra wins"}
```

### Excluding fields
Expand Down
39 changes: 39 additions & 0 deletions tests/test_quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Keep the Quick Start's displayed JSON output in sync with its examples."""

import json
from pathlib import Path
import re
import subprocess
import sys

import pytest


@pytest.mark.parametrize("section", ["Default Fields", "Static Fields", "Field Precedence"])
def test_output_field_examples(section: str) -> None:
quickstart = (Path(__file__).resolve().parents[1] / "docs" / "quickstart.md").read_text(
encoding="utf-8"
)
setup = re.search(
r"### Integrating with Python's logging framework.*?```python\n(.*?)```",
quickstart,
re.DOTALL,
)
assert setup is not None
example = quickstart.split(f"#### {section}\n", 1)[1].split("\n###", 1)[0]
code = re.search(r"```python\n(.*?)```", example, re.DOTALL)
output = re.search(r"```json\n(.*?)```", example, re.DOTALL)
assert code is not None
assert output is not None

result = subprocess.run(
[sys.executable, "-c", setup.group(1) + "\n" + code.group(1)],
capture_output=True,
text=True,
check=True,
timeout=10,
)
actual = [json.loads(line) for line in result.stderr.splitlines()]
expected = [json.loads(line) for line in output.group(1).splitlines()]
assert actual == expected
return None
Loading