mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
refactor(logging): build failure redaction values in one shot
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
935f51a8b6
commit
63f91e7177
10 changed files with 80 additions and 35 deletions
|
|
@ -67,6 +67,22 @@ _BASE64_INLINE_PATTERN: Final = re.compile(
|
|||
)
|
||||
|
||||
|
||||
def _redacted_failure_error_fields(standard_logging_object: Mapping[str, object]) -> dict[str, object]:
|
||||
from litellm.litellm_core_utils.redact_messages import redact_error_information
|
||||
|
||||
fields: Final[dict[str, object]] = {} # mutable-ok: merged into the standard_logging_object copy below
|
||||
if standard_logging_object.get("error_str"):
|
||||
fields["error_str"] = REDACTED_BY_LITELLM
|
||||
error_information: Final = standard_logging_object.get("error_information")
|
||||
if isinstance(error_information, Mapping):
|
||||
fields["error_information"] = redact_error_information(
|
||||
cast( # cast-ok: same TypedDict shape as the input mapping
|
||||
StandardLoggingPayloadErrorInformation, error_information
|
||||
)
|
||||
)
|
||||
return fields
|
||||
|
||||
|
||||
class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callback#callback-class
|
||||
# Class variables or attributes
|
||||
server_fulfilled_tool_names: ClassVar[frozenset[str]] = frozenset()
|
||||
|
|
@ -905,7 +921,9 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac
|
|||
|
||||
This method handles two features:
|
||||
1. turn_off_message_logging: When True, redacts messages and responses (unless the callback
|
||||
redacts them itself, see `redacts_messages_itself`)
|
||||
redacts them itself, see `redacts_messages_itself`), and redacts `error_str`,
|
||||
`error_information`'s message/traceback and `traceback_exception` independent of
|
||||
`redacts_messages_itself`
|
||||
2. standard_logging_payload_excluded_fields: Removes specified fields entirely
|
||||
|
||||
Return a modified copy of the provided logging payload.
|
||||
|
|
@ -966,15 +984,7 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac
|
|||
standard_logging_object_copy["response"] = model_response_dict
|
||||
|
||||
if turn_off_message_logging:
|
||||
if standard_logging_object_copy.get("error_str"):
|
||||
standard_logging_object_copy["error_str"] = REDACTED_BY_LITELLM
|
||||
error_information: Final = standard_logging_object_copy.get("error_information")
|
||||
if isinstance(error_information, Mapping):
|
||||
from litellm.litellm_core_utils.redact_messages import redact_error_information
|
||||
|
||||
standard_logging_object_copy["error_information"] = redact_error_information(
|
||||
cast(StandardLoggingPayloadErrorInformation, dict(error_information))
|
||||
)
|
||||
standard_logging_object_copy.update(_redacted_failure_error_fields(standard_logging_object_copy))
|
||||
|
||||
params: Final = model_call_details.get("litellm_params")
|
||||
request: Final = params.get("proxy_server_request") if isinstance(params, dict) else None
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class MlflowLogger(CustomLogger):
|
|||
span.add_event(
|
||||
SpanEvent(
|
||||
name="exception",
|
||||
attributes={
|
||||
attributes={ # mutable-ok: mlflow SpanEvent expects a plain dict of attributes
|
||||
"exception.type": type(exception).__name__,
|
||||
"exception.message": REDACTED_BY_LITELLM,
|
||||
"exception.stacktrace": REDACTED_BY_LITELLM,
|
||||
|
|
|
|||
|
|
@ -2244,7 +2244,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
|||
if redact:
|
||||
span.record_exception(
|
||||
exception,
|
||||
attributes={
|
||||
attributes={ # mutable-ok: record_exception accepts a dict of event attributes
|
||||
"exception.message": REDACTED_BY_LITELLM,
|
||||
"exception.stacktrace": REDACTED_BY_LITELLM,
|
||||
},
|
||||
|
|
@ -3601,13 +3601,13 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
|||
should_redact_message_logging,
|
||||
)
|
||||
|
||||
redact: Final = should_redact_message_logging({})
|
||||
redact: Final = should_redact_message_logging({}) # mutable-ok: read-only probe for the global flag
|
||||
error_information: Final = StandardLoggingPayloadSetup.get_error_information(original_exception=exception)
|
||||
error_information["error_code"] = str(status_code)
|
||||
self._record_exception_on_span(
|
||||
span=span,
|
||||
kwargs={
|
||||
"standard_logging_object": {
|
||||
kwargs={ # mutable-ok: _record_exception_on_span reads this kwargs dict
|
||||
"standard_logging_object": { # mutable-ok: standard_logging_object shape the recorder expects
|
||||
"error_information": redact_error_information(error_information) if redact else error_information
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ if TYPE_CHECKING:
|
|||
|
||||
LITELLM_TRACER_NAME: Final = "litellm"
|
||||
_published_v2_provider: ApiTracerProvider | None = None
|
||||
_GLOBAL_REDACTION_PROBE: Final[dict[str, object]] = {} # mutable-ok: read-only global-redaction probe
|
||||
|
||||
|
||||
def _span_error_from_exception(
|
||||
|
|
@ -753,7 +754,9 @@ class OpenTelemetryV2(CustomLogger):
|
|||
if is_recordable_span(span):
|
||||
stamp_error(
|
||||
span,
|
||||
_span_error_from_exception(exc, redact_content=should_redact_message_logging({})),
|
||||
_span_error_from_exception(
|
||||
exc, redact_content=should_redact_message_logging(_GLOBAL_REDACTION_PROBE)
|
||||
),
|
||||
record_event=False,
|
||||
set_status=False,
|
||||
)
|
||||
|
|
@ -798,7 +801,9 @@ class OpenTelemetryV2(CustomLogger):
|
|||
stamp_error(
|
||||
span,
|
||||
_span_error_from_exception(
|
||||
exception, status_code=status_code, redact_content=should_redact_message_logging({})
|
||||
exception,
|
||||
status_code=status_code,
|
||||
redact_content=should_redact_message_logging(_GLOBAL_REDACTION_PROBE),
|
||||
),
|
||||
record_event=not already_stamped,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import asyncio
|
|||
import copy
|
||||
import inspect
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import litellm
|
||||
|
|
@ -186,6 +187,13 @@ def redacted_standard_logging_payload(payload: Mapping[str, object]) -> Mapping[
|
|||
return _redact_standard_logging_object(payload)
|
||||
|
||||
|
||||
_REDACTED_ERROR_FIELDS: Final = ("error_message", "traceback")
|
||||
|
||||
|
||||
def _is_non_empty_str(value: object) -> bool:
|
||||
return isinstance(value, str) and bool(value)
|
||||
|
||||
|
||||
def redact_error_information(
|
||||
error_information: StandardLoggingPayloadErrorInformation,
|
||||
) -> StandardLoggingPayloadErrorInformation:
|
||||
|
|
@ -194,12 +202,17 @@ def redact_error_information(
|
|||
quote the prompt (``error_message``, ``traceback``) replaced by ``REDACTED_BY_LITELLM``
|
||||
when they are non-empty strings. Every other field is carried over unchanged.
|
||||
"""
|
||||
redacted: Final = dict(error_information)
|
||||
for field in ("error_message", "traceback"):
|
||||
value: Final = redacted.get(field)
|
||||
if isinstance(value, str) and value:
|
||||
redacted[field] = REDACTED_BY_LITELLM
|
||||
return cast(StandardLoggingPayloadErrorInformation, redacted)
|
||||
redacted_fields: Final = MappingProxyType(
|
||||
{
|
||||
field: REDACTED_BY_LITELLM
|
||||
for field in _REDACTED_ERROR_FIELDS
|
||||
if _is_non_empty_str(error_information.get(field))
|
||||
}
|
||||
)
|
||||
return cast( # cast-ok: same TypedDict shape as the input, two fields narrowed to the sentinel
|
||||
StandardLoggingPayloadErrorInformation,
|
||||
{**error_information, **redacted_fields}, # mutable-ok: callers pop/update keys on the fresh payload dict
|
||||
)
|
||||
|
||||
|
||||
def should_redact_failed_request(request_data: Mapping[str, object]) -> bool:
|
||||
|
|
@ -209,19 +222,32 @@ def should_redact_failed_request(request_data: Mapping[str, object]) -> bool:
|
|||
``litellm_metadata`` is included only when present so
|
||||
``get_metadata_variable_name_from_kwargs`` resolves ``metadata`` for chat routes.
|
||||
"""
|
||||
litellm_params: Final[dict[str, object]] = {"metadata": request_data.get("metadata")}
|
||||
if "litellm_metadata" in request_data:
|
||||
litellm_params["litellm_metadata"] = request_data.get("litellm_metadata")
|
||||
return should_redact_message_logging(
|
||||
litellm_params: Final = MappingProxyType(
|
||||
{
|
||||
key: request_data.get(key)
|
||||
for key in ("metadata", "litellm_metadata")
|
||||
if key == "metadata" or key in request_data
|
||||
}
|
||||
)
|
||||
return should_redact_message_logging(
|
||||
{ # mutable-ok: the model_call_details shape the decision helper reads
|
||||
"litellm_params": litellm_params,
|
||||
"standard_callback_dynamic_params": {
|
||||
"standard_callback_dynamic_params": { # mutable-ok: dynamic-params slot the helper reads
|
||||
"turn_off_message_logging": request_data.get("turn_off_message_logging")
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def maybe_redact_error_information(
|
||||
error_information: StandardLoggingPayloadErrorInformation,
|
||||
request_data: Mapping[str, object],
|
||||
) -> StandardLoggingPayloadErrorInformation:
|
||||
if should_redact_failed_request(request_data):
|
||||
return redact_error_information(error_information)
|
||||
return error_information
|
||||
|
||||
|
||||
def _redact_standard_logging_object(payload: Mapping[str, object]) -> dict[str, object]:
|
||||
standard_logging_object: Final = copy.deepcopy(without_classifier_audit(payload))
|
||||
redacted_str: Final = REDACTED_BY_LITELLM
|
||||
|
|
@ -252,7 +278,9 @@ def _redact_standard_logging_object(payload: Mapping[str, object]) -> dict[str,
|
|||
error_information: Final = standard_logging_object.get("error_information")
|
||||
if isinstance(error_information, Mapping):
|
||||
standard_logging_object["error_information"] = redact_error_information(
|
||||
cast(StandardLoggingPayloadErrorInformation, dict(error_information))
|
||||
cast( # cast-ok: same TypedDict shape as the input mapping
|
||||
StandardLoggingPayloadErrorInformation, error_information
|
||||
)
|
||||
)
|
||||
return standard_logging_object
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ from litellm.litellm_core_utils.core_helpers import (
|
|||
)
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import guardrail_information_cost
|
||||
from litellm.litellm_core_utils.redact_messages import (
|
||||
redact_error_information,
|
||||
should_redact_failed_request,
|
||||
)
|
||||
from litellm.litellm_core_utils.redact_messages import maybe_redact_error_information
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.auth_checks import (
|
||||
get_key_object,
|
||||
|
|
@ -175,8 +172,9 @@ class _ProxyDBLogger(CustomLogger):
|
|||
original_exception=original_exception,
|
||||
traceback_str=traceback_str,
|
||||
)
|
||||
if should_redact_failed_request(request_data):
|
||||
_error_information = redact_error_information(_error_information)
|
||||
_error_information = maybe_redact_error_information(
|
||||
error_information=_error_information, request_data=request_data
|
||||
)
|
||||
if should_suppress_spend_log_tracebacks():
|
||||
# Drop the traceback key entirely so the per-row Metadata pane in
|
||||
# the UI (which renders the JSON blob verbatim) doesn't show a
|
||||
|
|
|
|||
|
|
@ -3239,6 +3239,7 @@ def test_provisional_close_then_payload_close_does_not_duplicate():
|
|||
llm_spans = [s for s in exporter.get_finished_spans() if s.name.startswith("chat")]
|
||||
assert len(llm_spans) == 1
|
||||
|
||||
|
||||
def test_async_post_call_failure_hook_redacts_error_text_when_gated():
|
||||
"""With message redaction on, the proxy-level failure span must not carry the
|
||||
prompt through error.message / the exception event, while error.type and the
|
||||
|
|
|
|||
|
|
@ -6763,6 +6763,7 @@ class TestOpenTelemetryNonInferenceUsage(unittest.TestCase):
|
|||
self._time_per_output_token_calls("aget_responses", response_obj=self.BACKGROUND_RESPONSE_OBJ), 1
|
||||
)
|
||||
|
||||
|
||||
SECRET_PROMPT = "secret-prompt-marker"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9030,6 +9030,7 @@ def test_signoz_dispatch_requires_an_endpoint(monkeypatch):
|
|||
monkeypatch.delenv("LITELLM_OTEL_V2", raising=False)
|
||||
is_otel_v2_enabled.cache_clear()
|
||||
|
||||
|
||||
class _CapturingFailureLogger(CustomLogger):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,7 @@ def test_perform_redaction_drops_the_served_output_texts_from_the_callback_kwarg
|
|||
perform_redaction(details, None)
|
||||
assert SERVED_OUTPUT_TEXTS_KEY not in details
|
||||
|
||||
|
||||
def _error_information(**overrides):
|
||||
info = {
|
||||
"error_code": "400",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue