From b4f9e14a44789b408c3ceab56d01671f33a7b3d0 Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 28 Jul 2026 14:14:53 +0000 Subject: [PATCH 1/2] feat(dd_span_tagger): emit litellm_user_email span tag for JWT-authenticated requests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/dd_span_tagger.py | 8 +++++- .../proxy/test_common_request_processing.py | 27 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/dd_span_tagger.py b/litellm/proxy/dd_span_tagger.py index 08b7d928d0e..1fda0c638d4 100644 --- a/litellm/proxy/dd_span_tagger.py +++ b/litellm/proxy/dd_span_tagger.py @@ -38,11 +38,15 @@ class DDSpanTagger: - ``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. """ @@ -53,8 +57,10 @@ class DDSpanTagger: 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, ) diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 58f81cdad35..287e6f2f80c 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -2318,12 +2318,13 @@ class TestStreamingOverheadHeader: 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): @@ -2364,6 +2365,30 @@ class TestDDSpanTaggerTagRequest: 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.""" From 819faa01c336d825619b3592ed1c1d763a4156d4 Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 28 Jul 2026 14:40:04 +0000 Subject: [PATCH 2/2] refactor(dd_span_tagger): use dotted litellm.user_email tag for consistency Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/dd_span_tagger.py | 6 +++--- tests/test_litellm/proxy/test_common_request_processing.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/dd_span_tagger.py b/litellm/proxy/dd_span_tagger.py index 1fda0c638d4..cafae9bb2a3 100644 --- a/litellm/proxy/dd_span_tagger.py +++ b/litellm/proxy/dd_span_tagger.py @@ -38,14 +38,14 @@ class DDSpanTagger: - ``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 + - ``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 + 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. @@ -58,7 +58,7 @@ class DDSpanTagger: 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)) + 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/user tags", diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 287e6f2f80c..312c3d2a884 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -2375,7 +2375,7 @@ class TestDDSpanTaggerTagRequest: requested_model=None, ) - mock_set_tag.assert_called_once_with("litellm_user_email", "user@example.com") + 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.""" @@ -2387,7 +2387,7 @@ class TestDDSpanTaggerTagRequest: requested_model="gpt-4o", ) - assert all(call.args[0] != "litellm_user_email" for call in mock_set_tag.call_args_list) + assert all(call.args[0] != "litellm.user_email" for call in mock_set_tag.call_args_list) class TestHasAttributeErrorInChain: