python-json-logger review
python-json-logger 4.2.0 supplies JSON formatters for Python's built-in `logging` package. Handlers, logger hierarchy, filters, and propagation stay standard; the formatter turns `LogRecord` fields and `extra` data into one JSON object per event. It can rename fields, add static values, render tracebacks as arrays, and use optional orjson or msgspec serializers. Version 4.2.0 stops mutating a dictionary passed as the log message when exception or stack fields are added, and it recognizes unbraced `$name` placeholders in dollar-style formats.
python-json-logger 4.2.0 installed in 0.3 seconds as 1 dependency-free package, occupied 1 MB, imported in 0.17 seconds, and had 0 audit findings in our sandbox. Install it to convert an existing Python logging graph to collector-friendly JSON; skip it when you need a full event-processing model or a ready-made vendor schema.
We installed it
| Install | ✓ · 0.3s | 1 package on disk · 1 MB |
| Import | ✓ | import pythonjsonlogger in 0.17s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does python-json-logger install cleanly?
Yes. In a fresh container with an empty cache, pip install python-json-logger finished in 0.3s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does python-json-logger need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import pythonjsonlogger succeeded in 0.17s, and the package ships py.typed for type checkers.
python-json-logger or structlog: which should you use?
structlog: Use it when bound context, processor pipelines, and multiple renderers should define the logging model. python-json-logger 4.2.0 installed in 0.3 seconds as 1 dependency-free package, occupied 1 MB, imported in 0.17 seconds, and had 0 audit findings in our sandbox.
When should you not use python-json-logger?
You need context binding and processor chains before an event becomes a LogRecord. structlog owns that workflow more directly.
Use it if
- An existing standard-logging setup must emit one JSON object per line for a collector or container runtime.
- Third-party libraries already log through `logging`, and their records should enter the same structured output without API rewrites.
- Field renaming, static service metadata, or array-form tracebacks can bridge records to an ingestion schema.
- The team wants a formatter layer while retaining standard handlers, filters, levels, and `dictConfig`.
- You need context binding and processor chains before an event becomes a `LogRecord`. structlog owns that workflow more directly.
- The collector expects Elastic Common Schema field names and nesting out of the box. `ecs-logging` targets that contract without a custom formatter subclass.
- Python 3.9 is still in production. Version 4.2.0 requires Python 3.10 or newer after the 4.1 support cutoff.
- Secrets may enter arbitrary `extra` dictionaries and no redaction policy exists. The formatter serializes fields; it does not decide which application values are safe to export.
- A single formatter must produce pleasant terminal text as well as machine JSON. Use separate console and collector handlers instead of making humans read escaped JSON.
Setup reality
We installed python-json-logger 4.2.0 in a fresh Python 3.12 Bookworm sandbox in 0.3 seconds. It left 1 package using 1 MB, and import pythonjsonlogger completed in 0.17 seconds. pip-audit found 0 known vulnerabilities. The distribution is pure Python, has 0 direct dependencies, includes py.typed, and requires Python 3.10 or newer. PyPI does not supply a license value, while the repository identifies BSD-2-Clause.
No account or network setup is required. Attach pythonjsonlogger.json.JsonFormatter to the handler that writes toward the collector, preferably stdout in a container. The format selects and orders standard fields; values passed through extra are included unless reserved. An extra key that collides with a LogRecord attribute raises KeyError before the formatter runs, so settle names such as request_id, tenant_id, and trace_id centrally.
JSON output does not solve schema design or redaction. rename_fields can map levelname to severity, and static_fields can stamp service metadata, but request values should come from extra or a filter. A value unknown to the JSON encoder needs json_default. Optional OrjsonFormatter and MsgspecFormatter require their respective packages, which were not present in our 1-package install. Importing those modules without the extra serializer raises a package error.
Tracebacks are strings by default and contain embedded newlines. exc_info_as_array and stack_info_as_array make them easier for line-oriented collectors. Version 4.2.0 matters when callers log a dictionary: adding exception data no longer inserts fields into that same object. Formatter hooks changed argument names in version 4, and string paths for serializer callables were removed; dictConfig callers should use ext:// resolution or pass callable objects directly.
Patterns
Write JSON records to stdout attach-json-handler
import logging
import sys
from pythonjsonlogger.json import JsonFormatter
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter(
'%(asctime)s %(levelname)s %(name)s %(message)s'
))
log = logging.getLogger('billing')
log.setLevel(logging.INFO)
log.addHandler(handler)
log.info('worker ready')Use `pythonjsonlogger.json` for version 4 code; the older `jsonlogger` import path remains mainly for compatibility.
Attach fields to one event add-event-fields
log.info(
'payment captured',
extra={'order_id': 991, 'amount_cents': 4200},
)An `extra` key matching 1 built-in `LogRecord` attribute raises `KeyError` before JSON formatting.
Send a dictionary as the message log-dict-event
event = {
'message': 'payment captured',
'order_id': 991,
'amount_cents': 4200,
}
log.info(event)Version 4.2.0 no longer adds `exc_info` or `stack_info` to the caller's dictionary while formatting it.
Match collector field names rename-output-fields
formatter = JsonFormatter(
'%(asctime)s %(levelname)s %(message)s',
rename_fields={
'asctime': 'timestamp',
'levelname': 'severity',
},
)Only selected source fields can be renamed; include both original names in the format before mapping them.
Add fixed service metadata stamp-service-fields
formatter = JsonFormatter(
static_fields={
'service': 'checkout-api',
'environment': 'production',
},
)Static fields repeat on every record; request-specific values belong in `extra` or a context-aware filter.
Create the formatter with dictConfig configure-dictconfig
import logging.config
logging.config.dictConfig({
'version': 1,
'formatters': {
'json': {
'()': 'pythonjsonlogger.json.JsonFormatter',
'format': '%(asctime)s %(levelname)s %(name)s %(message)s',
},
},
'handlers': {
'stdout': {
'class': 'logging.StreamHandler',
'formatter': 'json',
'stream': 'ext://sys.stdout',
},
},
'root': {'level': 'INFO', 'handlers': ['stdout']},
})The `()` entry tells `dictConfig` to construct the version 4 JSON formatter with the remaining values.
Encode traceback lines as arrays array-traceback-lines
formatter = JsonFormatter(
'%(levelname)s %(message)s %(exc_info)s %(stack_info)s',
exc_info_as_array=True,
stack_info_as_array=True,
)Array output keeps each traceback line separate instead of embedding multiple newline characters in 1 JSON string.
Encode Decimal values serialize-custom-type
from decimal import Decimal
from pythonjsonlogger import defaults
def encode(value):
if isinstance(value, Decimal):
return str(value)
return defaults.json_default(value)
formatter = JsonFormatter(json_default=encode)Delegate unknown objects to the package default so 1 new value type does not break the whole log record.
Select the optional orjson backend use-orjson-formatter
from pythonjsonlogger.orjson import OrjsonFormatter
handler.setFormatter(OrjsonFormatter(
'%(levelname)s %(message)s'
))Install `orjson` separately; it was absent from our 1-package base install and the optional module fails without it.
Read a request id from ContextVar inject-request-context
import logging
from contextvars import ContextVar
request_id = ContextVar('request_id', default=None)
class RequestFields(logging.Filter):
def filter(self, record):
record.request_id = request_id.get()
return True
handler.addFilter(RequestFields())A handler filter enriches records from third-party modules too, as long as they reach the same handler.
Remove known secrets before serialization redact-secret-fields
class RedactingFormatter(JsonFormatter):
def process_log_record(self, data):
for field in ('password', 'access_token', 'authorization'):
if field in data:
data[field] = '[redacted]'
return dataThe base formatter applies no application-specific redaction; keep the sensitive-field list tied to your actual event schema.
Nest remaining attributes reshape-log-document
class EventFormatter(JsonFormatter):
def process_log_record(self, data):
return {
'timestamp': data.pop('asctime', None),
'severity': data.pop('levelname', None),
'message': data.pop('message', None),
'attributes': data,
}`process_log_record` is the final version 4 transformation hook; custom subclasses from 3.x need their argument names reviewed.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| structlog | PyPI | Use it when bound context, processor pipelines, and multiple renderers should define the logging model. |
| ecs-logging | PyPI | Use it when logs must follow Elastic Common Schema without hand-written field mapping. |
| loguru | PyPI | Use it when replacing standard logging with managed sinks, context, rotation, and formatting is acceptable. |
| orjson | PyPI | Use it directly when the task is fast JSON serialization rather than integration with Python logging records. |
More infra guides
boto3 · opentelemetry-api · psutil · distro · @opentelemetry/api · google-cloud-storage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

