diff --git a/litellm/_logging.py b/litellm/_logging.py index d5865777661..15c95996335 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -5,7 +5,7 @@ import re import sys from datetime import datetime from logging import Formatter -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_loads import safe_json_loads @@ -17,7 +17,7 @@ if set_verbose is True: "`litellm.set_verbose` is deprecated. Please set `os.environ['LITELLM_LOG'] = 'DEBUG'` for debug logs." ) -_ENABLE_SECRET_REDACTION = os.getenv("LITELLM_REDACT_SECRETS") == "TRUE" +_ENABLE_SECRET_REDACTION = os.getenv("LITELLM_REDACT_SECRETS", "").lower() == "true" _REDACTED = "REDACTED" @@ -64,6 +64,8 @@ def _redact_string(value: str) -> str: class SecretRedactionFilter(logging.Filter): """Scrubs known secret/credential patterns from log records.""" + _formatter = logging.Formatter() + def filter(self, record: logging.LogRecord) -> bool: if not _ENABLE_SECRET_REDACTION: return True @@ -75,6 +77,20 @@ class SecretRedactionFilter(logging.Filter): if isinstance(record.msg, str): record.msg = _redact_string(record.msg) + # Redact exception tracebacks + if record.exc_info and record.exc_info[1] is not None: + try: + record.exc_text = _redact_string( + self._formatter.formatException(record.exc_info) + ) + except Exception: + pass + + # Redact extra fields passed via logger.debug("msg", extra={...}) + for key, value in record.__dict__.items(): + if key not in _STANDARD_RECORD_ATTRS and isinstance(value, str): + setattr(record, key, _redact_string(value)) + return True diff --git a/tests/test_litellm/test_secret_redaction.py b/tests/test_litellm/test_secret_redaction.py index 6e634248651..6321f452f66 100644 --- a/tests/test_litellm/test_secret_redaction.py +++ b/tests/test_litellm/test_secret_redaction.py @@ -96,6 +96,42 @@ def test_filter_redacts_non_string_args(): assert "REDACTED" in output +def test_filter_redacts_exception_tracebacks(): + """Secrets embedded in exception messages must be redacted in tracebacks.""" + + def log_messages(): + try: + raise ValueError(f"Auth failed with key {SECRET}") + except ValueError: + verbose_logger.exception("Something went wrong") + + output = _capture_logger_output(log_messages) + assert SECRET not in output + assert "REDACTED" in output + assert "Something went wrong" in output + + +def test_filter_redacts_extra_fields(): + """Secrets passed via extra={...} must be redacted on the record.""" + record = logging.LogRecord( + name="test", + level=logging.DEBUG, + pathname="", + lineno=0, + msg="request completed", + args=(), + exc_info=None, + ) + record.api_key = SECRET + record.region = "us-east-1" + + _secret_filter.filter(record) + + assert SECRET not in record.api_key + assert "REDACTED" in record.api_key + assert record.region == "us-east-1" + + def test_disable_redaction_passes_secrets_through(): """When _ENABLE_SECRET_REDACTION is False, secrets pass through.""" with patch("litellm._logging._ENABLE_SECRET_REDACTION", False):