ProvenPin Install

Structured JSON logs with python-json-logger, captured in tests: working versions (tested 2026-10-16)

Emit one JSON object per log record with python-json-logger 4.x, using extra fields, renamed keys, static fields, timestamp and exception info, and assert the parsed output offline.

passed7 passed · 0 failed2026-10-16 21:57 UTC
Status
passed
Last passed
2026-10-16 21:57 UTC
Tests
7 / 7
Language
Python
Runtime
Python 3.13.16
pinned versions2
python-json-logger@4.2.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38972656245

The code

Exactly the files that passed. Your agent can pull them with get_recipe("python-json-logger-structured") (install), or see them on GitHub.

requirements.txt
python-json-logger==4.2.0
pytest==9.1.1
logging_setup.py
import io
import json
import logging

from pythonjsonlogger.json import JsonFormatter

SERVICE = "checkout"


def build_formatter(**overrides):
    """Build the JSON formatter shared by the app's loggers.

    Extras let a caller tweak one option without rebuilding the whole config.
    """
    options = {
        "fmt": "%(message)s %(levelname)s %(name)s",
        "rename_fields": {"message": "msg", "levelname": "level"},
        "static_fields": {"service": SERVICE},
        "timestamp": "ts",
    }
    options.update(overrides)
    return JsonFormatter(**options)


def make_logger(name, **formatter_overrides):
    """Return an isolated logger plus the buffer it writes JSON lines to.

    logging.getLogger is global, so we replace handlers per test logger and
    disable propagation to keep other handlers out of the capture.
    """
    buf = io.StringIO()
    handler = logging.StreamHandler(buf)
    handler.setFormatter(build_formatter(**formatter_overrides))
    logger = logging.getLogger(name)
    logger.handlers[:] = [handler]
    logger.setLevel(logging.INFO)
    logger.propagate = False
    return logger, buf


def last_event(buf):
    """Parse the most recent JSON line emitted into buf."""
    line = buf.getvalue().strip().splitlines()[-1]
    return json.loads(line)
tests/test_logging.py
from datetime import datetime, timezone

import pytest

from logging_setup import last_event, make_logger


def test_extra_fields_merge_with_static_fields():
    logger, buf = make_logger("t.extra")
    logger.info("order placed", extra={"order_id": "o-1", "amount": 42})
    event = last_event(buf)
    assert event["msg"] == "order placed"
    assert event["level"] == "INFO"
    assert event["name"] == "t.extra"
    assert event["service"] == "checkout"
    assert event["order_id"] == "o-1"
    assert event["amount"] == 42
    assert "ts" in event


def test_extra_cannot_overwrite_reserved_field():
    logger, _ = make_logger("t.collide")
    with pytest.raises(KeyError, match="in LogRecord"):
        logger.info("hi", extra={"message": "override"})


def test_exception_renders_as_string_by_default():
    logger, buf = make_logger("t.exc")
    try:
        1 / 0
    except ZeroDivisionError:
        logger.exception("boom")
    event = last_event(buf)
    assert event["msg"] == "boom"
    assert event["level"] == "ERROR"
    assert isinstance(event["exc_info"], str)
    assert "ZeroDivisionError" in event["exc_info"]


def test_exc_info_as_array_splits_lines():
    logger, buf = make_logger("t.excarr", exc_info_as_array=True)
    try:
        1 / 0
    except ZeroDivisionError:
        logger.exception("boom")
    event = last_event(buf)
    assert isinstance(event["exc_info"], list)
    assert event["exc_info"][0] == "Traceback (most recent call last):"
    assert any("ZeroDivisionError" in line for line in event["exc_info"])


def test_json_encoder_handles_datetime_and_unknown_objects():
    logger, buf = make_logger("t.enc")
    when = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
    logger.info("saved", extra={"when": when, "blob": object()})
    event = last_event(buf)
    assert event["when"] == "2026-01-02T03:04:05+00:00"
    assert isinstance(event["blob"], str)


def test_renaming_missing_fields_is_opt_in():
    logger, buf = make_logger("t.rename", rename_fields={"message": "msg", "custom": "renamed"})
    logger.info("plain")
    event = last_event(buf)
    assert event["msg"] == "plain"
    assert "renamed" not in event

    keep, keep_buf = make_logger(
        "t.rename.keep",
        rename_fields={"message": "msg", "custom": "renamed"},
        rename_fields_keep_missing=True,
    )
    keep.info("plain")
    assert last_event(keep_buf)["renamed"] is None


def test_defaults_are_renamed_and_overridable():
    logger, buf = make_logger(
        "t.defaults",
        defaults={"env": "prod", "message": "unused"},
        rename_fields={"message": "msg", "env": "environment"},
    )
    logger.info("live")
    event = last_event(buf)
    assert event["environment"] == "prod"
    assert event["msg"] == "live"
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.