Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Troubleshooting trace nesting #426

Merged
merged 2 commits into from
Sep 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions versioned_docs/version-2.0/how_to_guides/tracing/nest_traces.mdx
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.
Loading