diff --git a/docs/changelog.md b/docs/changelog.md index 901974b..c437992 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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 diff --git a/docs/quickstart.md b/docs/quickstart.md index 56d2de8..1d1e181 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -35,6 +35,7 @@ import logging from pythonjsonlogger.json import JsonFormatter logger = logging.getLogger() +logger.setLevel(logging.INFO) logHandler = logging.StreamHandler() formatter = JsonFormatter() @@ -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 diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py new file mode 100644 index 0000000..2b65941 --- /dev/null +++ b/tests/test_quickstart.py @@ -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