mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-09 11:08:48 +00:00
30b28da2b7
* Pyroscope: require PYROSCOPE_APP_NAME and PYROSCOPE_SERVER_ADDRESS, add UTF-8 locale hint - No defaults for PYROSCOPE_APP_NAME or PYROSCOPE_SERVER_ADDRESS; fail at startup if unset when Pyroscope is enabled - Set LANG/LC_ALL to C.UTF-8 when unset to reduce malformed_profile (invalid UTF-8) rejections - Startup message suggests PYTHONUTF8=1 if server rejects profiles - Simplify LITELLM_ENABLE_PYROSCOPE in config_settings; document Pyroscope env vars as required with no default - Add pyroscope_profiling to sidebar (Alerting & Monitoring) - pyproject.toml: pyroscope-io as required dep on non-Windows (marker), in proxy extra * proxy: add PYROSCOPE_SAMPLE_RATE env, use verbose logging, fix int type - Add optional PYROSCOPE_SAMPLE_RATE env (integer, no default) - Pass sample_rate to pyroscope.configure() as int for pyroscope-io - Replace print with verbose_proxy_logger (info/warning) - Document PYROSCOPE_SAMPLE_RATE in config_settings.md * Address Greptile PR feedback: Pyroscope optional, docs, tests, docstring - pyproject.toml: mark pyroscope-io as optional=true (proxy extra only) - Add docs/my-website/docs/proxy/pyroscope_profiling.md (fix broken sidebar link) - Add tests/test_litellm/proxy/test_pyroscope.py for _init_pyroscope() - proxy_server: fix _init_pyroscope docstring (required server/app name, sample rate as int) * Update litellm/proxy/proxy_server.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
139 lines
4.4 KiB
Python
139 lines
4.4 KiB
Python
"""Unit tests for ProxyStartupEvent._init_pyroscope (Grafana Pyroscope profiling)."""
|
|
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from litellm.proxy.proxy_server import ProxyStartupEvent
|
|
|
|
|
|
def _mock_pyroscope_module():
|
|
"""Return a mock module so 'import pyroscope' succeeds in _init_pyroscope."""
|
|
m = MagicMock()
|
|
m.configure = MagicMock()
|
|
return m
|
|
|
|
|
|
def test_init_pyroscope_returns_cleanly_when_disabled():
|
|
"""When LITELLM_ENABLE_PYROSCOPE is false, _init_pyroscope returns without error."""
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_secret_bool",
|
|
return_value=False,
|
|
):
|
|
ProxyStartupEvent._init_pyroscope()
|
|
|
|
|
|
def test_init_pyroscope_raises_when_enabled_but_missing_app_name():
|
|
"""When LITELLM_ENABLE_PYROSCOPE is true but PYROSCOPE_APP_NAME is not set, raises ValueError."""
|
|
mock_pyroscope = _mock_pyroscope_module()
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_secret_bool",
|
|
return_value=True,
|
|
), patch.dict(
|
|
sys.modules,
|
|
{"pyroscope": mock_pyroscope},
|
|
), patch.dict(
|
|
os.environ,
|
|
{
|
|
"PYROSCOPE_APP_NAME": "",
|
|
"PYROSCOPE_SERVER_ADDRESS": "http://localhost:4040",
|
|
},
|
|
clear=False,
|
|
):
|
|
with pytest.raises(ValueError, match="PYROSCOPE_APP_NAME"):
|
|
ProxyStartupEvent._init_pyroscope()
|
|
|
|
|
|
def test_init_pyroscope_raises_when_enabled_but_missing_server_address():
|
|
"""When LITELLM_ENABLE_PYROSCOPE is true but PYROSCOPE_SERVER_ADDRESS is not set, raises ValueError."""
|
|
mock_pyroscope = _mock_pyroscope_module()
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_secret_bool",
|
|
return_value=True,
|
|
), patch.dict(
|
|
sys.modules,
|
|
{"pyroscope": mock_pyroscope},
|
|
), patch.dict(
|
|
os.environ,
|
|
{
|
|
"PYROSCOPE_APP_NAME": "myapp",
|
|
"PYROSCOPE_SERVER_ADDRESS": "",
|
|
},
|
|
clear=False,
|
|
):
|
|
with pytest.raises(ValueError, match="PYROSCOPE_SERVER_ADDRESS"):
|
|
ProxyStartupEvent._init_pyroscope()
|
|
|
|
|
|
def test_init_pyroscope_raises_when_sample_rate_invalid():
|
|
"""When PYROSCOPE_SAMPLE_RATE is not a number, raises ValueError."""
|
|
mock_pyroscope = _mock_pyroscope_module()
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_secret_bool",
|
|
return_value=True,
|
|
), patch.dict(
|
|
sys.modules,
|
|
{"pyroscope": mock_pyroscope},
|
|
), patch.dict(
|
|
os.environ,
|
|
{
|
|
"PYROSCOPE_APP_NAME": "myapp",
|
|
"PYROSCOPE_SERVER_ADDRESS": "http://localhost:4040",
|
|
"PYROSCOPE_SAMPLE_RATE": "not-a-number",
|
|
},
|
|
clear=False,
|
|
):
|
|
with pytest.raises(ValueError, match="PYROSCOPE_SAMPLE_RATE"):
|
|
ProxyStartupEvent._init_pyroscope()
|
|
|
|
|
|
def test_init_pyroscope_accepts_integer_sample_rate():
|
|
"""When enabled with valid config and integer sample rate, configures pyroscope."""
|
|
mock_pyroscope = _mock_pyroscope_module()
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_secret_bool",
|
|
return_value=True,
|
|
), patch.dict(
|
|
sys.modules,
|
|
{"pyroscope": mock_pyroscope},
|
|
), patch.dict(
|
|
os.environ,
|
|
{
|
|
"PYROSCOPE_APP_NAME": "myapp",
|
|
"PYROSCOPE_SERVER_ADDRESS": "http://localhost:4040",
|
|
"PYROSCOPE_SAMPLE_RATE": "100",
|
|
},
|
|
clear=False,
|
|
):
|
|
ProxyStartupEvent._init_pyroscope()
|
|
mock_pyroscope.configure.assert_called_once()
|
|
call_kw = mock_pyroscope.configure.call_args[1]
|
|
assert call_kw["app_name"] == "myapp"
|
|
assert call_kw["server_address"] == "http://localhost:4040"
|
|
assert call_kw["sample_rate"] == 100
|
|
|
|
|
|
def test_init_pyroscope_accepts_float_sample_rate_parsed_as_int():
|
|
"""PYROSCOPE_SAMPLE_RATE can be a float string; it is parsed as integer."""
|
|
mock_pyroscope = _mock_pyroscope_module()
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_secret_bool",
|
|
return_value=True,
|
|
), patch.dict(
|
|
sys.modules,
|
|
{"pyroscope": mock_pyroscope},
|
|
), patch.dict(
|
|
os.environ,
|
|
{
|
|
"PYROSCOPE_APP_NAME": "myapp",
|
|
"PYROSCOPE_SERVER_ADDRESS": "http://localhost:4040",
|
|
"PYROSCOPE_SAMPLE_RATE": "100.7",
|
|
},
|
|
clear=False,
|
|
):
|
|
ProxyStartupEvent._init_pyroscope()
|
|
call_kw = mock_pyroscope.configure.call_args[1]
|
|
assert call_kw["sample_rate"] == 100
|