mrkeyoor.com_
Thu 06 Aug 05:52 UTC
PyPIInfraupdated 06 Aug 2026

python-json-logger

python-json-logger is a set of logging.Formatter classes that make the Python standard library emit one JSON object per log line instead of a formatted string. You keep every logger.info() call you already have; you swap the formatter on your handler and the output becomes machine-readable for CloudWatch, Loki, Datadog, or whatever else parses your container stdout. It picks up any non-standard attribute you attach to a record (through extra=, a logging filter, or by logging a dict instead of a string), and it can rename fields to match your platform (asctime to timestamp, levelname to severity), pin static fields like service and environment onto every line, and swap the encoder for orjson or msgspec when the stdlib json module is too slow. It is a maintained fork: everything from 3.0.0 onward comes from nhairs after the original project stopped releasing.

Verdict

The lowest-friction way to get JSON logs out of an application that already uses the standard logging module, and the reason it has tens of millions of weekly downloads. If you are designing logging from scratch and want context binding and processors, start with structlog instead.

API stability4/5The 3.x fork moved the module path and 4.0 dropped string-valued encoder arguments, but both changes were documented with migration notes and the legacy jsonlogger import still works; the formatter constructor has been stable across 4.x.
Docs4/5The MkDocs site has a quickstart, a cookbook with request-ID, dictConfig and lazy-evaluation recipes, and a generated API reference; what is missing is guidance on picking between this and structlog.
Maintenance3/5Pushed 1 August 2026 with only 10 open issues (13 counting PRs) and releases through 2026, but it is one maintainer running a fork of an abandoned project and PyPI marks it Development Status 6 (Mature), so expect fixes rather than features.
Ecosystem4/5About 28.5 million weekly downloads, almost all of it transitive through frameworks and platform SDKs, and it plugs into any logging config; the direct plugin surface is small because it is just a formatter.

Use it if

  • You already use stdlib logging, directly or through Django, Flask, uvicorn, gunicorn, or Celery, and you only need the output format to change so a log aggregator can index fields
  • Your logging is configured with dictConfig or fileConfig in YAML or JSON, and you want a formatter you can name in that config with no code change
  • Your platform expects specific key names or a fixed set of tags: rename_fields maps levelname to severity, static_fields stamps service, env and version on every record
  • Log volume is high enough that JSON encoding shows up in profiles, and you want the orjson or msgspec formatter with the same configuration surface
Skip it if

Setup reality

pip install python-json-logger, no runtime dependencies, Python 3.10 or newer. Two things trip people up. First, the import path moved: new code uses pythonjsonlogger.json.JsonFormatter, while pythonjsonlogger.jsonlogger.JsonFormatter is the legacy path kept for compatibility, and anything pinned below 3.0 is the older unmaintained codebase with different behaviour. Second, output is opt-in: a bare JsonFormatter() emits message plus whatever extras you attach, and nothing else, because every standard LogRecord attribute sits in reserved_attrs by default. You either list the fields you want in fmt or pass reserved_attrs=[] to get all of them. The orjson and msgspec formatters raise MissingPackageError unless you install those packages yourself, and since 4.0 you can no longer pass json_default or json_encoder as dotted strings, so dictConfig users need the ext:// prefix instead.

Patterns

Emit JSON from the root loggerattach-formatter

import logging
from pythonjsonlogger.json import JsonFormatter

handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())

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

logger.info("service started")
# {"message": "service started"}

Import from pythonjsonlogger.json, not pythonjsonlogger.jsonlogger, which is the legacy path. Notice how little is in the output by default: every standard record attribute is reserved until you ask for it.

Pick which record fields appearchoose-fields

from pythonjsonlogger.json import JsonFormatter

# printf style
JsonFormatter("%(asctime)s %(levelname)s %(name)s %(message)s")

# str.format style
JsonFormatter("{asctime}{levelname}{message}", style="{")

# comma style, specific to this library
JsonFormatter("asctime,levelname,message", style=",")

# plain sequence of field names
JsonFormatter(["asctime", "levelname", "message", "exc_info"])

Any LogRecord attribute name works. The comma style and the list form were added in 4.0 and are specific to this package, so do not reuse that fmt string with another Formatter class.

Match your log platform key namesrename-fields

from pythonjsonlogger.json import JsonFormatter

formatter = JsonFormatter(
    "%(asctime)s %(levelname)s %(message)s",
    rename_fields={"asctime": "timestamp", "levelname": "severity"},
    timestamp=True,
)

rename_fields only renames keys that are actually emitted; set rename_fields_keep_missing=True if you want the renamed key present as null when the source field is absent. timestamp=True adds a separate UTC timestamp key, or pass a string to name it.

Stamp service metadata on every linestatic-and-default-fields

from pythonjsonlogger.json import JsonFormatter

formatter = JsonFormatter(
    static_fields={"service": "checkout-api", "version": "1.4.2"},
    defaults={"environment": "dev"},
)

