mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 10:21:52 +00:00
Add Langfuse OpenTelemetry Integration (#11607)
* feat(langfuse_otel): add Langfuse OpenTelemetry integration for observability - Introduced a new integration for Langfuse OpenTelemetry, allowing users to send LiteLLM traces and observability data. - Updated sidebars to include documentation for the new integration. - Added example usage and configuration details in the documentation. - Implemented necessary classes and methods to handle OpenTelemetry attributes and configuration. - Included tests to validate the integration functionality and environment variable handling. Still WIP * Remove example script for Langfuse OpenTelemetry integration with LiteLLM
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# Langfuse OpenTelemetry Integration
|
||||
|
||||
The Langfuse OpenTelemetry integration allows you to send LiteLLM traces and observability data to Langfuse using the OpenTelemetry protocol. This provides a standardized way to collect and analyze your LLM usage data.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatic trace collection for all LiteLLM requests
|
||||
- Support for Langfuse Cloud (EU and US regions)
|
||||
- Support for self-hosted Langfuse instances
|
||||
- Custom endpoint configuration
|
||||
- Secure authentication using Basic Auth
|
||||
- Consistent attribute mapping with other OTEL integrations
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Langfuse Account**: Sign up at [Langfuse Cloud](https://cloud.langfuse.com) or set up a self-hosted instance
|
||||
2. **API Keys**: Get your public and secret keys from your Langfuse project settings
|
||||
3. **Dependencies**: Install required packages:
|
||||
```bash
|
||||
pip install litellm opentelemetry-api opentelemetry-sdk
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description | Example |
|
||||
|----------|----------|-------------|---------|
|
||||
| `LANGFUSE_PUBLIC_KEY` | Yes | Your Langfuse public key | `pk-lf-...` |
|
||||
| `LANGFUSE_SECRET_KEY` | Yes | Your Langfuse secret key | `sk-lf-...` |
|
||||
| `LANGFUSE_HOST` | No | Langfuse host URL | `https://us.cloud.langfuse.com` (default) |
|
||||
|
||||
### Endpoint Resolution
|
||||
|
||||
The integration automatically constructs the OTEL endpoint from the `LANGFUSE_HOST`:
|
||||
- **Default (US)**: `https://us.cloud.langfuse.com/api/public/otel`
|
||||
- **EU Region**: `https://cloud.langfuse.com/api/public/otel`
|
||||
- **Self-hosted**: `{LANGFUSE_HOST}/api/public/otel`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```python
|
||||
import os
|
||||
import litellm
|
||||
|
||||
# Set your Langfuse credentials
|
||||
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..."
|
||||
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..."
|
||||
|
||||
# Enable Langfuse OTEL integration
|
||||
litellm.callbacks = ["langfuse_otel"]
|
||||
|
||||
# Make LLM requests as usual
|
||||
response = litellm.completion(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Hello!"}]
|
||||
)
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```python
|
||||
import os
|
||||
import litellm
|
||||
|
||||
# Set your Langfuse credentials
|
||||
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..."
|
||||
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..."
|
||||
|
||||
# Use EU region
|
||||
os.environ["LANGFUSE_HOST"] = "https://cloud.langfuse.com" # EU region
|
||||
# os.environ["LANGFUSE_HOST"] = "https://us.cloud.langfuse.com" # US region (default)
|
||||
|
||||
# Or use self-hosted instance
|
||||
# os.environ["LANGFUSE_HOST"] = "https://my-langfuse.company.com"
|
||||
|
||||
litellm.callbacks = ["langfuse_otel"]
|
||||
```
|
||||
|
||||
### Manual OTEL Configuration
|
||||
|
||||
If you need direct control over the OpenTelemetry configuration:
|
||||
|
||||
```python
|
||||
import os
|
||||
import base64
|
||||
import litellm
|
||||
|
||||
# Get keys for your project from the project settings page: https://cloud.langfuse.com
|
||||
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..."
|
||||
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..."
|
||||
os.environ["LANGFUSE_HOST"] = "https://cloud.langfuse.com" # EU region
|
||||
# os.environ["LANGFUSE_HOST"] = "https://us.cloud.langfuse.com" # US region
|
||||
|
||||
LANGFUSE_AUTH = base64.b64encode(
|
||||
f"{os.environ.get('LANGFUSE_PUBLIC_KEY')}:{os.environ.get('LANGFUSE_SECRET_KEY')}".encode()
|
||||
).decode()
|
||||
|
||||
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = os.environ.get("LANGFUSE_HOST") + "/api/public/otel"
|
||||
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic {LANGFUSE_AUTH}"
|
||||
|
||||
litellm.callbacks = ["langfuse_otel"]
|
||||
```
|
||||
|
||||
### With LiteLLM Proxy
|
||||
|
||||
Add the integration to your proxy configuration:
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
litellm_settings:
|
||||
callbacks: ["langfuse_otel"]
|
||||
|
||||
environment_variables:
|
||||
LANGFUSE_PUBLIC_KEY: "pk-lf-..."
|
||||
LANGFUSE_SECRET_KEY: "sk-lf-..."
|
||||
LANGFUSE_HOST: "https://us.cloud.langfuse.com" # Default US region
|
||||
```
|
||||
|
||||
## Data Collected
|
||||
|
||||
The integration automatically collects the following data:
|
||||
|
||||
- **Request Details**: Model, messages, parameters (temperature, max_tokens, etc.)
|
||||
- **Response Details**: Generated content, token usage, finish reason
|
||||
- **Timing Information**: Request duration, time to first token
|
||||
- **Metadata**: User ID, session ID, custom tags (if provided)
|
||||
- **Error Information**: Exception details and stack traces (if errors occur)
|
||||
|
||||
## Authentication
|
||||
|
||||
The integration uses HTTP Basic Authentication with your Langfuse public and secret keys:
|
||||
|
||||
```
|
||||
Authorization: Basic <base64(public_key:secret_key)>
|
||||
```
|
||||
|
||||
This is automatically handled by the integration - you just need to provide the keys via environment variables.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Missing Credentials Error**
|
||||
```
|
||||
ValueError: LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set
|
||||
```
|
||||
**Solution**: Ensure both environment variables are set with valid keys.
|
||||
|
||||
2. **Connection Issues**
|
||||
- Check your internet connection
|
||||
- Verify the endpoint URL is correct
|
||||
- For self-hosted instances, ensure the `/api/public/otel` endpoint is accessible
|
||||
|
||||
3. **Authentication Errors**
|
||||
- Verify your public and secret keys are correct
|
||||
- Check that the keys belong to the same Langfuse project
|
||||
- Ensure the keys have the necessary permissions
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable verbose logging to see detailed information:
|
||||
|
||||
```python
|
||||
import litellm
|
||||
litellm.set_verbose = True
|
||||
```
|
||||
|
||||
This will show:
|
||||
- Endpoint resolution logic
|
||||
- Authentication header creation
|
||||
- OTEL trace submission details
|
||||
|
||||
## Related Links
|
||||
|
||||
- [Langfuse Documentation](https://langfuse.com/docs)
|
||||
- [Langfuse OpenTelemetry Guide](https://langfuse.com/docs/integrations/opentelemetry)
|
||||
- [OpenTelemetry Python SDK](https://opentelemetry.io/docs/languages/python/)
|
||||
- [LiteLLM Observability](https://docs.litellm.ai/docs/observability/)
|
||||
@@ -479,6 +479,7 @@ const sidebars = {
|
||||
items: [
|
||||
"observability/agentops_integration",
|
||||
"observability/langfuse_integration",
|
||||
"observability/langfuse_otel_integration",
|
||||
"observability/lunary_integration",
|
||||
"observability/deepeval_integration",
|
||||
"observability/mlflow",
|
||||
|
||||
@@ -109,6 +109,7 @@ _custom_logger_compatible_callbacks_literal = Literal[
|
||||
"argilla",
|
||||
"mlflow",
|
||||
"langfuse",
|
||||
"langfuse_otel",
|
||||
"pagerduty",
|
||||
"humanloop",
|
||||
"gcs_pubsub",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.integrations.arize import _utils
|
||||
from litellm.types.integrations.langfuse_otel import LangfuseOtelConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opentelemetry.trace import Span as _Span
|
||||
|
||||
from litellm.types.integrations.arize import Protocol as _Protocol
|
||||
|
||||
from litellm.integrations.opentelemetry import OpenTelemetryConfig as _OpenTelemetryConfig
|
||||
|
||||
Protocol = _Protocol
|
||||
OpenTelemetryConfig = _OpenTelemetryConfig
|
||||
Span = Union[_Span, Any]
|
||||
else:
|
||||
Protocol = Any
|
||||
OpenTelemetryConfig = Any
|
||||
Span = Any
|
||||
|
||||
|
||||
LANGFUSE_CLOUD_EU_ENDPOINT = "https://cloud.langfuse.com/api/public/otel"
|
||||
LANGFUSE_CLOUD_US_ENDPOINT = "https://us.cloud.langfuse.com/api/public/otel"
|
||||
|
||||
|
||||
class LangfuseOtelLogger:
|
||||
@staticmethod
|
||||
def set_langfuse_otel_attributes(span: Span, kwargs, response_obj):
|
||||
"""
|
||||
Sets OpenTelemetry span attributes for Langfuse observability.
|
||||
Uses the same attribute setting logic as Arize Phoenix for consistency.
|
||||
"""
|
||||
_utils.set_attributes(span, kwargs, response_obj)
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def get_langfuse_otel_config() -> LangfuseOtelConfig:
|
||||
"""
|
||||
Retrieves the Langfuse OpenTelemetry configuration based on environment variables.
|
||||
|
||||
Environment Variables:
|
||||
LANGFUSE_PUBLIC_KEY: Required. Langfuse public key for authentication.
|
||||
LANGFUSE_SECRET_KEY: Required. Langfuse secret key for authentication.
|
||||
LANGFUSE_HOST: Optional. Custom Langfuse host URL. Defaults to US cloud.
|
||||
|
||||
Returns:
|
||||
LangfuseOtelConfig: A Pydantic model containing Langfuse OTEL configuration.
|
||||
|
||||
Raises:
|
||||
ValueError: If required keys are missing.
|
||||
"""
|
||||
public_key = os.environ.get("LANGFUSE_PUBLIC_KEY", None)
|
||||
secret_key = os.environ.get("LANGFUSE_SECRET_KEY", None)
|
||||
|
||||
if not public_key or not secret_key:
|
||||
raise ValueError(
|
||||
"LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set for Langfuse OpenTelemetry integration."
|
||||
)
|
||||
|
||||
# Determine endpoint - default to US cloud
|
||||
langfuse_host = os.environ.get("LANGFUSE_HOST", None)
|
||||
|
||||
if langfuse_host:
|
||||
# If LANGFUSE_HOST is provided, construct OTEL endpoint from it
|
||||
if not langfuse_host.startswith("http"):
|
||||
langfuse_host = "https://" + langfuse_host
|
||||
endpoint = f"{langfuse_host.rstrip('/')}/api/public/otel"
|
||||
verbose_logger.debug(f"Using Langfuse OTEL endpoint from host: {endpoint}")
|
||||
else:
|
||||
# Default to US cloud endpoint
|
||||
endpoint = LANGFUSE_CLOUD_US_ENDPOINT
|
||||
verbose_logger.debug(f"Using Langfuse US cloud endpoint: {endpoint}")
|
||||
|
||||
# Create Basic Auth header
|
||||
auth_string = f"{public_key}:{secret_key}"
|
||||
auth_header = base64.b64encode(auth_string.encode()).decode()
|
||||
otlp_auth_headers = f"Authorization=Basic {auth_header}"
|
||||
|
||||
# Set standard OTEL environment variables
|
||||
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint
|
||||
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = otlp_auth_headers
|
||||
|
||||
return LangfuseOtelConfig(
|
||||
otlp_auth_headers=otlp_auth_headers,
|
||||
protocol="otlp_http"
|
||||
)
|
||||
@@ -578,6 +578,13 @@ class OpenTelemetry(CustomLogger):
|
||||
span, kwargs, response_obj
|
||||
)
|
||||
return
|
||||
elif self.callback_name == "langfuse_otel":
|
||||
from litellm.integrations.langfuse.langfuse_otel import LangfuseOtelLogger
|
||||
|
||||
LangfuseOtelLogger.set_langfuse_otel_attributes(
|
||||
span, kwargs, response_obj
|
||||
)
|
||||
return
|
||||
from litellm.proxy._types import SpanAttributes
|
||||
|
||||
optional_params = kwargs.get("optional_params", {})
|
||||
|
||||
@@ -125,6 +125,7 @@ from ..integrations.humanloop import HumanloopLogger
|
||||
from ..integrations.lago import LagoLogger
|
||||
from ..integrations.langfuse.langfuse import LangFuseLogger
|
||||
from ..integrations.langfuse.langfuse_handler import LangFuseHandler
|
||||
from ..integrations.langfuse.langfuse_otel import LangfuseOtelLogger
|
||||
from ..integrations.langfuse.langfuse_prompt_management import LangfusePromptManagement
|
||||
from ..integrations.langsmith import LangsmithLogger
|
||||
from ..integrations.literal_ai import LiteralAILogger
|
||||
@@ -3146,6 +3147,30 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915
|
||||
langfuse_logger = LangfusePromptManagement()
|
||||
_in_memory_loggers.append(langfuse_logger)
|
||||
return langfuse_logger # type: ignore
|
||||
elif logging_integration == "langfuse_otel":
|
||||
from litellm.integrations.opentelemetry import (
|
||||
OpenTelemetry,
|
||||
OpenTelemetryConfig,
|
||||
)
|
||||
|
||||
langfuse_otel_config = LangfuseOtelLogger.get_langfuse_otel_config()
|
||||
|
||||
# The endpoint and headers are now set as environment variables by get_langfuse_otel_config()
|
||||
otel_config = OpenTelemetryConfig(
|
||||
exporter=langfuse_otel_config.protocol,
|
||||
)
|
||||
|
||||
for callback in _in_memory_loggers:
|
||||
if (
|
||||
isinstance(callback, OpenTelemetry)
|
||||
and callback.callback_name == "langfuse_otel"
|
||||
):
|
||||
return callback # type: ignore
|
||||
_otel_logger = OpenTelemetry(
|
||||
config=otel_config, callback_name="langfuse_otel"
|
||||
)
|
||||
_in_memory_loggers.append(_otel_logger)
|
||||
return _otel_logger # type: ignore
|
||||
elif logging_integration == "pagerduty":
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, PagerDutyAlerting):
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
Protocol = Literal["otlp_grpc", "otlp_http"]
|
||||
else:
|
||||
Protocol = Any
|
||||
|
||||
|
||||
class LangfuseOtelConfig(BaseModel):
|
||||
otlp_auth_headers: Optional[str] = None
|
||||
protocol: Protocol = "otlp_http"
|
||||
@@ -0,0 +1,84 @@
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from litellm.integrations.langfuse.langfuse_otel import LangfuseOtelLogger
|
||||
from litellm.types.integrations.langfuse_otel import LangfuseOtelConfig
|
||||
|
||||
|
||||
class TestLangfuseOtelIntegration:
|
||||
|
||||
def test_get_langfuse_otel_config_with_required_env_vars(self):
|
||||
"""Test that config is created correctly with required environment variables."""
|
||||
# Clean environment of any Langfuse-related variables
|
||||
env_vars_to_clean = ['LANGFUSE_HOST', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'OTEL_EXPORTER_OTLP_HEADERS']
|
||||
with patch.dict(os.environ, {
|
||||
'LANGFUSE_PUBLIC_KEY': 'test_public_key',
|
||||
'LANGFUSE_SECRET_KEY': 'test_secret_key'
|
||||
}, clear=False):
|
||||
# Remove any existing Langfuse variables
|
||||
for var in env_vars_to_clean:
|
||||
if var in os.environ:
|
||||
del os.environ[var]
|
||||
|
||||
config = LangfuseOtelLogger.get_langfuse_otel_config()
|
||||
|
||||
assert isinstance(config, LangfuseOtelConfig)
|
||||
assert config.protocol == "otlp_http"
|
||||
assert "Authorization=Basic" in config.otlp_auth_headers
|
||||
# Check that environment variables are set correctly (US default)
|
||||
assert os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") == "https://us.cloud.langfuse.com/api/public/otel"
|
||||
assert "Authorization=Basic" in os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
||||
|
||||
def test_get_langfuse_otel_config_missing_keys(self):
|
||||
"""Test that ValueError is raised when required keys are missing."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(ValueError, match="LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set"):
|
||||
LangfuseOtelLogger.get_langfuse_otel_config()
|
||||
|
||||
def test_get_langfuse_otel_config_with_eu_host(self):
|
||||
"""Test config with EU host."""
|
||||
with patch.dict(os.environ, {
|
||||
'LANGFUSE_PUBLIC_KEY': 'test_public_key',
|
||||
'LANGFUSE_SECRET_KEY': 'test_secret_key',
|
||||
'LANGFUSE_HOST': 'https://cloud.langfuse.com'
|
||||
}, clear=False):
|
||||
config = LangfuseOtelLogger.get_langfuse_otel_config()
|
||||
|
||||
assert os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") == "https://cloud.langfuse.com/api/public/otel"
|
||||
|
||||
def test_get_langfuse_otel_config_with_custom_host(self):
|
||||
"""Test config with custom host."""
|
||||
with patch.dict(os.environ, {
|
||||
'LANGFUSE_PUBLIC_KEY': 'test_public_key',
|
||||
'LANGFUSE_SECRET_KEY': 'test_secret_key',
|
||||
'LANGFUSE_HOST': 'https://my-langfuse.com'
|
||||
}, clear=False):
|
||||
config = LangfuseOtelLogger.get_langfuse_otel_config()
|
||||
|
||||
assert os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") == "https://my-langfuse.com/api/public/otel"
|
||||
|
||||
def test_get_langfuse_otel_config_with_host_no_protocol(self):
|
||||
"""Test config with custom host without protocol."""
|
||||
with patch.dict(os.environ, {
|
||||
'LANGFUSE_PUBLIC_KEY': 'test_public_key',
|
||||
'LANGFUSE_SECRET_KEY': 'test_secret_key',
|
||||
'LANGFUSE_HOST': 'my-langfuse.com'
|
||||
}, clear=False):
|
||||
config = LangfuseOtelLogger.get_langfuse_otel_config()
|
||||
|
||||
assert os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") == "https://my-langfuse.com/api/public/otel"
|
||||
|
||||
def test_set_langfuse_otel_attributes(self):
|
||||
"""Test that set_langfuse_otel_attributes calls the Arize utils function."""
|
||||
mock_span = MagicMock()
|
||||
mock_kwargs = {"test": "kwargs"}
|
||||
mock_response = {"test": "response"}
|
||||
|
||||
with patch('litellm.integrations.arize._utils.set_attributes') as mock_set_attributes:
|
||||
LangfuseOtelLogger.set_langfuse_otel_attributes(mock_span, mock_kwargs, mock_response)
|
||||
|
||||
mock_set_attributes.assert_called_once_with(mock_span, mock_kwargs, mock_response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
Reference in New Issue
Block a user