Redact denylisted fields before a log line is rendered

A processor in the shared chain masks sensitive field names (credential
fragments like password/token/authorization, plus exact names such as
card_number and holder_name) at any depth, for structlog and stdlib
records alike. It sees field names only: values interpolated into the
event string still leak until call sites pass them as fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Movses Aslanyan
2026-10-13 20:45:01 +03:30
co-authored by Claude Opus 5.5
parent 455b0970b1
commit 52a6d711a0
+36
View File
@@ -36,6 +36,41 @@ def _add_trace_context(logger, method_name, event_dict):
return event_dict return event_dict
# Field names whose values never reach a log line. Keys are compared
# lowercased with "-" folded to "_", so header names (X-Api-Key) match too.
# Fragments match anywhere in the key (db_password, refresh_token); the exact
# names are too short or too specific to match as fragments ("pan" would
# catch "company").
_REDACT_FRAGMENTS = ("password", "passwd", "secret", "token", "authorization", "cookie", "api_key")
_REDACT_EXACT = {"pan", "card_number", "cvv", "cvc", "holder_name"}
_REDACTED = "[REDACTED]"
def _is_sensitive(key) -> bool:
k = str(key).lower().replace("-", "_")
return k in _REDACT_EXACT or any(f in k for f in _REDACT_FRAGMENTS)
def _scrub(value):
if isinstance(value, dict):
return {k: _REDACTED if _is_sensitive(k) else _scrub(v) for k, v in value.items()}
if isinstance(value, list):
return [_scrub(v) for v in value]
if isinstance(value, tuple):
# Stays a tuple: format_exc_info only trusts an exc_info that is one.
return tuple(_scrub(v) for v in value)
return value
def _redact(logger, method_name, event_dict):
"""Mask denylisted fields, at any depth (a logged headers dict included).
This works on field names only, so it is only as good as the call sites:
a value interpolated into the `event` string is invisible to it.
"""
return _scrub(event_dict)
def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger: def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger:
"""One JSON object per line on stdout, for structlog and stdlib loggers alike. """One JSON object per line on stdout, for structlog and stdlib loggers alike.
@@ -50,6 +85,7 @@ def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger:
structlog.processors.TimeStamper(fmt="iso", utc=True), structlog.processors.TimeStamper(fmt="iso", utc=True),
_add_service(service_name), _add_service(service_name),
_add_trace_context, _add_trace_context,
_redact,
] ]
structlog.configure( structlog.configure(