logger.info("boot")                                  # environment=dev
logger.info("boot", extra={"environment": "prod"})   # environment=prod

static_fields cannot be overridden per call; defaults can. Use static for identity (service name, version) and defaults for values a specific record may want to replace.

Add fields to a single log recordper-call-fields

# via extra=
logger.info("payment captured", extra={"order_id": 991, "amount_cents": 4200})

# or log a dict directly
logger.info({"message": "payment captured", "order_id": 991, "amount_cents": 4200})

Keys in extra= that clash with LogRecord attributes (message, args, name, module, levelname) raise KeyError from the standard library. Logging a dict avoids that, but other formatters in the same process will render it as a Python repr.

Include every standard fieldall-record-attributes

from pythonjsonlogger.json import JsonFormatter

formatter = JsonFormatter(reserved_attrs=[])

# or keep the defaults but let two custom names through
from pythonjsonlogger.core import RESERVED_ATTRS

formatter = JsonFormatter(
    reserved_attrs=[a for a in RESERVED_ATTRS if a not in ("thread", "process")],
)

reserved_attrs is a deny list, so emptying it emits everything including filename, lineno, thread and process. That is verbose and costs bytes per line; usually a short fmt list is the better trade.

Configure through dictConfigdict-config

import logging.config

logging.config.dictConfig({
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "json": {
            "()": "pythonjsonlogger.json.JsonFormatter",
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s",
            "rename_fields": {"asctime": "timestamp"},
            "static_fields": {"service": "checkout-api"},
        }
    },
    "handlers": {
        "stdout": {"class": "logging.StreamHandler", "formatter": "json", "stream": "ext://sys.stdout"}
    },
    "root": {"level": "INFO", "handlers": ["stdout"]},
})

The "()" key is what lets dictConfig pass keyword arguments to the formatter. Since 4.0 callables such as json_default must be referenced with the ext:// prefix rather than a bare dotted string.

Swap in orjson or msgspecfaster-encoder

# pip install orjson
from pythonjsonlogger.orjson import OrjsonFormatter

handler.setFormatter(OrjsonFormatter("%(asctime)s %(levelname)s %(message)s"))

# pip install msgspec
from pythonjsonlogger.msgspec import MsgspecFormatter

Neither encoder is installed for you; importing without the package raises MissingPackageError. They also serialize some types differently from the stdlib formatter (orjson base64-encodes bytes, both render exceptions as "ValueError: message" strings).

Handle objects json cannot encodecustom-serializer

from decimal import Decimal
from pythonjsonlogger.json import JsonFormatter
import pythonjsonlogger.defaults as defaults

def my_default(obj):
    if isinstance(obj, Decimal):
        return str(obj)
    return defaults.json_default(obj)

formatter = JsonFormatter(json_default=my_default)

Fall back to the package default rather than raising: it already handles dataclasses, enums, exceptions, tracebacks and types, and a formatter that throws will lose the log line entirely.

Log tracebacks as a list of linesexceptions-as-array

from pythonjsonlogger.json import JsonFormatter

formatter = JsonFormatter(
    "%(asctime)s %(levelname)s %(message)s %(exc_info)s",
    exc_info_as_array=True,
    stack_info_as_array=True,
)

try:
    1 / 0
except ZeroDivisionError:
    logger.exception("division failed")

Without these flags a traceback is one long string full of newlines, which most log viewers render badly. As an array each frame stays a separate element.

Attach a request ID to every recordrequest-id-filter

import logging
from contextvars import ContextVar

request_id: ContextVar[str | None] = ContextVar("request_id", default=None)

class RequestIdFilter(logging.Filter):
    def filter(self, record):
        record.request_id = request_id.get()
        return True

handler.addFilter(RequestIdFilter())
logger.info("handling request")  # {"message": ..., "request_id": "..."}

A filter is better than repeating extra= at every call site, and it works for logs emitted by libraries you do not control. Any attribute set on the record shows up in the JSON automatically.

Rewrite the whole record before it is encodedreshape-output

from pythonjsonlogger.json import JsonFormatter

class NestedFormatter(JsonFormatter):
    def process_log_record(self, log_data):
        return {
            "ts": log_data.pop("asctime", None),
            "level": log_data.pop("levelname", None),
            "msg": log_data.pop("message", None),
            "attrs": log_data,
        }

handler.setFormatter(NestedFormatter("%(asctime)s %(levelname)s %(message)s"))

process_log_record is the supported hook for reshaping, including nesting fields under a parent key. Note that 4.0 renamed the log_record arguments on the base class hooks, so subclasses written against 3.x may need signature updates.

Alternatives

PackageRegistryPick it when
structlogPyPIYou want context binding, event dicts and processor pipelines rather than a formatter bolted onto stdlib logging.
loguruPyPIYou want one opinionated logger with pretty console output, rotation and JSON serialization built in, and you control the whole application.
ecs-loggingPyPIYou ship to Elasticsearch and want output that already matches the Elastic Common Schema without hand-mapping field names.