diff --git a/docs/my-website/docs/observability/langfuse_otel_integration.md b/docs/my-website/docs/observability/langfuse_otel_integration.md index c45c33f0f25..4801fa8e1b0 100644 --- a/docs/my-website/docs/observability/langfuse_otel_integration.md +++ b/docs/my-website/docs/observability/langfuse_otel_integration.md @@ -24,7 +24,7 @@ The Langfuse OpenTelemetry integration allows you to send LiteLLM traces and obs 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 + pip install litellm opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp ``` ## Configuration @@ -147,6 +147,41 @@ The integration automatically collects the following data: - **Metadata**: User ID, session ID, custom tags (if provided) - **Error Information**: Exception details and stack traces (if errors occur) +## Metadata Support + +All metadata fields available in the vanilla Langfuse integration are now **fully supported** when you use the OTEL integration. + +- Any key you pass in the `metadata` dictionary (`generation_name`, `trace_id`, `session_id`, `tags`, and the rest) is exported as an OpenTelemetry span attribute. +- Attribute names are prefixed with `langfuse.` so you can filter or search for them easily in your observability backend. + Examples: `langfuse.generation.name`, `langfuse.trace.id`, `langfuse.trace.session_id`. + +### Passing Metadata – Example + +```python +response = litellm.completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Hello!"}], + metadata={ + "generation_name": "welcome-message", + "trace_id": "trace-123", + "session_id": "sess-42", + "tags": ["prod", "beta-user"] + } +) +``` + +The resulting span will contain attributes similar to: + +``` +langfuse.generation.name = "welcome-message" +langfuse.trace.id = "trace-123" +langfuse.trace.session_id = "sess-42" +langfuse.trace.tags = ["prod", "beta-user"] +``` + +Use the **Langfuse UI** (Traces tab) to search, filter and analyse spans that contain the `langfuse.*` attributes. +The OTEL exporter in this integration sends data directly to Langfuse’s OTLP HTTP endpoint; it is **not** intended for Grafana, Honeycomb, Datadog, or other generic OTEL back-ends. + ## Authentication The integration uses HTTP Basic Authentication with your Langfuse public and secret keys: diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index f257da6d95e..9617db77462 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -1,5 +1,6 @@ import base64 import os +import json # <--- NEW from typing import TYPE_CHECKING, Any, Union from urllib.parse import quote @@ -49,21 +50,93 @@ class LangfuseOtelLogger: 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 attributes to the span. + 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 - langfuse_environment = os.environ.get("LANGFUSE_TRACING_ENVIRONMENT", None) + from litellm.types.integrations.langfuse_otel import LangfuseSpanAttributes + + # 1) Environment variable override + langfuse_environment = os.environ.get("LANGFUSE_TRACING_ENVIRONMENT") if langfuse_environment: safe_set_attribute( - span=span, - key=LangfuseSpanAttributes.LANGFUSE_ENVIRONMENT.value, - value=langfuse_environment + 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: """ diff --git a/litellm/types/integrations/langfuse_otel.py b/litellm/types/integrations/langfuse_otel.py index d4b61304546..37eac1f54a9 100644 --- a/litellm/types/integrations/langfuse_otel.py +++ b/litellm/types/integrations/langfuse_otel.py @@ -13,5 +13,28 @@ class LangfuseOtelConfig(BaseModel): otlp_auth_headers: Optional[str] = None protocol: Protocol = "otlp_http" -class LangfuseSpanAttributes(Enum): - LANGFUSE_ENVIRONMENT = "langfuse.environment" \ No newline at end of file +class LangfuseSpanAttributes(str, Enum): + LANGFUSE_ENVIRONMENT = "langfuse.environment" + + # ---- Generation-level metadata ---- + GENERATION_NAME = "langfuse.generation.name" + GENERATION_ID = "langfuse.generation.id" + PARENT_OBSERVATION_ID = "langfuse.generation.parent_observation_id" + GENERATION_VERSION = "langfuse.generation.version" + MASK_INPUT = "langfuse.generation.mask_input" + MASK_OUTPUT = "langfuse.generation.mask_output" + + # ---- Trace-level metadata ---- + TRACE_USER_ID = "langfuse.trace.user_id" + SESSION_ID = "langfuse.trace.session_id" + TAGS = "langfuse.trace.tags" + TRACE_NAME = "langfuse.trace.name" + TRACE_ID = "langfuse.trace.id" + TRACE_METADATA = "langfuse.trace.metadata" + TRACE_VERSION = "langfuse.trace.version" + TRACE_RELEASE = "langfuse.trace.release" + EXISTING_TRACE_ID = "langfuse.trace.existing_id" + UPDATE_TRACE_KEYS = "langfuse.trace.update_keys" + + # ---- Misc / flags ---- + DEBUG_LANGFUSE = "langfuse.debug" \ No newline at end of file