Repository navigation
fix(tracing): don't export provider gen_ai spans nested in a Langfuse generation - #2019
Open
zikrulihsan wants to merge 1 commit into
Open
zikrulihsan wants to merge 1 commit into
zikrulihsan wants to merge 1 commit into
Conversation
… generation Provider SDKs such as anthropic>=1.14 now emit their own gen_ai.* spans when a global tracer provider exists. With the LangChain CallbackHandler, that span is a direct child of the Langfuse generation for the same call, so every LLM call was exported twice. With the default export filter, drop third-party gen_ai.* spans whose direct parent is an open Langfuse generation. A custom should_export_span keeps the previous behavior. Fixes langfuse/langfuse#18978
|
Verified this against a repro I'd built earlier (anthropic 1.15 with a mock transport and an in-memory exporter): the double-export reproduces on main, and with this change the nested span drops while a root-level provider span still exports — that second part is the case worth protecting, since setups using anthropic's native OTel as their only instrumentation rely on that span as their only generation record. One edge case worth considering: a provider span nested under a non-generation Langfuse span (agent/span wrappers) would still double-log — might be fine to leave, just flagging it. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes langfuse/langfuse#18978
Since anthropic 1.14, the Anthropic SDK emits its own
anthropic.messages.createspan when a global tracer provider exists. With the LangChainCallbackHandler, this span is a direct child of theChatAnthropicgeneration, and the default filter exports it because it hasgen_ai.*attributes. So every Claude call shows up as two generations, and token/cost dashboards show about 2x.With this change, when the default export filter is used,
LangfuseSpanProcessordrops third-partygen_ai.*spans whose direct parent is an open Langfuse generation. The parent's type is checked when the child ends, becauselangfuse.observation.typeis set after the span starts.See the image below:
Notes:
should_export_spanis set, nothing changes. Users who want both spans can passshould_export_span=is_default_export_span.gen_ai.*span directly under a Langfuse generation, not only Anthropic. If someone relies on an instrumentor child span for usage/cost under a manual generation, they would lose it. They can keep the old behavior withshould_export_span=is_default_export_span. I can narrow it (e.g. only the Anthropic scope) if you prefer.ANTHROPIC_OPEN_TELEMETRY=false.invokeandstream, notainvoke/astream. In async runs, LangChain calls the syncCallbackHandlerin an executor with a copied context, so the generation is not the OTel parent of the provider call, and the Anthropic span becomes its own trace. Settingrun_inline = Trueon the handler fixes this in my local test, but it was added in fix(langchain): maintain async context in tool executions #1380 and removed in feat!: prepare v4 release #1486, so I left it out of this PR.cc @hassiebp since you're assigned to the issue and own most of
span_processor.py. Happy to change the approach if you prefer another one.Type of change
Verification
List the main commands you ran:
New tests in
tests/unit/test_generation_child_span_filter.py. The two "drops" tests fail without the fix.tests/unit: all pass except 18 errors intests/unit/test_prompt.py("Langfuse client is not initialized"), which also happen onmainwithout this change in my local setup.End to end with anthropic 1.15.0 + langchain-anthropic 1.7.4 against a local fake Anthropic API and OTLP endpoint:
invokestreamainvoke/astreamA direct
anthropic.Anthropic().messages.create(...)call is still exported.Same runs against a local self-hosted Langfuse server (v4.59.0, official
docker-compose.yml), counting storedGENERATIONobservations through/api/public/v2/observations:invokeandstreamwent from 2 generations / 230 total tokens to 1 generation / 115 tokens.ainvokeandastreamstay at 2 generations in 2 separate traces.Checklist
code_review.md..env.templateif needed.

Confidence Score: 5/5
The PR appears safe to merge within its stated scope.Summary
The default filter now drops third-party
gen_ai.*spans directly beneath an open Langfuse generation. It reads the parent's current observation type when the child ends.Acknowledged scope from zikrulihsan: suppression intentionally covers all providers, including calls under manual generations. Async LangChain calls remain outside this fix because their provider spans do not share the generation parent.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Provider span ends] --> B{Default filter and gen_ai attributes?} B -->|No| E[Apply configured export filter] B -->|Yes| C{Direct parent is an open Langfuse generation?} C -->|No| E C -->|Yes| D[Drop duplicate provider span]Reviews (1) · Last reviewed commit: "fix(tracing): don't export provider gen_..." · Reviewed by Greptile