-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Troubleshooting trace nesting (#426)
- Loading branch information
Showing
1 changed file
with
190 additions
and
0 deletions.
There are no files selected for viewing
190 changes: 190 additions & 0 deletions
190
versioned_docs/version-2.0/how_to_guides/tracing/nest_traces.mdx
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
--- | ||
sidebar_position: 21 | ||
--- | ||
|
||
import { | ||
CodeTabs, | ||
PythonBlock, | ||
TypeScriptBlock, | ||
} from "@site/src/components/InstructionsWithCode"; | ||
import { RegionalUrl } from "@site/src/components/RegionalUrls"; | ||
|
||
# Troubleshoot trace nesting | ||
|
||
When tracing with the LangSmith SDK, LangGraph, and LangChain, tracing should automatically propagate the correct context so that code executed within a parent trace will be rendered in the expected location in the UI. | ||
|
||
If you see a child run go to a separate trace (and appear on the top level), it may be caused by one of the following known "edge cases". | ||
|
||
## Python | ||
|
||
The following outlines common causes for "split" traces when building with python. | ||
|
||
### Context propagation using asyncio | ||
|
||
When using async calls (especially with streaming) in Python versions < 3.11, you may encounter issues with trace nesting. This is because Python's `asyncio` only [added full support for passing context](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) in version 3.11. | ||
|
||
#### Understanding the Issue | ||
|
||
LangChain and LangSmith SDK use [contextvars](https://docs.python.org/3/library/contextvars.html) to propagate tracing information implicitly. In Python 3.11 and above, this works seamlessly. However, in earlier versions (3.8, 3.9, 3.10), `asyncio` tasks lack proper `contextvar` support, which can lead to disconnected traces. | ||
|
||
#### Resolution Steps | ||
|
||
1. **Upgrade Python Version (Recommended)** | ||
If possible, upgrade to Python 3.11 or later for automatic context propagation. | ||
|
||
2. **Manual Context Propagation** | ||
If upgrading isn't an option, you'll need to manually propagate the tracing context. The method varies depending on your setup: | ||
|
||
a) **Using LangGraph or LangChain** | ||
Pass the parent `config` to the child call: | ||
|
||
```python | ||
import asyncio | ||
from langchain_core.runnables import RunnableConfig, RunnableLambda | ||
|
||
@RunnableLambda | ||
async def my_child_runnable( | ||
inputs: str, | ||
# The config arg (present in parent_runnable below) is optional | ||
): | ||
yield "A" | ||
yield "response" | ||
|
||
@RunnableLambda | ||
async def parent_runnable(inputs: str, config: RunnableConfig): | ||
# highlight-next-line | ||
async for chunk in my_child_runnable.astream(inputs, config): | ||
yield chunk | ||
|
||
async def main(): | ||
return [val async for val in parent_runnable.astream("call")] | ||
|
||
asyncio.run(main()) | ||
``` | ||
|
||
b) **Using LangSmith Directly** | ||
Pass the run tree directly: | ||
|
||
```python | ||
import asyncio | ||
import langsmith as ls | ||
|
||
@ls.traceable | ||
async def my_child_function(inputs: str): | ||
yield "A" | ||
yield "response" | ||
|
||
@ls.traceable | ||
async def parent_function( | ||
inputs: str, | ||
# The run tree can be auto-populated by the decorator | ||
run_tree: ls.RunTree, | ||
): | ||
# highlight-next-line | ||
async for chunk in my_child_function(inputs, langsmith_extra={"parent": run_tree}): | ||
yield chunk | ||
|
||
async def main(): | ||
return [val async for val in parent_function("call")] | ||
|
||
asyncio.run(main()) | ||
``` | ||
|
||
c) **Combining Decorated Code with LangGraph/LangChain** | ||
Use a combination of techniques for manual handoff: | ||
|
||
```python | ||
import asyncio | ||
import langsmith as ls | ||
from langchain_core.runnables import RunnableConfig, RunnableLambda | ||
|
||
@RunnableLambda | ||
async def my_child_runnable(inputs: str): | ||
yield "A" | ||
yield "response" | ||
|
||
@ls.traceable | ||
async def my_child_function(inputs: str, run_tree: ls.RunTree): | ||
# highlight-next-line | ||
with ls.tracing_context(parent=run_tree): | ||
async for chunk in my_child_runnable.astream(inputs): | ||
yield chunk | ||
|
||
@RunnableLambda | ||
async def parent_runnable(inputs: str, config: RunnableConfig): | ||
# @traceable decorated functions can directly accept a RunnableConfig when passed in via "config" | ||
# highlight-next-line | ||
async for chunk in my_child_function(inputs, langsmith_extra={"config": config}): | ||
yield chunk | ||
|
||
@ls.traceable | ||
async def parent_function(inputs: str, run_tree: ls.RunTree): | ||
# You can set the tracing context manually | ||
# highlight-next-line | ||
with ls.tracing_context(parent=run_tree): | ||
async for chunk in parent_runnable.astream(inputs): | ||
yield chunk | ||
|
||
async def main(): | ||
return [val async for val in parent_function("call")] | ||
|
||
asyncio.run(main()) | ||
``` | ||
|
||
### Context propagation using threading | ||
|
||
It's common to start tracing and want to apply some parallelism on child tasks all within a single trace. Python's stdlib `ThreadPoolExecutor` by default breaks tracing. | ||
|
||
#### Understanding the Issue | ||
|
||
Python's contextvars start empty within new threads. Here are two approaches to handle maintain trace contiguity: | ||
|
||
1. **Using LangSmith's ContextThreadPoolExecutor** | ||
|
||
LangSmith provides a `ContextThreadPoolExecutor` that automatically handles context propagation: | ||
|
||
```python | ||
from langsmith.utils import ContextThreadPoolExecutor | ||
from langsmith import traceable | ||
|
||
@traceable | ||
def outer_func(): | ||
with ContextThreadPoolExecutor() as executor: | ||
inputs = [1, 2] | ||
r = list(executor.map(inner_func, inputs)) | ||
|
||
@traceable | ||
def inner_func(x): | ||
print(x) | ||
|
||
outer_func() | ||
``` | ||
|
||
2. **Manually providing the parent run tree** | ||
|
||
Alternatively, you can manually pass the parent run tree to the inner function: | ||
|
||
```python | ||
from langsmith import traceable, get_current_run_tree | ||
from concurrent.futures import ThreadPoolExecutor | ||
|
||
@traceable | ||
def outer_func(): | ||
rt = get_current_run_tree() | ||
with ThreadPoolExecutor() as executor: | ||
r = list( | ||
executor.map( | ||
lambda x: inner_func(x, langsmith_extra={"parent": rt}), [1, 2] | ||
) | ||
) | ||
|
||
@traceable | ||
def inner_func(x): | ||
print(x) | ||
|
||
outer_func() | ||
``` | ||
|
||
In this approach, we use `get_current_run_tree()` to obtain the current run tree and pass it to the inner function using the `langsmith_extra` parameter. | ||
|
||
Both methods ensure that the inner function calls are correctly aggregated under the initial trace stack, even when executed in separate threads. |