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:
parent
36c494fdd2
commit
73e32a31bf
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user