A Tracer Decorator With Arg Redaction (Python)

A `@trace()` decorator I bolt onto Python services when the production logs go quiet at the wrong layer. Logs entry, exit, duration, and exceptions, with secret-arg redaction baked in.

Python
Compiler
3 snippets
py-decorators
debugging
logging
tracing
elisehuang

By @elisehuang

March 17, 2026

·

Updated May 20, 2026

862 views

6

4.4 (12)

from __future__ import annotations
import functools
import inspect
import time
from typing import Any, Callable

REDACT_PARAM_NAMES = {'password', 'token', 'api_key', 'secret', 'card_number'}

def trace(redact: set[str] | None = None) -> Callable[..., Any]:
    redact_set = (redact or set()) | REDACT_PARAM_NAMES

    def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
        sig = inspect.signature(fn)

        @functools.wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            bound = sig.bind_partial(*args, **kwargs)
            bound.apply_defaults()
            scrubbed = {
                k: ('[REDACTED]' if k in redact_set else _safe_repr(v))
                for k, v in bound.arguments.items()
            }
            t0 = time.perf_counter()
            print(f'[trace] enter {fn.__qualname__}({scrubbed})')
            try:
                result = fn(*args, **kwargs)
                dt = (time.perf_counter() - t0) * 1000
                print(f'[trace] exit  {fn.__qualname__} ok dt={dt:.1f}ms')
                return result
            except Exception as exc:
                dt = (time.perf_counter() - t0) * 1000
                print(f'[trace] exit  {fn.__qualname__} raised {type(exc).__name__} dt={dt:.1f}ms')
                raise
        return wrapper
    return decorator

def _safe_repr(v: Any) -> str:
    s = repr(v)
    return s if len(s) <= 80 else s[:80] + f'...({len(s)})'

@trace()
def login(user: str, password: str) -> dict:
    return {'user': user, 'session': 'abc123'}

@trace(redact={'card_number'})
def charge(user: str, amount: int, card_number: str) -> str:
    return f'charged {amount} to ****{card_number[-4:]}'

login('[email protected]', password='hunter2')
charge('[email protected]', 4200, card_number='4242424242424242')

I keep this in a tracer.py next to my dotfile pretty-printer. The trick is inspect.signature(fn).bind_partial(*args, **kwargs), which gives me a name-keyed view of arguments regardless of how the caller passed them. That lets the redact set work on password whether the caller did login('me', 'hunter2') or login('me', password='hunter2'). The _safe_repr helper truncates long blobs so a single 50KB request body does not bury the rest of the log. Drop the decorator on a suspect function during an incident and remove it after the postmortem.