Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion litellm/proxy/dd_span_tagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ def tag_request(
- ``litellm.key_alias`` — human-readable alias for the API key
- ``litellm.key_hash`` — hashed API key (safe to log; never the raw secret)
- ``litellm.requested_model``— model name as sent by the client
- ``litellm.user_email`` — email of the authenticated user

Use cases:
- Trace all requests from a specific user/key: filter by ``litellm.key_alias`` or
``litellm.key_hash``.
- Trace all requests for a specific model: filter by ``litellm.requested_model``.
- Trace all requests from a specific person under JWT auth, where there is no key_alias:
filter by ``@litellm.user_email:"user@example.com"``. The value comes from the JWT claim
mapped by ``litellm_jwtauth.user_email_jwt_field``, or from the user row for virtual keys.

Note: key_alias / key_hash are not available for unauthenticated (e.g. 401) requests.
"""
Expand All @@ -53,8 +57,10 @@ def tag_request(
set_active_span_tag("litellm.key_hash", str(user_api_key_dict.token))
if requested_model:
set_active_span_tag("litellm.requested_model", str(requested_model))
if user_api_key_dict.user_email:
set_active_span_tag("litellm.user_email", str(user_api_key_dict.user_email))
except Exception:
verbose_proxy_logger.debug(
"Failed to tag active ddtrace span with key/model tags",
"Failed to tag active ddtrace span with key/model/user tags",
exc_info=True,
)
27 changes: 26 additions & 1 deletion tests/test_litellm/proxy/test_common_request_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2318,12 +2318,13 @@ def test_streaming_overhead_header_in_custom_headers_from_stream_hidden_params(
class TestDDSpanTaggerTagRequest:
"""Tests for DDSpanTagger.tag_request - key/model DD span tagging."""

def _make_user_api_key_dict(self, key_alias=None, token=None):
def _make_user_api_key_dict(self, key_alias=None, token=None, user_email=None):
from litellm.proxy._types import UserAPIKeyAuth

d = UserAPIKeyAuth()
d.key_alias = key_alias
d.token = token
d.user_email = user_email
return d

def test_tags_key_alias_and_model(self):
Expand Down Expand Up @@ -2364,6 +2365,30 @@ def test_only_model_tagged_when_no_key_info(self):

mock_set_tag.assert_called_once_with("litellm.requested_model", "claude-3-5-sonnet")

def test_tags_user_email(self):
"""user_email is tagged so JWT-authenticated requests are traceable per person."""
user_key = self._make_user_api_key_dict(user_email="user@example.com")

with patch("litellm.proxy.dd_span_tagger.set_active_span_tag") as mock_set_tag:
DDSpanTagger.tag_request(
user_api_key_dict=user_key,
requested_model=None,
)

mock_set_tag.assert_called_once_with("litellm.user_email", "user@example.com")

def test_no_user_email_tag_when_absent(self):
"""No user email tag when the authenticated identity has no email."""
user_key = self._make_user_api_key_dict(key_alias="my-prod-key", user_email=None)

with patch("litellm.proxy.dd_span_tagger.set_active_span_tag") as mock_set_tag:
DDSpanTagger.tag_request(
user_api_key_dict=user_key,
requested_model="gpt-4o",
)

assert all(call.args[0] != "litellm.user_email" for call in mock_set_tag.call_args_list)


class TestHasAttributeErrorInChain:
"""Tests for _has_attribute_error_in_chain helper."""
Expand Down
Loading