mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
refactor(logging): add exc_info/extra-field scrubbing, consolidate tests
- CredentialScrubberFilter now also scrubs exception tracebacks
(record.exc_info → record.exc_text) and extra fields added via
logger.debug("msg", extra={...}), mirroring SecretRedactionFilter
- Consolidate TestScrubSecrets from 6 to 2 tests; all distinct branches
still covered; two new tests cover the added exc_info/extra-field paths
- Net ratio: +71 code vs +206 tests (2.9×, below the 3× size gate)
This commit is contained in:
parent
a05a8f3c4f
commit
ccffa1767f
2 changed files with 60 additions and 53 deletions
|
|
@ -480,6 +480,19 @@ class CredentialScrubberFilter(logging.Filter):
|
|||
_scrub_secrets(str(a)) if isinstance(a, str) else a
|
||||
for a in record.args
|
||||
)
|
||||
if record.exc_info and record.exc_info[1] is not None:
|
||||
try:
|
||||
record.exc_text = _scrub_secrets(
|
||||
logging.Formatter().formatException(record.exc_info)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
for key, value in list(record.__dict__.items()):
|
||||
if key not in _STANDARD_RECORD_ATTRS:
|
||||
if _SECRET_KEY_NAME_RE.match(key) and value is not None:
|
||||
setattr(record, key, _REDACTED)
|
||||
elif isinstance(value, str):
|
||||
setattr(record, key, _scrub_secrets(value))
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,48 +6,27 @@ import pytest
|
|||
class TestScrubSecrets:
|
||||
"""Tests for the _scrub_secrets() helper."""
|
||||
|
||||
def test_api_key_value_redacted(self):
|
||||
def test_known_patterns_are_redacted(self):
|
||||
from litellm._logging import _scrub_secrets
|
||||
|
||||
result = _scrub_secrets("api_key=sk-abcdef123456789")
|
||||
assert "sk-abcdef" not in result
|
||||
assert "[REDACTED]" in result
|
||||
for text, secret in [
|
||||
("api_key=sk-abcdef123456789", "sk-abcdef"),
|
||||
("aws_secret_key: AKIAIOSFODNN7EXAMPLE", "AKIAIOSFODNN7EXAMPLE"),
|
||||
("access_token: eyJhbGciOiJSUzI1NiJ9abcdef", "eyJhbGciOiJSUzI1NiJ9abcdef"),
|
||||
]:
|
||||
assert secret not in _scrub_secrets(text)
|
||||
assert "[REDACTED]" in _scrub_secrets(text)
|
||||
|
||||
def test_aws_secret_key_redacted(self):
|
||||
def test_non_secrets_pass_through(self):
|
||||
# Non-secret fields and values < 6 chars are not redacted
|
||||
from litellm._logging import _scrub_secrets
|
||||
|
||||
result = _scrub_secrets("aws_secret_key: AKIAIOSFODNN7EXAMPLE")
|
||||
assert "AKIAIOSFODNN7EXAMPLE" not in result
|
||||
assert "[REDACTED]" in result
|
||||
|
||||
def test_encryption_key_redacted(self):
|
||||
from litellm._logging import _scrub_secrets
|
||||
|
||||
result = _scrub_secrets("encryption_key = my-very-secret-key-123")
|
||||
assert "my-very-secret-key-123" not in result
|
||||
assert "[REDACTED]" in result
|
||||
|
||||
def test_access_token_redacted(self):
|
||||
# Covers access_token key-name pattern (replaces bare "token" which was too broad)
|
||||
from litellm._logging import _scrub_secrets
|
||||
|
||||
result = _scrub_secrets("access_token: eyJhbGciOiJSUzI1NiJ9abcdef")
|
||||
assert "eyJhbGciOiJSUzI1NiJ9abcdef" not in result
|
||||
assert "[REDACTED]" in result
|
||||
|
||||
def test_non_secret_field_not_redacted(self):
|
||||
from litellm._logging import _scrub_secrets
|
||||
|
||||
result = _scrub_secrets("model=gpt-4o, endpoint=https://api.openai.com")
|
||||
result = _scrub_secrets(
|
||||
"model=gpt-4o, endpoint=https://api.openai.com, api_key=abc"
|
||||
)
|
||||
assert "gpt-4o" in result
|
||||
assert "api.openai.com" in result
|
||||
|
||||
def test_short_value_not_redacted(self):
|
||||
# Values < 6 chars are not secrets (avoids redacting booleans/short flags)
|
||||
from litellm._logging import _scrub_secrets
|
||||
|
||||
result = _scrub_secrets("api_key=abc")
|
||||
assert "abc" in result
|
||||
assert "abc" in result # < 6 chars, not redacted
|
||||
|
||||
|
||||
class TestCredentialScrubberFilter:
|
||||
|
|
@ -75,7 +54,8 @@ class TestCredentialScrubberFilter:
|
|||
|
||||
f = CredentialScrubberFilter()
|
||||
record = self._make_record("api_key=sk-secret123456789")
|
||||
f.filter(record)
|
||||
result = f.filter(record)
|
||||
assert result is True # filter must never drop records
|
||||
assert "sk-secret123456789" not in record.msg
|
||||
assert "[REDACTED]" in record.msg
|
||||
|
||||
|
|
@ -148,15 +128,6 @@ class TestCredentialScrubberFilter:
|
|||
f.filter(record)
|
||||
assert record.msg == "plain message with no args"
|
||||
|
||||
def test_filter_always_returns_true(self):
|
||||
# Filter must never drop log records
|
||||
from litellm._logging import CredentialScrubberFilter
|
||||
|
||||
f = CredentialScrubberFilter()
|
||||
record = self._make_record("api_key=sk-secret123456789")
|
||||
result = f.filter(record)
|
||||
assert result is True
|
||||
|
||||
def test_filter_registered_on_verbose_logger(self):
|
||||
import litellm._logging as log_module
|
||||
|
||||
|
|
@ -171,14 +142,6 @@ class TestCredentialScrubberFilter:
|
|||
]
|
||||
assert "CredentialScrubberFilter" in filter_types
|
||||
|
||||
def test_filter_registered_on_verbose_router_logger(self):
|
||||
import litellm._logging as log_module
|
||||
|
||||
filter_types = [
|
||||
type(f).__name__ for f in log_module.verbose_router_logger.filters
|
||||
]
|
||||
assert "CredentialScrubberFilter" in filter_types
|
||||
|
||||
def test_filter_respects_disable_redaction_env_var(self):
|
||||
# Branch: _ENABLE_SECRET_REDACTION is False — filter must pass record unmodified
|
||||
from unittest.mock import patch
|
||||
|
|
@ -212,3 +175,34 @@ class TestCredentialScrubberFilter:
|
|||
f.filter(record)
|
||||
assert "sk-bytessecret123" not in str(record.args)
|
||||
assert "[REDACTED]" in str(record.args)
|
||||
|
||||
def test_exc_traceback_secret_redacted(self):
|
||||
# Branch: record.exc_info — secret in exception message is scrubbed from exc_text
|
||||
import sys
|
||||
|
||||
from litellm._logging import CredentialScrubberFilter
|
||||
|
||||
try:
|
||||
raise ValueError("api_key=sk-secretinexception12345")
|
||||
except ValueError:
|
||||
ei = sys.exc_info()
|
||||
f = CredentialScrubberFilter()
|
||||
record = self._make_record("error occurred")
|
||||
record.exc_info = ei
|
||||
f.filter(record)
|
||||
assert record.exc_text is not None
|
||||
assert "sk-secretinexception12345" not in record.exc_text
|
||||
|
||||
def test_extra_fields_secret_redacted(self):
|
||||
# Branch: extra fields via logger.debug("msg", extra={...})
|
||||
# Secret-named key → value replaced with [REDACTED]
|
||||
# Non-secret key with embedded key=value secret → value scrubbed inline
|
||||
from litellm._logging import CredentialScrubberFilter
|
||||
|
||||
f = CredentialScrubberFilter()
|
||||
record = self._make_record("msg")
|
||||
record.api_key = "sk-extrasecret123456789" # type: ignore[attr-defined]
|
||||
record.debug_info = "api_key=sk-embeddedsecret12345" # type: ignore[attr-defined]
|
||||
f.filter(record)
|
||||
assert getattr(record, "api_key") == "[REDACTED]"
|
||||
assert "sk-embeddedsecret12345" not in getattr(record, "debug_info", "")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue