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.
Python code
44 linesimport 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
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
- Use a custom `logging.Formatter` to redact string representations of any dict, not just the message/args.
- 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
More from Auth & security at scale
- ACME LetsEncrypt Mock Challenge Server in Python medium
- AES GCM encryption and decryption in Python medium
- Build a Mock OIDC Userinfo Endpoint in Python with Flask easy
- ChaCha20-Poly1305 mock in Python medium
- ECDH key agreement in Python with cryptography medium
- Enforce TLS 1.2 Minimum in Python easy
Keep learning
Related tutorials and quizzes for this topic.