From 171dc2379f95f672913382be2df0d5241da466f0 Mon Sep 17 00:00:00 2001 From: Prashant Vasudevan <71649489+vprashrex@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:15:13 +0530 Subject: [PATCH] fix(langfuse): isolate OTel tracer provider so LLM spans don't duplicate into Sentry --- backend/app/core/langfuse/langfuse.py | 23 +++++++++++++++++++++++ backend/app/utils.py | 3 +++ 2 files changed, 26 insertions(+) diff --git a/backend/app/core/langfuse/langfuse.py b/backend/app/core/langfuse/langfuse.py index 15b6ae4b0..7d43c4bac 100644 --- a/backend/app/core/langfuse/langfuse.py +++ b/backend/app/core/langfuse/langfuse.py @@ -1,5 +1,6 @@ import json import logging +import threading import uuid from collections.abc import Callable from functools import wraps @@ -9,6 +10,7 @@ from langfuse import Langfuse, LangfuseOtelSpanAttributes from langfuse._client.span import LangfuseGeneration, LangfuseSpan from langfuse.api.core.api_error import ApiError +from opentelemetry.sdk.trace import TracerProvider from app.models.llm import ( AudioOutput, @@ -20,6 +22,25 @@ logger = logging.getLogger(__name__) +_LANGFUSE_TRACER_PROVIDER: TracerProvider | None = None +_LANGFUSE_TRACER_PROVIDER_LOCK = threading.Lock() + + +def get_langfuse_tracer_provider() -> TracerProvider: + """Isolated OTel provider for Langfuse so LLM spans never reach Sentry. + + Sentry's SpanProcessor sits on the global provider and exports every span + with no per-project filter; sharing it surfaces each Langfuse span in Sentry + as a duplicate root trace. Langfuse's own processors filter by public_key, so + one shared isolated provider stays multi-tenant safe. + """ + global _LANGFUSE_TRACER_PROVIDER + if _LANGFUSE_TRACER_PROVIDER is None: + with _LANGFUSE_TRACER_PROVIDER_LOCK: + if _LANGFUSE_TRACER_PROVIDER is None: + _LANGFUSE_TRACER_PROVIDER = TracerProvider() + return _LANGFUSE_TRACER_PROVIDER + def format_langfuse_error(exc: Exception) -> str: """Return a concise message for a Langfuse SDK exception. @@ -153,6 +174,7 @@ def __init__( secret_key=credentials["secret_key"], host=credentials["host"], tracing_enabled=True, # This ensures the client is active + tracer_provider=get_langfuse_tracer_provider(), ) except Exception as e: logger.warning( @@ -311,6 +333,7 @@ def wrapper( public_key=credentials.get("public_key"), secret_key=credentials.get("secret_key"), host=credentials.get("host"), + tracer_provider=get_langfuse_tracer_provider(), ) logger.info( f"[observe_llm_execution] Tracing enabled | session_id={session_id or 'auto'}" diff --git a/backend/app/utils.py b/backend/app/utils.py index 7c9f83d1c..65c40431c 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -348,11 +348,14 @@ def get_anthropic_client(session: Session, org_id: int, project_id: int) -> Anth def _build_langfuse_client(credentials: dict[str, Any]) -> Langfuse: + from app.core.langfuse.langfuse import get_langfuse_tracer_provider + return Langfuse( public_key=credentials["public_key"], secret_key=credentials["secret_key"], host=credentials["host"], timeout=60, + tracer_provider=get_langfuse_tracer_provider(), )