feat(prometheus): add user_email and user_alias to user budget metrics (#28155)

* feat(prometheus): add user_email and user_alias to user budget metrics

User budget Prometheus gauges now expose human-readable labels alongside
user_id, matching team and API key budget metrics for Grafana filtering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(prometheus): gate user budget email/alias labels behind opt-in flag

Address greptile review: adding labels to existing metrics is a
breaking cardinality change. Gate behind
prometheus_user_budget_label_include_email_alias=True (default: False)
so existing dashboards and recording rules are unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sameer Kankute 2026-05-19 04:58:14 +05:30 committed by GitHub
parent 36c494fdd2
commit 73e32a31bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 129 additions and 18 deletions

View File

@ -416,6 +416,7 @@ custom_prometheus_metadata_labels: List[str] = []
custom_prometheus_tags: List[str] = []
prometheus_metrics_config: Optional[List] = None
prometheus_emit_stream_label: bool = False
prometheus_user_budget_label_include_email_alias: bool = False
prometheus_end_user_metrics_max_series_per_metric: Optional[int] = 10000
prometheus_end_user_metrics_ttl_seconds: Optional[float] = 3600.0
prometheus_end_user_metrics_cleanup_interval_seconds: Optional[float] = 60.0

View File

@ -3540,6 +3540,10 @@ class PrometheusLogger(CustomLogger):
user_object.budget_reset_at = user_info.budget_reset_at
if user_object.max_budget is None and user_info.max_budget is not None:
user_object.max_budget = user_info.max_budget
if user_info.user_email is not None:
user_object.user_email = user_info.user_email
if user_info.user_alias is not None:
user_object.user_alias = user_info.user_alias
return user_object
@ -3556,6 +3560,8 @@ class PrometheusLogger(CustomLogger):
"""
enum_values = UserAPIKeyLabelValues(
user=user.user_id,
user_email=user.user_email or "",
user_alias=user.user_alias or "",
)
_labels = prometheus_label_factory(

View File

@ -160,6 +160,7 @@ class UserAPIKeyLabelNames(Enum):
END_USER = "end_user"
USER = "user"
USER_EMAIL = "user_email"
USER_ALIAS = "user_alias"
API_KEY_HASH = "hashed_api_key"
API_KEY_ALIAS = "api_key_alias"
TEAM = "team"
@ -533,17 +534,9 @@ class PrometheusMetricLabels:
UserAPIKeyLabelNames.USER.value,
]
litellm_user_max_budget_metric = [
UserAPIKeyLabelNames.USER.value,
]
litellm_user_max_budget_metric = litellm_remaining_user_budget_metric
litellm_user_budget_remaining_hours_metric = [
UserAPIKeyLabelNames.USER.value,
]
litellm_user_budget_remaining_hours_metric = [
UserAPIKeyLabelNames.USER.value,
]
litellm_user_budget_remaining_hours_metric = litellm_remaining_user_budget_metric
litellm_remaining_api_key_requests_for_model = [
UserAPIKeyLabelNames.API_KEY_HASH.value,
@ -730,6 +723,22 @@ class PrometheusMetricLabels:
):
custom_labels.append(UserAPIKeyLabelNames.STREAM.value)
_user_budget_metrics = {
"litellm_remaining_user_budget_metric",
"litellm_user_max_budget_metric",
"litellm_user_budget_remaining_hours_metric",
}
if (
label_name in _user_budget_metrics
and litellm.prometheus_user_budget_label_include_email_alias is True
):
for label in [
UserAPIKeyLabelNames.USER_EMAIL.value,
UserAPIKeyLabelNames.USER_ALIAS.value,
]:
if label not in default_labels and label not in custom_labels:
custom_labels.append(label)
if label_name in PrometheusMetricLabels._org_label_metrics:
for label in [
UserAPIKeyLabelNames.ORG_ID.value,
@ -759,6 +768,7 @@ class UserAPIKeyLabelValues:
end_user: Optional[str] = None
user: Optional[str] = None
user_email: Optional[str] = None
user_alias: Optional[str] = None
hashed_api_key: Optional[str] = None
api_key_alias: Optional[str] = None
team: Optional[str] = None

View File

@ -610,22 +610,18 @@ def extract_user_budget_metrics(metrics_text: str, user_id: str) -> Dict[str, fl
# Escape user_id for regex pattern matching
escaped_user_id = re.escape(user_id)
# Get remaining budget
remaining_pattern = (
f'litellm_remaining_user_budget_metric{{user="{escaped_user_id}"}} ([0-9.]+)'
)
# Get remaining budget (user_email and user_alias may also be present as labels)
remaining_pattern = rf'litellm_remaining_user_budget_metric{{[^}}]*user="{escaped_user_id}"[^}}]*}} ([0-9.]+)'
remaining_match = re.search(remaining_pattern, metrics_text)
metrics["remaining"] = float(remaining_match.group(1)) if remaining_match else None
# Get total budget
total_pattern = (
f'litellm_user_max_budget_metric{{user="{escaped_user_id}"}} ([0-9.]+)'
)
total_pattern = rf'litellm_user_max_budget_metric{{[^}}]*user="{escaped_user_id}"[^}}]*}} ([0-9.]+)'
total_match = re.search(total_pattern, metrics_text)
metrics["total"] = float(total_match.group(1)) if total_match else None
# Get remaining hours
hours_pattern = f'litellm_user_budget_remaining_hours_metric{{user="{escaped_user_id}"}} ([0-9.]+)'
hours_pattern = rf'litellm_user_budget_remaining_hours_metric{{[^}}]*user="{escaped_user_id}"[^}}]*}} ([0-9.]+)'
hours_match = re.search(hours_pattern, metrics_text)
metrics["remaining_hours"] = float(hours_match.group(1)) if hours_match else None

View File

@ -460,6 +460,104 @@ async def test_assemble_user_object_does_not_override_metadata_max_budget(
), "max_budget from metadata must not be replaced by the DB value"
async def test_assemble_user_object_populates_user_email_and_alias_from_db(
prometheus_logger,
):
db_user = MagicMock()
db_user.max_budget = None
db_user.budget_reset_at = None
db_user.user_email = "alice@example.com"
db_user.user_alias = "Alice"
with patch("litellm.proxy.auth.auth_checks.get_user_object") as mock_get_user:
mock_get_user.return_value = db_user
user_object = await prometheus_logger._assemble_user_object(
user_id="user-abc-123",
spend=10.0,
max_budget=None,
response_cost=0.5,
)
assert user_object.user_email == "alice@example.com"
assert user_object.user_alias == "Alice"
def test_set_user_budget_metrics_default_no_email_alias_labels(
prometheus_logger,
):
"""By default (flag off), only user label is emitted."""
import litellm
from litellm.proxy._types import LiteLLM_UserTable
litellm.prometheus_user_budget_label_include_email_alias = False
user = LiteLLM_UserTable(
user_id="user-abc-123",
user_email="alice@example.com",
user_alias="Alice",
spend=25.0,
max_budget=100.0,
budget_reset_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
)
prometheus_logger.litellm_remaining_user_budget_metric = MagicMock()
prometheus_logger.litellm_user_max_budget_metric = MagicMock()
prometheus_logger.litellm_user_budget_remaining_hours_metric = MagicMock()
prometheus_logger._set_user_budget_metrics(user)
prometheus_logger.litellm_remaining_user_budget_metric.labels.assert_called_once_with(
user="user-abc-123",
)
def test_set_user_budget_metrics_includes_user_email_and_alias_labels_when_opted_in(
prometheus_logger,
):
"""When prometheus_user_budget_label_include_email_alias=True, email+alias labels appear."""
import litellm
from litellm.proxy._types import LiteLLM_UserTable
litellm.prometheus_user_budget_label_include_email_alias = True
user = LiteLLM_UserTable(
user_id="user-abc-123",
user_email="alice@example.com",
user_alias="Alice",
spend=25.0,
max_budget=100.0,
budget_reset_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
)
prometheus_logger.litellm_remaining_user_budget_metric = MagicMock()
prometheus_logger.litellm_user_max_budget_metric = MagicMock()
prometheus_logger.litellm_user_budget_remaining_hours_metric = MagicMock()
try:
prometheus_logger._set_user_budget_metrics(user)
prometheus_logger.litellm_remaining_user_budget_metric.labels.assert_called_once_with(
user="user-abc-123",
user_email="alice@example.com",
user_alias="Alice",
)
prometheus_logger.litellm_remaining_user_budget_metric.labels().set.assert_called_once_with(
75.0
)
prometheus_logger.litellm_user_max_budget_metric.labels.assert_called_once_with(
user="user-abc-123",
user_email="alice@example.com",
user_alias="Alice",
)
prometheus_logger.litellm_user_budget_remaining_hours_metric.labels.assert_called_once_with(
user="user-abc-123",
user_email="alice@example.com",
user_alias="Alice",
)
finally:
litellm.prometheus_user_budget_label_include_email_alias = False
async def test_set_user_budget_metrics_after_api_request_no_inf_when_metadata_budget_none(
prometheus_logger,
):