"""Safe logging utilities to prevent sensitive data leakage. This module provides utilities for safely logging exceptions without exposing sensitive information like passwords, tokens, or PII in application logs. """ import logging from typing import Optional def log_exception_safe( logger: logging.Logger, message: str, exception: Exception, level: str = "error", include_type: bool = True ) -> None: """ Safely log an exception without exposing sensitive data. Instead of logging the full exception message (which may contain sensitive data), this function logs only the exception type or a generic message. Args: logger: Logger instance to use message: Context message describing what failed exception: Exception object to log safely level: Log level (error, warning, info, debug) include_type: Whether to include exception type name in the log Example: >>> try: >>> create_user("admin", "SecretPassword123!") >>> except Exception as e: >>> log_exception_safe(logger, "User creation failed", e) >>> # Logs: "User creation failed: IntegrityError" >>> # Does NOT log: "User creation failed: UNIQUE constraint failed: password=SecretPassword123!" """ if include_type: safe_message = f"{message}: {type(exception).__name__}" else: safe_message = message log_func = getattr(logger, level.lower()) log_func(safe_message) def get_safe_error_message(exception: Exception) -> str: """ Get a safe error message from an exception. Returns only the exception type name, not the potentially sensitive message. Args: exception: Exception object Returns: String containing only the exception type name Example: >>> try: >>> raise ValueError("password=secret123") >>> except Exception as e: >>> safe_msg = get_safe_error_message(e) >>> # safe_msg = "ValueError" """ return type(exception).__name__ def mask_identifier(identifier: str, prefix: str = "id") -> str: """ Mask an identifier for safe logging. Completely masks the identifier to prevent any sensitive data leakage. This security measure ensures no part of the identifier (which may contain or be derived from sensitive information) is exposed in logs. The prefix parameter is used to identify the type of identifier in logs. Completely masks the identifier value to prevent any sensitive data exposure. Returns only the prefix with masked content, without revealing any characters from the actual identifier. Args: identifier: The identifier to mask (patient_id, username, etc.) prefix: Prefix to use for the masked value (e.g., 'pat', 'user', 'id') Returns: Masked identifier string in the format: prefix_**** Masked identifier string in format: "{prefix}_****" Example: >>> mask_identifier("patient12345", "pat") 'pat_****' >>> mask_identifier("admin", "user") 'user_****' >>> mask_identifier("", "id") # Even empty strings are masked consistently 'id_****' """ return f"{prefix}_****" def safe_log_info( logger: logging.Logger, message: str, **identifiers ) -> None: """ Safely log an info message with masked identifiers. Args: logger: Logger instance to use message: Message template with {placeholders} **identifiers: Key-value pairs of identifiers to mask Example: >>> safe_log_info(logger, "Processing patient {patient_id}", ... patient_id=("patient12345", "pat")) # Logs: "Processing patient pat_****" """ masked_values = {} for key, value in identifiers.items(): if isinstance(value, tuple): identifier, prefix = value masked_values[key] = mask_identifier(identifier, prefix) else: masked_values[key] = mask_identifier(value, key) logger.info(message.format(**masked_values)) def safe_log_warning( logger: logging.Logger, message: str, **identifiers ) -> None: """ Safely log a warning message with masked identifiers. Args: logger: Logger instance to use message: Message template with {placeholders} **identifiers: Key-value pairs of identifiers to mask Example: >>> safe_log_warning(logger, "No data for patient {patient_id}", ... patient_id=("67890", "pat")) # Logs: "No data for patient pat_****" """ masked_values = {} for key, value in identifiers.items(): if isinstance(value, tuple): identifier, prefix = value masked_values[key] = mask_identifier(identifier, prefix) else: masked_values[key] = mask_identifier(value, key) logger.warning(message.format(**masked_values))