From 5b775d1274f79bb09165e08e7cb85198cfe37632 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 00:19:01 +0000 Subject: [PATCH] feat(spend-logs): suppress traceback in SpendLogs error_information row 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 --- .../proxy/hooks/proxy_track_cost_callback.py | 16 ++-- .../spend_tracking/spend_log_error_logger.py | 27 ++++--- .../hooks/test_proxy_track_cost_callback.py | 75 +++++++++++++++++++ 3 files changed, 101 insertions(+), 17 deletions(-) diff --git a/litellm/proxy/hooks/proxy_track_cost_callback.py b/litellm/proxy/hooks/proxy_track_cost_callback.py index 0a677b5423a..823e19025e4 100644 --- a/litellm/proxy/hooks/proxy_track_cost_callback.py +++ b/litellm/proxy/hooks/proxy_track_cost_callback.py @@ -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, diff --git a/litellm/proxy/spend_tracking/spend_log_error_logger.py b/litellm/proxy/spend_tracking/spend_log_error_logger.py index 44414de3828..bcb90f9bbd4 100644 --- a/litellm/proxy/spend_tracking/spend_log_error_logger.py +++ b/litellm/proxy/spend_tracking/spend_log_error_logger.py @@ -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 diff --git a/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py b/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py index 8b5835139b4..ad8c01db0a5 100644 --- a/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py +++ b/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py @@ -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"