190 lines
7.6 KiB
Python
190 lines
7.6 KiB
Python
import base64
|
|
import os
|
|
import json # <--- NEW
|
|
from typing import TYPE_CHECKING, Any, Union
|
|
from urllib.parse import quote
|
|
|
|
from litellm._logging import verbose_logger
|
|
from litellm.integrations.arize import _utils
|
|
from litellm.types.integrations.langfuse_otel import (
|
|
LangfuseOtelConfig,
|
|
LangfuseSpanAttributes,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from opentelemetry.trace import Span as _Span
|
|
|
|
from litellm.integrations.opentelemetry import (
|
|
OpenTelemetryConfig as _OpenTelemetryConfig,
|
|
)
|
|
from litellm.types.integrations.arize import Protocol as _Protocol
|
|
|
|
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)
|
|
|
|
#########################################################
|
|
# Set Langfuse specific attributes eg Langfuse Environment
|
|
#########################################################
|
|
LangfuseOtelLogger._set_langfuse_specific_attributes(
|
|
span=span,
|
|
kwargs=kwargs
|
|
)
|
|
return
|
|
|
|
@staticmethod
|
|
def _extract_langfuse_metadata(kwargs: dict) -> dict:
|
|
"""
|
|
Extracts Langfuse metadata from the standard LiteLLM kwargs structure.
|
|
|
|
1. Reads kwargs["litellm_params"]["metadata"] if present and is a dict.
|
|
2. Enriches it with any `langfuse_*` request-header params via the
|
|
existing LangFuseLogger.add_metadata_from_header helper so that proxy
|
|
users get identical behaviour across vanilla and OTEL integrations.
|
|
"""
|
|
litellm_params = kwargs.get("litellm_params", {}) or {}
|
|
metadata = litellm_params.get("metadata") or {}
|
|
# Ensure we only work with dicts
|
|
if metadata is None or not isinstance(metadata, dict):
|
|
metadata = {}
|
|
|
|
# Re-use header extraction logic from the vanilla logger if available
|
|
try:
|
|
from litellm.integrations.langfuse.langfuse import (
|
|
LangFuseLogger as _LFLogger,
|
|
)
|
|
|
|
metadata = _LFLogger.add_metadata_from_header(litellm_params, metadata) # type: ignore
|
|
except Exception:
|
|
# Fallback silently if import fails; header enrichment just won't happen
|
|
pass
|
|
|
|
return metadata
|
|
|
|
@staticmethod
|
|
def _set_langfuse_specific_attributes(span: Span, kwargs):
|
|
"""
|
|
Sets Langfuse specific metadata attributes onto the OTEL span.
|
|
|
|
All keys supported by the vanilla Langfuse integration are mapped to
|
|
OTEL-safe attribute names defined in LangfuseSpanAttributes. Complex
|
|
values (lists/dicts) are serialised to JSON strings for OTEL
|
|
compatibility.
|
|
"""
|
|
from litellm.integrations.arize._utils import safe_set_attribute
|
|
|
|
# 1) Environment variable override
|
|
langfuse_environment = os.environ.get("LANGFUSE_TRACING_ENVIRONMENT")
|
|
if langfuse_environment:
|
|
safe_set_attribute(
|
|
span,
|
|
LangfuseSpanAttributes.LANGFUSE_ENVIRONMENT.value,
|
|
langfuse_environment,
|
|
)
|
|
|
|
# 2) Dynamic metadata from kwargs / headers
|
|
metadata = LangfuseOtelLogger._extract_langfuse_metadata(kwargs)
|
|
|
|
# Mapping from metadata key -> OTEL attribute enum
|
|
mapping = {
|
|
"generation_name": LangfuseSpanAttributes.GENERATION_NAME,
|
|
"generation_id": LangfuseSpanAttributes.GENERATION_ID,
|
|
"parent_observation_id": LangfuseSpanAttributes.PARENT_OBSERVATION_ID,
|
|
"version": LangfuseSpanAttributes.GENERATION_VERSION,
|
|
"mask_input": LangfuseSpanAttributes.MASK_INPUT,
|
|
"mask_output": LangfuseSpanAttributes.MASK_OUTPUT,
|
|
"trace_user_id": LangfuseSpanAttributes.TRACE_USER_ID,
|
|
"session_id": LangfuseSpanAttributes.SESSION_ID,
|
|
"tags": LangfuseSpanAttributes.TAGS,
|
|
"trace_name": LangfuseSpanAttributes.TRACE_NAME,
|
|
"trace_id": LangfuseSpanAttributes.TRACE_ID,
|
|
"trace_metadata": LangfuseSpanAttributes.TRACE_METADATA,
|
|
"trace_version": LangfuseSpanAttributes.TRACE_VERSION,
|
|
"trace_release": LangfuseSpanAttributes.TRACE_RELEASE,
|
|
"existing_trace_id": LangfuseSpanAttributes.EXISTING_TRACE_ID,
|
|
"update_trace_keys": LangfuseSpanAttributes.UPDATE_TRACE_KEYS,
|
|
"debug_langfuse": LangfuseSpanAttributes.DEBUG_LANGFUSE,
|
|
}
|
|
|
|
for key, enum_attr in mapping.items():
|
|
if key in metadata and metadata[key] is not None:
|
|
value = metadata[key]
|
|
# Lists / dicts must be stringified for OTEL
|
|
if isinstance(value, (list, dict)):
|
|
try:
|
|
value = json.dumps(value)
|
|
except Exception:
|
|
value = str(value)
|
|
safe_set_attribute(span, enum_attr.value, value)
|
|
|
|
@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()
|
|
# URL encode the entire header value as required by OpenTelemetry specification
|
|
otlp_auth_headers = f"Authorization={quote(f'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"
|
|
)
|