litellm/tests/unit/integrations/test_prometheus_services.py
devin-ai-integration[bot] fa2c8984ba
test: move the unit half of 126 mixed legacy files into tests/unit (#45090)
* test: move the unit half of 126 mixed legacy files into tests/unit

* test: restore litellm globals that moved tests set

* test: finalize migration test cleanup

* test: restore original bodies of moved legacy tests

The move into tests/unit had rewritten 612 test bodies, and some of the rewrites dropped assertions. Each moved test now carries its original body from the legacy file, with only the imports, helpers, fake provider credentials and monkeypatched env it needs to run under tests/unit

test_timeout_streaming goes back to tests/local_testing because it needs the fake OpenAI endpoint server. The image payload fixture moves with its only user, and two tests that leaked global state (a registered model cost entry and queued logging tasks) are now isolated

* test: drop module imports shadowed by restored local imports

* test: assert on LiteLLM output in no-assertion moved tests and isolate leaks

Twenty no-assertion candidates get one assertion on the value LiteLLM returns, with the original lines unchanged. Four tests go back to their legacy files because they only check types or imports, write into the working directory, or cannot assert without a body change

Two moved tests leaked globals into later tests in the same worker, so monkeypatch fixtures now restore the retry-after header parser and the end user cost tracking flags

* test: drain queued logging tasks before the Phoenix span test

The moved Phoenix test counted spans from logging tasks that earlier tests had queued, so the drain fixture moves to tests/unit/conftest.py and both it and the Datadog batch test use it. test_factory_function goes back to its legacy file because its returned wrapper calls the real Assistants API and cannot be asserted on without a body change

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-07 14:07:43 -07:00

330 lines
11 KiB
Python

import json
import time
from typing import Final, cast
from unittest.mock import AsyncMock, patch
import litellm
import pytest
from fastapi.testclient import TestClient
from litellm._service_logger import ServiceLogging
from litellm.integrations.prometheus_services import (
PrometheusServicesLogger,
ServiceMetrics,
ServiceTypes,
)
from litellm.types.services import ServiceLoggerPayload
def test_is_metric_registered_does_not_use_registry_collect():
"""is_metric_registered() must use _names_to_collectors, not REGISTRY.collect() (perf; #19921)."""
from prometheus_client import CollectorRegistry, Counter, Histogram
registry = CollectorRegistry()
for i in range(80):
Counter(
f"litellm_service_{i}_total_requests",
"Total requests",
labelnames=["service"],
registry=registry,
)
Histogram(
f"litellm_service_{i}_latency",
"Latency",
labelnames=["service"],
registry=registry,
)
pl = PrometheusServicesLogger()
pl.REGISTRY = registry
original_collect = registry.collect
collect_called = []
def track_collect(*args, **kwargs):
collect_called.append(1)
return original_collect(*args, **kwargs)
registry.collect = track_collect
n_calls = 30 * 2
start = time.perf_counter()
for _ in range(30):
pl.is_metric_registered("litellm_service_0_latency")
pl.is_metric_registered("litellm_service_79_total_requests")
elapsed_s = time.perf_counter() - start
elapsed_ms = elapsed_s * 1000
per_call_us = (elapsed_s / n_calls) * 1_000_000 if n_calls else 0
n_collect = len(collect_called)
path = "slow (REGISTRY.collect)" if n_collect else "fast (_names_to_collectors)"
print(
f"\n is_metric_registered: {elapsed_ms:.2f} ms total | "
f"{per_call_us:.1f} µs/call | {n_calls} calls | {n_collect} collect() | {path}\n"
)
assert n_collect == 0, (
f"is_metric_registered() must not use REGISTRY.collect() when _names_to_collectors "
f"is available. Latency: {elapsed_ms:.2f} ms, {per_call_us:.1f} µs/call, {n_calls} calls, "
f"collect() called {n_collect} times."
)
assert (
elapsed_s < 0.05
), f"is_metric_registered() took {elapsed_ms:.2f} ms for {n_calls} calls; expected <50 ms."
def test_create_gauge_new():
"""Test creating a new gauge"""
pl = PrometheusServicesLogger()
# Create new gauge
gauge = pl.create_gauge(service="test_service", type_of_request="size")
assert gauge is not None
assert pl._get_metric("litellm_test_service_size") is gauge
def test_update_gauge():
"""Test updating a gauge's value"""
pl = PrometheusServicesLogger()
# Create a gauge to test with
gauge = pl.create_gauge(service="test_service", type_of_request="size")
# Mock the labels method to verify it's called correctly
with patch.object(gauge, "labels") as mock_labels:
mock_gauge = AsyncMock()
mock_labels.return_value = mock_gauge
# Call update_gauge
pl.update_gauge(gauge=gauge, labels="test_label", amount=42.5)
# Verify correct methods were called
mock_labels.assert_called_once_with("test_label")
mock_gauge.set.assert_called_once_with(42.5)
def test_services_logger_default_latency_buckets():
"""PrometheusServicesLogger uses the new reduced default latency buckets."""
from litellm.types.integrations.prometheus import LATENCY_BUCKETS
pl = PrometheusServicesLogger()
assert pl.latency_buckets == LATENCY_BUCKETS
assert 420.0 in pl.latency_buckets
assert 600.0 in pl.latency_buckets
assert 1.5 not in pl.latency_buckets
def test_services_logger_custom_latency_buckets():
"""prometheus_latency_buckets setting is respected by PrometheusServicesLogger."""
import litellm
from prometheus_client import REGISTRY
custom_buckets = [0.1, 0.5, 1.0, 5.0, 10.0]
original = litellm.prometheus_latency_buckets
for collector in list(REGISTRY._collector_to_names.keys()):
try:
REGISTRY.unregister(collector)
except Exception:
pass
try:
litellm.prometheus_latency_buckets = custom_buckets
pl = PrometheusServicesLogger()
assert pl.latency_buckets == tuple(custom_buckets)
finally:
litellm.prometheus_latency_buckets = original
for collector in list(REGISTRY._collector_to_names.keys()):
try:
REGISTRY.unregister(collector)
except Exception:
pass
def test_anthropic_wif_services_are_wired_into_the_registry():
"""Reverting the ANTHROPIC_WIF/ANTHROPIC_WIF_CACHE ServiceTypes members or their
DEFAULT_SERVICE_CONFIGS entries must fail here: the exchange service gets counters plus a
latency histogram, while the cache-hit service is counter-only so a hit can never fake a latency."""
from litellm.types.services import DEFAULT_SERVICE_CONFIGS
assert ServiceTypes.ANTHROPIC_WIF.value == "anthropic_wif"
assert ServiceTypes.ANTHROPIC_WIF_CACHE.value == "anthropic_wif_cache"
assert DEFAULT_SERVICE_CONFIGS["anthropic_wif"]["metrics"] == [ServiceMetrics.COUNTER, ServiceMetrics.HISTOGRAM]
assert DEFAULT_SERVICE_CONFIGS["anthropic_wif_cache"]["metrics"] == [ServiceMetrics.COUNTER]
pl = PrometheusServicesLogger()
wif_names = {obj._name for obj in pl.payload_to_prometheus_map["anthropic_wif"]}
assert wif_names == {
"litellm_anthropic_wif_latency",
"litellm_anthropic_wif_failed_requests",
"litellm_anthropic_wif_total_requests",
}
cache_names = {obj._name for obj in pl.payload_to_prometheus_map["anthropic_wif_cache"]}
assert cache_names == {
"litellm_anthropic_wif_cache_failed_requests",
"litellm_anthropic_wif_cache_total_requests",
}
@pytest.mark.asyncio
async def test_init_prometheus():
"""
- Run completion with caching
- Assert success callback gets called
"""
pl = PrometheusServicesLogger(mock_testing=True)
@pytest.mark.asyncio
async def test_service_logger_db_monitoring():
"""
Test prometheus monitoring for database operations
"""
litellm.service_callback = ["prometheus_system"]
sl = ServiceLogging()
# Create spy on prometheus logger's async_service_success_hook
with patch.object(
sl.prometheusServicesLogger,
"async_service_success_hook",
new_callable=AsyncMock,
) as mock_prometheus_success:
# Test DB success monitoring
await sl.async_service_success_hook(
service=ServiceTypes.DB,
duration=0.3,
call_type="query",
event_metadata={"query_type": "SELECT", "table": "api_keys"},
)
# Assert prometheus logger's success hook was called
mock_prometheus_success.assert_called_once()
# Optionally verify the payload
actual_payload = mock_prometheus_success.call_args[1]["payload"]
print("actual_payload sent to prometheus: ", actual_payload)
assert actual_payload.service == ServiceTypes.DB
assert actual_payload.duration == 0.3
assert actual_payload.call_type == "query"
assert actual_payload.is_error is False
@pytest.mark.asyncio
async def test_service_logger_db_monitoring_failure():
"""
Test prometheus monitoring for failed database operations
"""
litellm.service_callback = ["prometheus_system"]
sl = ServiceLogging()
# Create spy on prometheus logger's async_service_failure_hook
with patch.object(
sl.prometheusServicesLogger,
"async_service_failure_hook",
new_callable=AsyncMock,
) as mock_prometheus_failure:
# Test DB failure monitoring
test_error = Exception("Database connection failed")
await sl.async_service_failure_hook(
service=ServiceTypes.DB,
duration=0.3,
error=test_error,
call_type="query",
event_metadata={"query_type": "SELECT", "table": "api_keys"},
)
# Assert prometheus logger's failure hook was called
mock_prometheus_failure.assert_called_once()
# Verify the payload
actual_payload = mock_prometheus_failure.call_args[1]["payload"]
print("actual_payload sent to prometheus: ", actual_payload)
assert actual_payload.service == ServiceTypes.DB
assert actual_payload.duration == 0.3
assert actual_payload.call_type == "query"
assert actual_payload.is_error is True
assert actual_payload.error == "Database connection failed"
def test_get_metric_existing():
"""Test _get_metric when metric exists. _get_metric should return the metric object"""
pl = PrometheusServicesLogger()
# Create a metric first
hist = pl.create_histogram(
service="test_service", type_of_request="test_type_of_request"
)
# Test retrieving existing metric
retrieved_metric = pl._get_metric("litellm_test_service_test_type_of_request")
assert retrieved_metric is hist
assert retrieved_metric is not None
def test_get_metric_non_existing():
"""Test _get_metric when metric doesn't exist, returns None"""
pl = PrometheusServicesLogger()
# Test retrieving non-existent metric
non_existent = pl._get_metric("non_existent_metric")
assert non_existent is None
def test_create_histogram_new():
"""Test creating a new histogram"""
pl = PrometheusServicesLogger()
# Create new histogram
hist = pl.create_histogram(
service="test_service", type_of_request="test_type_of_request"
)
assert hist is not None
assert pl._get_metric("litellm_test_service_test_type_of_request") is hist
def test_create_histogram_existing():
"""Test creating a histogram that already exists"""
pl = PrometheusServicesLogger()
# Create initial histogram
hist1 = pl.create_histogram(
service="test_service", type_of_request="test_type_of_request"
)
# Create same histogram again
hist2 = pl.create_histogram(
service="test_service", type_of_request="test_type_of_request"
)
assert hist2 is hist1 # same object
assert pl._get_metric("litellm_test_service_test_type_of_request") is hist1
def test_create_counter_new():
"""Test creating a new counter"""
pl = PrometheusServicesLogger()
# Create new counter
counter = pl.create_counter(
service="test_service", type_of_request="test_type_of_request"
)
assert counter is not None
assert pl._get_metric("litellm_test_service_test_type_of_request") is counter
def test_create_counter_existing():
"""Test creating a counter that already exists"""
pl = PrometheusServicesLogger()
# Create initial counter
counter1 = pl.create_counter(
service="test_service", type_of_request="test_type_of_request"
)
# Create same counter again
counter2 = pl.create_counter(
service="test_service", type_of_request="test_type_of_request"
)
assert counter2 is counter1
assert pl._get_metric("litellm_test_service_test_type_of_request") is counter1