How to redact secrets from log messages in Python

This code defines a logging.Filter subclass that automatically redacts sensitive keys like password, token, and API key from any dict logged.

Medium Python 3.9+ Aug 9, 2026 Auth & security at scale 12 views 0 copies

Python code

44 lines
Python 3.9+
import logging
from dataclasses import dataclass


@dataclass
class ApiResponse:
    status: int
    body: dict


class SecretRedactor(logging.Filter):
    SENSITIVE_KEYS = {"password", "token", "secret", "api_key"}

    def filter(self, record: logging.LogRecord) -> bool:
        if isinstance(record.msg, dict):
            record.msg = self._redact(record.msg)
        elif isinstance(record.args, dict):
            record.args = self._redact(record.args)
        return True

    def _redact(self, data: dict) -> dict:
        return {
            key: ("[REDACTED]" if key.lower() in self.SENSITIVE_KEYS else value)
            for key, value in data.items()
        }


def main() -> None:
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    logger = logging.getLogger("api")
    logger.addFilter(SecretRedactor())

    mock_response = ApiResponse(
        status=200,
        body={"user": "alice", "password": "sup3rs3cret", "session_token": "abc123"}
    )

    logger.info("Mock response: %s", mock_response.body)
    logger.info({"user": "bob", "api_key": "1234-5678", "message": "hello"})
    logger.info("Safe message: no secrets here")


if __name__ == "__main__":
    main()

Output

stdout
Mock response: {'user': 'alice', 'password': '[REDACTED]', 'session_token': '[REDACTED]'}
{'user': 'bob', 'api_key': '[REDACTED]', 'message': 'hello'}
Safe message: no secrets here

How it works

The SecretRedactor filter is attached to a logger and inspects every log record. It checks if the message itself is a dict or if the message arguments are a dict, then redacts any keys that match a set of sensitive names (case-insensitively). This approach works because logging filters run before the record is emitted, so the redaction happens even when the dict is passed as the message directly or as an argument. The filter returns True to allow the record through, but with the sensitive values replaced. This pattern is production-safe because it ensures secrets never appear in logs, even if developers forget to manually redact.

Common mistakes

  • Only redacting the message dict but not the args dict, missing cases where secrets are in formatting arguments.
  • Not making the sensitive keys case-insensitive, so 'Password' or 'TOKEN' slip through.
  • Assuming the filter runs on all loggers; it must be explicitly added to each logger you want protected.
  • Forgetting that logging already serializes args if you use lazy formatting (`%s`), so you must redact before that.

Variations

  1. Use a custom `logging.Formatter` to redact string representations of any dict, not just the message/args.
  2. Wrap the `_redact` method to handle nested dicts recursively for deep redaction.

Real-world use cases

  • Preventing credential leakage when logging API request/response payloads in a proxy or middleware.
  • Sanitizing user data in log lines for compliance with GDPR or PCI-DSS requirements.
  • Shielding database connection strings or OAuth tokens from appearing in server error logs.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Auth & security at scale

Related tutorials and quizzes for this topic.