ProvenPin Install

Capture loguru logs in pytest caplog via a propagate sink and structured extra: working versions (tested 2026-10-15)

Forward loguru records into stdlib logging so pytest caplog sees them, keep bound context as structured extra, and test it offline without a real log backend.

passed5 passed · 0 failed2026-10-15 13:23 UTC
Status
passed
Last passed
2026-10-15 13:23 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions2
loguru@0.7.3pytest@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 38741369695

The code

Exactly the files that passed. Your agent can pull them with get_recipe("loguru-pytest-caplog") (install), or see them on GitHub.

requirements.txt
loguru==0.7.3
pytest==9.1.1
logsetup.py
import logging
from loguru import logger

logging.addLevelName(25, "SUCCESS")  # loguru's SUCCESS level has no stdlib name

def _to_stdlib(message):
    r = message.record
    logging.getLogger(r["name"]).log(
        r["level"].no,
        r["message"],  # already formatted; no %-formatting is applied without args
        # nest extras so bound keys like "message" or "name" cannot clash with LogRecord attributes
        extra={"loguru_extra": dict(r["extra"])},
    )

def setup_propagation(level="DEBUG") -> int:
    """Forward loguru records to stdlib logging. Returns the sink id for logger.remove()."""
    return logger.add(_to_stdlib, level=level)
orders.py
from loguru import logger

def charge(order_id: str, amount_cents: int) -> bool:
    log = logger.bind(order_id=order_id, amount_cents=amount_cents)
    log.info("charging order")
    if amount_cents <= 0:
        log.warning("non-positive amount, skipped")
        return False
    log.success("charged")
    return True
tests/test_logging.py
import logging

import pytest
from loguru import logger

from logsetup import setup_propagation
from orders import charge

@pytest.fixture
def propagate(caplog):
    caplog.set_level(logging.DEBUG)
    sink_id = setup_propagation()
    yield
    logger.remove(sink_id)  # without this, sinks leak and every later test sees duplicates

def test_records_reach_caplog(propagate, caplog):
    charge("o1", 500)
    assert "charging order" in caplog.messages
    assert "charged" in caplog.messages

def test_bound_context_travels_as_extra(propagate, caplog):
    charge("o1", 500)
    rec = next(r for r in caplog.records if r.getMessage() == "charging order")
    assert rec.loguru_extra == {"order_id": "o1", "amount_cents": 500}

def test_success_level_has_a_stdlib_name(propagate, caplog):
    charge("o1", 500)
    rec = next(r for r in caplog.records if r.getMessage() == "charged")
    assert rec.levelname == "SUCCESS" and rec.levelno == 25

def test_braces_and_percent_are_not_reformatted(propagate, caplog):
    logger.info("100% done, {not_a_field}")
    assert caplog.messages[-1] == "100% done, {not_a_field}"

def test_bind_keys_named_like_logrecord_attrs(propagate, caplog):
    logger.bind(message="hi", name="x").info("ok")
    rec = caplog.records[-1]
    assert rec.loguru_extra["message"] == "hi" and rec.loguru_extra["name"] == "x"
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.