diff --git a/litellm/_logging.py b/litellm/_logging.py index 9f301aa6ca8..cf402987442 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -5,10 +5,7 @@ import re import sys from datetime import datetime from logging import Formatter -from collections.abc import Mapping -from typing import Any, Dict, List, Optional, Tuple, Union - -LogRecordArgs = Union[Tuple[Any, ...], Mapping[str, Any]] +from typing import Any, Dict, List, Optional, Union from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_loads import safe_json_loads @@ -20,11 +17,7 @@ if set_verbose is True: "`litellm.set_verbose` is deprecated. Please set `os.environ['LITELLM_LOG'] = 'DEBUG'` for debug logs." ) -_DISABLE_SECRET_REDACTION = os.getenv("LITELLM_DISABLE_REDACTION", "").lower() in ( - "1", - "true", - "yes", -) +_ENABLE_SECRET_REDACTION = bool(os.getenv("LITELLM_REDACT_SECRETS", False)) _REDACTED = "REDACTED" @@ -72,30 +65,18 @@ class SecretRedactionFilter(logging.Filter): """Scrubs known secret/credential patterns from log records.""" def filter(self, record: logging.LogRecord) -> bool: - if _DISABLE_SECRET_REDACTION: + if not _ENABLE_SECRET_REDACTION: return True - if isinstance(record.msg, str): - record.msg = _redact_string(record.msg) - - if record.args: - record.args = self._redact_args(record.args) + try: + record.msg = _redact_string(record.getMessage()) + record.args = None + except Exception: + if isinstance(record.msg, str): + record.msg = _redact_string(record.msg) return True - @staticmethod - def _redact_args(args: LogRecordArgs) -> LogRecordArgs: - if isinstance(args, Mapping): - return { - k: _redact_string(v) if isinstance(v, str) else v - for k, v in args.items() - } - if isinstance(args, tuple): - return tuple( - _redact_string(a) if isinstance(a, str) else a for a in args - ) - return args - _secret_filter = SecretRedactionFilter() @@ -401,7 +382,6 @@ def _turn_on_json(): """ handler = logging.StreamHandler() handler.setFormatter(JsonFormatter()) - handler.addFilter(_secret_filter) _initialize_loggers_with_handler(handler) # Set up exception handlers _setup_json_exception_handlers(JsonFormatter()) diff --git a/tests/test_litellm/test_secret_redaction.py b/tests/test_litellm/test_secret_redaction.py index be848454a0a..c5136874652 100644 --- a/tests/test_litellm/test_secret_redaction.py +++ b/tests/test_litellm/test_secret_redaction.py @@ -1,14 +1,10 @@ import logging -import os -import sys from io import StringIO +from unittest.mock import patch import pytest -sys.path.insert(0, os.path.abspath("../../..")) - from litellm._logging import ( - SecretRedactionFilter, _redact_string, _secret_filter, verbose_logger, @@ -16,32 +12,18 @@ from litellm._logging import ( verbose_router_logger, ) - -def test_redact_string_covers_all_secret_formats(): - secrets = { - "AWS access key": "AKIAIOSFODNN7EXAMPLE", - "AWS temp key": "ASIAISAMPLEKEYID1234", - "AWS secret": "aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", - "AWS session token": "aws_session_token: FwoGZXIvYXdzEBYaDHqa0AP1RIF0re2EXAMPLETOKEN1234567890", - "Bearer token": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig", - "OpenAI key": "sk-proj-abc123def456ghi789jklmnopqrst", - "x-api-key header": "x-api-key: mysecretapikey123", - "api-key header": "api-key: mysecretapikey123", - "Azure key": "api_key=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", - "Google key": "AIzaSyA1234567890abcdefghijklmnopqrstuvwx", - "Anthropic x-ak": "x-ak-abcdefghijklmnopqrstuvwxyz12345", - "Dict repr with secrets": "Request Headers: {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig', 'x-api-key': 'sk-ant-api03-abcdefghijklmnopqrstuvwxyz'}", - } - for label, secret in secrets.items(): - result = _redact_string(f"msg: {secret}") - assert secret not in result, f"{label} was not redacted" - assert "REDACTED" in result, f"{label} missing REDACTED marker" - - normal = "Loaded model gpt-4 with 3 replicas on us-east-1" - assert _redact_string(normal) == normal +SECRET = "sk-proj-abc123def456ghi789jklmnopqrst" -def test_filter_redacts_secrets_in_logger_output(): +@pytest.fixture(autouse=True) +def _enable_redaction(): + """Enable secret redaction for all tests in this module.""" + with patch("litellm._logging._ENABLE_SECRET_REDACTION", True): + yield + + +def _capture_logger_output(fn): + """Run fn with all litellm loggers wired to a StringIO buffer, return output.""" buf = StringIO() h = logging.StreamHandler(buf) h.addFilter(_secret_filter) @@ -51,22 +33,80 @@ def test_filter_redacts_secrets_in_logger_output(): lg.handlers.clear() lg.addHandler(h) lg.setLevel(logging.DEBUG) - try: - verbose_logger.debug(f"Credentials: AKIAIOSFODNN7EXAMPLE") - verbose_proxy_logger.debug(f"Headers: Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig") - verbose_router_logger.debug(f"Key: sk-proj-abc123def456ghi789jklmnopqrst") - verbose_logger.debug("Normal message with no secrets") - - output = buf.getvalue() - assert "AKIAIOSFODNN7EXAMPLE" not in output - assert "eyJhbGciOiJSUzI1NiJ9" not in output - assert "sk-proj-abc123def456ghi789jklmnopqrst" not in output - assert "REDACTED" in output - assert "Normal message with no secrets" in output + fn() + return buf.getvalue() finally: for lg, handlers, level in saved: lg.handlers.clear() for old_h in handlers: lg.addHandler(old_h) lg.setLevel(level) + + +def test_redact_string_catches_secret_patterns(): + """Core regex patterns redact known secret formats.""" + cases = [ + "Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig", + "api_key=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + "password=supersecretpassword123", + "postgresql://admin:s3cretpass@db.example.com:5432/mydb", + SECRET, + ] + for secret in cases: + result = _redact_string("msg: " + secret) + assert secret not in result, f"{secret!r} was not redacted" + assert "REDACTED" in result + + normal = "Loaded model gpt-4 with 3 replicas on us-east-1" + assert _redact_string(normal) == normal + + +def test_filter_redacts_secrets_in_logger_output(): + def log_messages(): + verbose_logger.debug("Key: " + SECRET) + verbose_logger.debug("Normal message with no secrets") + + output = _capture_logger_output(log_messages) + assert SECRET not in output + assert "REDACTED" in output + assert "Normal message with no secrets" in output + + +def test_filter_redacts_percent_style_args(): + """Secrets passed as %-style args should be redacted.""" + + def log_messages(): + verbose_logger.debug("key=%s region=%s", SECRET, "us-east-1") + + output = _capture_logger_output(log_messages) + assert SECRET not in output + assert "us-east-1" in output + + +def test_filter_redacts_non_string_args(): + """Secrets inside dicts/lists passed as %-style args should be redacted.""" + + def log_messages(): + verbose_logger.debug("Config: %s", {"nested": {"key": SECRET}}) + verbose_logger.debug("Keys: %s", [SECRET]) + + output = _capture_logger_output(log_messages) + assert SECRET not in output + assert "REDACTED" in output + + +def test_disable_redaction_passes_secrets_through(_enable_redaction): + """When _ENABLE_SECRET_REDACTION is False, secrets pass through.""" + with patch("litellm._logging._ENABLE_SECRET_REDACTION", False): + record = logging.LogRecord( + name="test", + level=logging.DEBUG, + pathname="", + lineno=0, + msg="key=" + SECRET, + args=(), + exc_info=None, + ) + _secret_filter.filter(record) + assert "sk-proj-" in record.msg