feat(spend-logs): suppress traceback in SpendLogs error_information row
Some checks are pending
Unit Tests: Caching (Redis) / caching-redis (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run

Extend LITELLM_SUPPRESS_SPEND_LOG_TRACEBACKS to the failure callback so the
per-row Metadata pane in the UI no longer shows the stack trace when the
opt-in env var is set, matching the existing console-side suppression.

https://claude.ai/code/session_014dztoRbRnRvq54HL9EyHx6
This commit is contained in:
Claude 2026-05-02 00:19:01 +00:00 committed by Cursor Agent
parent dfd8c406ee
commit 5b775d1274
No known key found for this signature in database
3 changed files with 101 additions and 17 deletions

View file

@ -19,7 +19,10 @@ from litellm.proxy.auth.auth_checks import (
)
from litellm.proxy.auth.route_checks import RouteChecks
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error
from litellm.proxy.spend_tracking.spend_log_error_logger import (
should_suppress_spend_log_tracebacks,
spend_log_error,
)
from litellm.proxy.utils import ProxyUpdateSpend
from litellm.types.utils import StandardLoggingPayload
from litellm.utils import get_end_user_id_for_cost_tracking
@ -75,12 +78,13 @@ class _ProxyDBLogger(CustomLogger):
)
_metadata["user_api_key"] = user_api_key_dict.api_key
_metadata["status"] = "failure"
_metadata["error_information"] = (
StandardLoggingPayloadSetup.get_error_information(
original_exception=original_exception,
traceback_str=traceback_str,
)
_error_information = StandardLoggingPayloadSetup.get_error_information(
original_exception=original_exception,
traceback_str=traceback_str,
)
if should_suppress_spend_log_tracebacks():
_error_information = {**_error_information, "traceback": ""}
_metadata["error_information"] = _error_information
_metadata = await _ProxyDBLogger._enrich_failure_metadata_with_key_info(
metadata=_metadata,

View file

@ -1,18 +1,23 @@
"""
Logging helpers for spend-tracking error paths.
Proxy operators have asked for a way to keep their downstream log sinks free of
the stack traces that the spend-tracking machinery emits when it hits 4xx/5xx
or transient DB errors. The errors still need to be logged (and still need to
flow to Sentry via ``proxy_logging_obj.failure_handler``), but the multi-line
stack traces dominate the log volume and make the surrounding INFO/ERROR lines
hard to read.
Proxy operators have asked for a way to keep both their downstream log sinks
and the SpendLogs UI free of the stack traces that the spend-tracking
machinery emits when it hits 4xx/5xx or transient DB errors. The errors still
need to be logged (and still flow to Sentry via
``proxy_logging_obj.failure_handler``), but the multi-line stack traces
dominate log volume and clutter the per-row Metadata pane in the UI.
This module exposes ``spend_log_error`` a thin wrapper around
``verbose_proxy_logger.error`` that drops the traceback portion when the
operator has opted in via ``LITELLM_SUPPRESS_SPEND_LOG_TRACEBACKS=true`` and
the proxy logger is at INFO or above. At DEBUG the full traceback is always
preserved.
The opt-in is a single env var, ``LITELLM_SUPPRESS_SPEND_LOG_TRACEBACKS=true``,
gated by ``should_suppress_spend_log_tracebacks``. When it returns ``True``:
* ``spend_log_error`` drops the traceback from the console / structured log
record (this module), and
* the failure callback in ``proxy_track_cost_callback`` blanks the
``error_information.traceback`` field on the SpendLogs row before it is
persisted, so the UI's per-row Metadata pane stays clean.
At DEBUG the full traceback is always preserved so operators can still
troubleshoot. The UI suppression follows the same gate.
"""
import logging

View file

@ -990,3 +990,78 @@ async def test_async_post_call_failure_hook_uses_actual_start_time():
# Duration should be approximately 60 seconds, not 0
duration = (call_args["end_time"] - call_args["start_time"]).total_seconds()
assert duration >= 55, f"Duration should be ~60s, got {duration}s"
async def _invoke_failure_hook_with_raised_exception():
"""Run the failure hook with an exception that has a real ``__traceback__``.
Returns the metadata dict that was forwarded to ``update_database`` so the
caller can assert on its ``error_information`` payload.
"""
logger = _ProxyDBLogger()
user_api_key_dict = UserAPIKeyAuth(
api_key="test_api_key",
user_id="u",
team_id="t",
)
request_data = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"metadata": {},
"proxy_server_request": {},
}
try:
raise RuntimeError("boom-with-traceback")
except RuntimeError as exc:
original_exception = exc
with patch(
"litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter.update_database",
new_callable=AsyncMock,
) as mock_update_database:
await logger.async_post_call_failure_hook(
request_data=request_data,
original_exception=original_exception,
user_api_key_dict=user_api_key_dict,
)
call_args = mock_update_database.call_args[1]
return call_args["kwargs"]["litellm_params"]["metadata"]
@pytest.mark.asyncio
async def test_failure_hook_keeps_error_information_traceback_by_default(monkeypatch):
"""Without the opt-in env var, the SpendLogs row carries the full traceback."""
monkeypatch.delenv("LITELLM_SUPPRESS_SPEND_LOG_TRACEBACKS", raising=False)
metadata = await _invoke_failure_hook_with_raised_exception()
error_information = metadata["error_information"]
assert error_information["error_class"] == "RuntimeError"
assert error_information["error_message"] == "boom-with-traceback"
assert error_information["traceback"], "expected a non-empty traceback by default"
@pytest.mark.asyncio
async def test_failure_hook_blanks_error_information_traceback_when_env_set(
monkeypatch,
):
"""With the opt-in env var, the traceback in the SpendLogs row is blanked
so the per-row Metadata pane in the UI stays clean. The other fields
(error_class / error_message / error_code) are preserved."""
import logging
from litellm._logging import verbose_proxy_logger
monkeypatch.setenv("LITELLM_SUPPRESS_SPEND_LOG_TRACEBACKS", "true")
original_level = verbose_proxy_logger.level
verbose_proxy_logger.setLevel(logging.INFO)
try:
metadata = await _invoke_failure_hook_with_raised_exception()
finally:
verbose_proxy_logger.setLevel(original_level)
error_information = metadata["error_information"]
assert error_information["traceback"] == ""
assert error_information["error_class"] == "RuntimeError"
assert error_information["error_message"] == "boom-with-traceback"