-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Add pyroscope for observability #21167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2fb793d
8087cab
dfa0293
e604a04
99a9c66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Grafana Pyroscope CPU profiling | ||
|
|
||
| LiteLLM proxy can send continuous CPU profiles to [Grafana Pyroscope](https://grafana.com/docs/pyroscope/latest/) when enabled via environment variables. This is optional and off by default. | ||
|
|
||
| ## Quick start | ||
|
|
||
| 1. **Install the optional dependency** (required only when enabling Pyroscope): | ||
|
|
||
| ```bash | ||
| pip install pyroscope-io | ||
| ``` | ||
|
|
||
| Or install the proxy extra: | ||
|
|
||
| ```bash | ||
| pip install "litellm[proxy]" | ||
| ``` | ||
|
|
||
| 2. **Set environment variables** before starting the proxy: | ||
|
|
||
| | Variable | Required | Description | | ||
| |----------|----------|-------------| | ||
| | `LITELLM_ENABLE_PYROSCOPE` | Yes (to enable) | Set to `true` to enable Pyroscope profiling. | | ||
| | `PYROSCOPE_APP_NAME` | Yes (when enabled) | Application name shown in the Pyroscope UI. | | ||
| | `PYROSCOPE_SERVER_ADDRESS` | Yes (when enabled) | Pyroscope server URL (e.g. `http://localhost:4040`). | | ||
| | `PYROSCOPE_SAMPLE_RATE` | No | Sample rate (integer). If unset, the pyroscope-io library default is used. | | ||
|
|
||
| 3. **Start the proxy**; profiling will begin automatically when the proxy starts. | ||
|
|
||
| ```bash | ||
| export LITELLM_ENABLE_PYROSCOPE=true | ||
| export PYROSCOPE_APP_NAME=litellm-proxy | ||
| export PYROSCOPE_SERVER_ADDRESS=http://localhost:4040 | ||
| litellm --config config.yaml | ||
| ``` | ||
|
|
||
| 4. **View profiles** in the Pyroscope (or Grafana) UI and select your `PYROSCOPE_APP_NAME`. | ||
|
|
||
| ## Notes | ||
|
|
||
| - **Optional dependency**: `pyroscope-io` is an optional dependency. If it is not installed and `LITELLM_ENABLE_PYROSCOPE=true`, the proxy will log a warning and continue without profiling. | ||
| - **Platform support**: The `pyroscope-io` package uses a native extension and is not available on all platforms (e.g. Windows is excluded by the package). | ||
| - **Other settings**: See [Configuration settings](/proxy/config_settings) for all proxy environment variables. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -69,6 +69,7 @@ polars = {version = "^1.31.0", optional = true, python = ">=3.10"} | |||
| semantic-router = {version = ">=0.1.12", optional = true, python = ">=3.9,<3.14"} | ||||
| mlflow = {version = ">3.1.4", optional = true, python = ">=3.10"} | ||||
| soundfile = {version = "^0.12.1", optional = true} | ||||
| pyroscope-io = {version = "^0.8", optional = true, markers = "sys_platform != 'win32'"} | ||||
| # grpcio constraints: | ||||
| # - 1.62.3+ required by grpcio-status | ||||
| # - 1.68.0-1.68.1 has reconnect bug (https://github.com/grpc/grpc/issues/38290) | ||||
|
|
@@ -104,6 +105,7 @@ proxy = [ | |||
| "rich", | ||||
| "polars", | ||||
| "soundfile", | ||||
| "pyroscope-io", | ||||
| ] | ||||
|
|
||||
| extra_proxy = [ | ||||
|
|
@@ -121,6 +123,8 @@ utils = [ | |||
| "numpydoc", | ||||
| ] | ||||
|
|
||||
|
|
||||
|
|
||||
|
Comment on lines
125
to
+127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extraneous blank lines added Two extra blank lines were introduced here. The rest of the file uses single blank lines between sections.
Suggested change
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||
| caching = ["diskcache"] | ||||
|
|
||||
| semantic-router = ["semantic-router"] | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| """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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing test for The Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing documentation page
The sidebar references
proxy/pyroscope_profiling, andconfig_settings.mdlinks to/proxy/pyroscope_profiling, but no corresponding documentation file (docs/my-website/docs/proxy/pyroscope_profiling.md) is included in this PR. This will cause a broken link in the docs site at build time or at runtime.