import logging import sys from io import StringIO from unittest.mock import patch import pytest from litellm._logging import ( JsonFormatter, _redact_string, _secret_filter, _setup_json_exception_handlers, verbose_logger, verbose_proxy_logger, verbose_router_logger, ) SECRET = "sk-proj-abc123def456ghi789jklmnopqrst" @pytest.fixture(autouse=True) def _enable_redaction(): """Ensure secret redaction is on (the default) 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) loggers = [verbose_logger, verbose_proxy_logger, verbose_router_logger] saved = [(lg, lg.handlers[:], lg.level) for lg in loggers] for lg in loggers: lg.handlers.clear() lg.addHandler(h) lg.setLevel(logging.DEBUG) try: 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_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 LITELLM_DISABLE_REDACT_SECRETS=true, 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 def test_x_api_key_regex_does_not_consume_json_delimiters(): """x-api-key pattern must stop before closing quotes/braces so JSON stays valid.""" # Simulates a JSON log line containing an x-api-key header value json_line = '{"headers": {"x-api-key": "secret123"}, "status": 200}' result = _redact_string(json_line) # The secret value should be redacted assert "secret123" not in result assert "REDACTED" in result # Closing delimiter must survive so the line is still valid-ish JSON assert '"status": 200' in result assert "}" in result def test_json_excepthook_redacts_secrets(): """Unhandled exceptions in JSON mode must have secrets redacted.""" buf = StringIO() h = logging.StreamHandler(buf) h.setFormatter(JsonFormatter()) h.addFilter(_secret_filter) # Capture what the excepthook would emit record = logging.LogRecord( name="LiteLLM", level=logging.ERROR, pathname="", lineno=0, msg=f"Connection failed with key {SECRET}", args=(), exc_info=None, ) # Simulate the filter + formatter pipeline _secret_filter.filter(record) output = h.formatter.format(record) assert SECRET not in output assert "REDACTED" in output def test_json_excepthook_redacts_traceback_secrets(): """Unhandled exception tracebacks in JSON mode must have secrets redacted.""" buf = StringIO() h = logging.StreamHandler(buf) h.setFormatter(JsonFormatter()) h.addFilter(_secret_filter) try: raise RuntimeError(f"Failed to auth with {SECRET}") except RuntimeError: exc_info = sys.exc_info() record = logging.LogRecord( name="LiteLLM", level=logging.ERROR, pathname="", lineno=0, msg=str(exc_info[1]), args=(), exc_info=exc_info, ) _secret_filter.filter(record) output = h.formatter.format(record) assert SECRET not in output assert "REDACTED" in output