fix(langfuse): send v4 ingestion header for otel callback

This commit is contained in:
Hassieb Pakzad 2026-04-20 14:38:46 +02:00
parent a67b7a7e87
commit ef27e618ad
2 changed files with 66 additions and 4 deletions

View file

@ -25,6 +25,8 @@ else:
LANGFUSE_CLOUD_EU_ENDPOINT = "https://cloud.langfuse.com/api/public/otel"
LANGFUSE_CLOUD_US_ENDPOINT = "https://us.cloud.langfuse.com/api/public/otel"
LANGFUSE_OTEL_INGESTION_VERSION_HEADER = "x-langfuse-ingestion-version"
LANGFUSE_OTEL_INGESTION_VERSION = "4"
class LangfuseOtelLogger(OpenTelemetry):
@ -295,7 +297,9 @@ class LangfuseOtelLogger(OpenTelemetry):
auth_header = LangfuseOtelLogger._get_langfuse_authorization_header(
public_key=public_key, secret_key=secret_key
)
otlp_auth_headers = f"Authorization={auth_header}"
otlp_auth_headers = LangfuseOtelLogger._format_otel_headers(
LangfuseOtelLogger._build_langfuse_otel_headers(auth_header)
)
return OpenTelemetryConfig(
exporter="otlp_http",
@ -344,7 +348,9 @@ class LangfuseOtelLogger(OpenTelemetry):
auth_header = LangfuseOtelLogger._get_langfuse_authorization_header(
public_key=public_key, secret_key=secret_key
)
otlp_auth_headers = f"Authorization={auth_header}"
otlp_auth_headers = LangfuseOtelLogger._format_otel_headers(
LangfuseOtelLogger._build_langfuse_otel_headers(auth_header)
)
# Prevent modification of global env vars which causes leakage
# os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint
@ -365,6 +371,17 @@ class LangfuseOtelLogger(OpenTelemetry):
auth_header = base64.b64encode(auth_string.encode()).decode()
return f"Basic {auth_header}"
@staticmethod
def _build_langfuse_otel_headers(auth_header: str) -> dict[str, str]:
return {
"Authorization": auth_header,
LANGFUSE_OTEL_INGESTION_VERSION_HEADER: LANGFUSE_OTEL_INGESTION_VERSION,
}
@staticmethod
def _format_otel_headers(headers: dict[str, str]) -> str:
return ",".join(f"{key}={value}" for key, value in headers.items())
def construct_dynamic_otel_headers(
self, standard_callback_dynamic_params: StandardCallbackDynamicParams
) -> Optional[dict]:
@ -389,7 +406,9 @@ class LangfuseOtelLogger(OpenTelemetry):
public_key=dynamic_langfuse_public_key,
secret_key=dynamic_langfuse_secret_key,
)
dynamic_headers["Authorization"] = auth_header
dynamic_headers = LangfuseOtelLogger._build_langfuse_otel_headers(
auth_header
)
return dynamic_headers

View file

@ -8,10 +8,15 @@ from unittest.mock import patch, AsyncMock, MagicMock
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
import litellm
from litellm.integrations.langfuse.langfuse_otel import LangfuseOtelLogger
from litellm.integrations.langfuse.langfuse_otel import (
LANGFUSE_OTEL_INGESTION_VERSION,
LANGFUSE_OTEL_INGESTION_VERSION_HEADER,
LangfuseOtelLogger,
)
from litellm.integrations.opentelemetry import OpenTelemetry
from litellm.types.services import ServiceTypes
from litellm._service_logger import ServiceLogging
from litellm.types.utils import StandardCallbackDynamicParams
class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase):
@ -108,6 +113,44 @@ class TestServiceLoggerOTEL(unittest.IsolatedAsyncioTestCase):
"Generic OTEL logger should have received the log exactly once.",
)
@patch("litellm.integrations.opentelemetry.OpenTelemetry._init_tracing")
@patch("litellm.integrations.opentelemetry.OpenTelemetry._init_metrics")
@patch("litellm.integrations.opentelemetry.OpenTelemetry._init_logs")
async def test_langfuse_otel_env_config_includes_v4_ingestion_header(
self, mock_logs, mock_metrics, mock_tracing
):
logger = LangfuseOtelLogger()
headers = OpenTelemetry._get_headers_dictionary(logger.config.headers)
self.assertEqual(
headers[LANGFUSE_OTEL_INGESTION_VERSION_HEADER],
LANGFUSE_OTEL_INGESTION_VERSION,
)
self.assertTrue(headers["Authorization"].startswith("Basic "))
@patch("litellm.integrations.opentelemetry.OpenTelemetry._init_tracing")
@patch("litellm.integrations.opentelemetry.OpenTelemetry._init_metrics")
@patch("litellm.integrations.opentelemetry.OpenTelemetry._init_logs")
async def test_langfuse_otel_dynamic_headers_include_v4_ingestion_header(
self, mock_logs, mock_metrics, mock_tracing
):
logger = LangfuseOtelLogger()
headers = logger.construct_dynamic_otel_headers(
StandardCallbackDynamicParams(
langfuse_public_key="pk-lf-dynamic",
langfuse_secret_key="sk-lf-dynamic",
)
)
self.assertIsNotNone(headers)
self.assertEqual(
headers[LANGFUSE_OTEL_INGESTION_VERSION_HEADER],
LANGFUSE_OTEL_INGESTION_VERSION,
)
self.assertTrue(headers["Authorization"].startswith("Basic "))
if __name__ == "__main__":
unittest.main()