litellm/tests/unit/rust_bridge/test_logger.py
yujonglee 1f68cc5649
refactor(rust-bridge): require the native extension (#48852)
* refactor(rust-bridge): require the native extension

_native ships in every wheel and the sdist will not install without it, yet the bridge loaded it optionally and carried None paths that could fail open for fork safety, exception mapping and OTLP errors. Delete loader.py and import _native directly. Tests inject a fake module through NativeBinding(source=...) or with_source() instead of patching the loader

The install smoke checks now verify the compiled module, and tests fail instead of skipping when _native is missing. The interactions spec canary installs the project so it has the extension

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): build Rust bridge in Windows smoke tests

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 13:16:56 -07:00

134 lines
5.5 KiB
Python

import logging
from typing import Final
import pytest
import litellm
from litellm._logging import (
DiagnosticProcessingFilter,
_python_process_diagnostic,
redact_secrets,
session_id_var,
trace_id_var,
verbose_logger,
)
from litellm.constants import MINIMUM_CUSTOM_KEY_LENGTH
from litellm.litellm_core_utils.secret_redaction import (
_python_redact_internal_details,
python_redact_string,
python_redact_structured_value,
)
from litellm.rust_bridge import diagnostics, logger
def test_native_records_preserve_metadata_and_redact_before_custom_handlers(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(verbose_logger, "handlers", [])
secret: Final = "sk-" + "a" * 48
message: Final = f"Authorization: Bearer {secret}"
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
logger.emit(
logging.WARNING, message, "native.rs", 42, "litellm_http", {"retry": True, "api_key": secret}, ("", "")
)
record: Final = caplog.records[0]
assert len(caplog.records) == 1
assert record.getMessage() == redact_secrets(message)
assert secret not in record.getMessage()
assert (record.pathname, record.lineno, record.funcName) == ("native.rs", 42, "litellm_http")
assert record.__dict__["rust_fields"]["retry"] is True
assert secret not in str(record.__dict__["rust_fields"])
def test_native_context_is_scoped_and_respects_correlation_setting(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "request_correlation_in_logs", True)
context_before: Final = logger.context()
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
logger.emit(logging.WARNING, "native", "native.rs", 1, "litellm_http", {}, ("session", "trace"))
monkeypatch.setattr(litellm, "request_correlation_in_logs", False)
logger.emit(logging.WARNING, "disabled", "native.rs", 2, "litellm_http", {}, ("hidden", "hidden"))
first, second = caplog.records
assert (first.__dict__["session_id"], first.__dict__["trace_id"]) == ("session", "trace")
assert "session_id" not in second.__dict__
assert "trace_id" not in second.__dict__
assert (session_id_var.get(), trace_id_var.get()) == context_before
def test_native_logging_observes_level_changes(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.ERROR, logger="LiteLLM"):
assert not logger.enabled(logging.WARNING)
logger.emit(logging.WARNING, "filtered", "native.rs", 1, "litellm_http", {}, ("", ""))
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
assert logger.enabled(logging.WARNING)
logger.emit(logging.WARNING, "visible", "native.rs", 1, "litellm_http", {}, ("", ""))
assert [record.getMessage() for record in caplog.records] == ["visible"]
@pytest.mark.parametrize(
"text",
(
"Authorization: Bearer abcdefghijklmnop",
"s3_secret_access_key=secret123",
"postgres://user:pass@database.internal/name",
'{"type":"service_account","private_key":"secret123"}',
"GET /v1?key=abcdefghij&page=2",
),
)
def test_native_credential_patterns_match_python(text: str) -> None:
from litellm.rust_bridge._native import NativeDiagnosticProcessor
processor: Final = NativeDiagnosticProcessor(MINIMUM_CUSTOM_KEY_LENGTH)
assert processor.redact_text(text) == python_redact_string(text)
assert processor.redact_structured_text("api_key", "secret123") == python_redact_structured_value(
"api_key", "secret123"
)
def test_native_client_redaction_matches_python() -> None:
from litellm.rust_bridge._native import NativeDiagnosticProcessor
text: Final = "error at /etc/secrets/config on db.internal\nTraceback (most recent call last):\nsecret"
processor: Final = NativeDiagnosticProcessor(MINIMUM_CUSTOM_KEY_LENGTH)
assert processor.redact_client_message(text) == _python_redact_internal_details(text)
def test_native_diagnostic_batch_matches_python() -> None:
from litellm.rust_bridge._native import NativeDiagnosticProcessor
message: Final = "é" * 110 + "sk-" + "q" * 48 + "界" * 1000
exception: Final = "document=" + "Q" * 200
stack: Final = "api_key=secret123"
leaves: Final = (("api_key", "secret123"), (None, "safe"))
processor: Final = NativeDiagnosticProcessor(MINIMUM_CUSTOM_KEY_LENGTH)
rust: Final = processor.process_diagnostic(message, exception, stack, leaves, (True, 20, 500))
python: Final = _python_process_diagnostic(message, exception, stack, leaves, True, 20, 500)
assert rust[:3] == python[:3]
assert tuple(rust[3]) == python[3]
assert rust[4] == python[4]
assert "sk-qq" not in rust[0]
assert len(rust[0]) <= 500
assert rust[3] == ["REDACTED", "safe"]
def test_missing_native_diagnostic_processor_falls_back_before_record_mutation(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("LITELLM_RUST", "1")
diagnostics.PROCESSOR.override(None)
try:
record: Final = logging.makeLogRecord({"name": "LiteLLM", "levelno": logging.INFO, "msg": "api_key=secret123"})
assert DiagnosticProcessingFilter().filter(record) is True
assert record.getMessage() == "REDACTED"
finally:
diagnostics.PROCESSOR.reset()
def test_unsupported_unicode_uses_safe_python_redaction(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LITELLM_RUST", "1")
assert redact_secrets("broken\ud800 api_key=secret123") == "broken\ud800 REDACTED"