Yeda AI Tips · #160

Español

Redact Secrets at the Logger Config, Not at Every Call Site

One line — log.info(request) — and every API key, session token, and password on that request object just landed in plaintext in your logs, your log aggregator, and everyone with read access to it. The fix isn't more discipline. It's moving redaction to the one place every log line has to pass through: the logger config.

Why call-site redaction fails

The common pattern is to scrub secrets where you log them:

log.info("auth attempt", user=user.id, token="***REDACTED***")

This works until the day someone logs the whole object instead of hand-picked fields, adds a new sensitive field to the payload, or copies a debug log.info(request) from a branch and forgets to delete it. Call-site redaction is a rule every engineer must remember on every line forever. The failure mode is one forgotten field, and someone always forgets.

Redact once, at the logger

A processor (structlog) or filter (stdlib logging) sits in the pipeline that every event passes through. It inspects the event, replaces sensitive keys with a placeholder, and only then hands the record to the formatter and handlers. Nothing reaches disk or your aggregator unscrubbed.

import structlog

SENSITIVE = {"password", "token", "authorization", "api_key", "secret", "cookie"}

def redact(logger, method_name, event_dict):
    for key in list(event_dict):
        if key.lower() in SENSITIVE:
            event_dict[key] = "***REDACTED***"
    return event_dict

structlog.configure(processors=[redact, structlog.processors.JSONRenderer()])

A structlog processor is just a callable that takes (logger, method_name, event_dict) and returns the dict for the next processor in the chain. Put redact before the renderer and every event is scrubbed before it's serialized.

The standard library has the same seam. A logging.Filter subclass mutates the LogRecord before any handler emits it:

import logging

class RedactFilter(logging.Filter):
    def filter(self, record):
        for key in SENSITIVE:
            if hasattr(record, key):
                setattr(record, key, "***REDACTED***")
        record.msg = scrub(record.msg)  # regex over the message string
        return True  # keep the record; we only mutate it

Attach the filter to your handlers and it runs on every record.

Rules of thumb

DecisionDo this
Where to redactAt the logger config (processor/filter), not the call site
What to matchA denylist of key names plus regex for values (bearer tokens, keys)
Key matchingCase-insensitive; normalize Authorization and authorization
On no matchReturn/keep the record — a filter that returns True mutates, doesn't drop
Nested payloadsWalk dicts recursively; secrets hide inside headers, body, meta

Power moves

Resources

Building AI features that log a lot? Yeda AI designs, audits, and ships production LLM systems — including the logging and secrets hygiene around them. Talk to us · Read the blog