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
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed
- Add runnable Quick Start examples and JSON outputs explaining defaults, static fields, and field precedence. [#36](https://github.com/nhairs/python-json-logger/issues/36)

## [4.2.0](https://github.com/nhairs/python-json-logger/compare/v4.1.0...v4.2.0) - 2026-08-15

### Changed
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