from __future__ import annotations import logging import logging.config import re from collections.abc import Mapping, Sequence from typing import Any SENSITIVE_KEYS = { "password", "password_hash", "token", "access_token", "refresh_token", "authorization", "email", } TOKEN_PATTERN = re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*", re.IGNORECASE) def _sanitize_value(value: Any) -> Any: if isinstance(value, str): redacted = TOKEN_PATTERN.sub("Bearer [REDACTED]", value) for key in SENSITIVE_KEYS: redacted = re.sub( rf"({key}\s*=\s*)([^,\s]+)", r"\1[REDACTED]", redacted, flags=re.IGNORECASE, ) return redacted if isinstance(value, Mapping): return { key: "[REDACTED]" if str(key).lower() in SENSITIVE_KEYS else _sanitize_value(item) for key, item in value.items() } if isinstance(value, tuple): return tuple(_sanitize_value(item) for item in value) if isinstance(value, list): return [_sanitize_value(item) for item in value] if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): return [_sanitize_value(item) for item in value] return value class SensitiveDataFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: record.msg = _sanitize_value(record.msg) if record.args: record.args = _sanitize_value(record.args) return True def setup_logging(log_level: str) -> None: logging.config.dictConfig( { "version": 1, "disable_existing_loggers": False, "filters": { "sensitive_data_filter": { "()": "app.core.logging.SensitiveDataFilter", } }, "formatters": { "standard": { "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s", } }, "handlers": { "console": { "class": "logging.StreamHandler", "filters": ["sensitive_data_filter"], "formatter": "standard", } }, "root": { "level": log_level.upper(), "handlers": ["console"], }, } )