-
Notifications
You must be signed in to change notification settings - Fork 1k
fix(langchain): fix nesting of langgraph spans #3206
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
Changes from 9 commits
3eb14c5
40fb725
5ff9bbf
15476d1
644435f
a5296a4
bb72946
2136589
293b501
5bd2330
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,189 @@ | ||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| Reproduce the exact GitHub issue #3203 and show the trace waterfall. | ||||||||||||||||||||||||||||||||||||||||
| This demonstrates the span hierarchy issue before and after the fix. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||
| from typing import TypedDict | ||||||||||||||||||||||||||||||||||||||||
| import httpx | ||||||||||||||||||||||||||||||||||||||||
| from langgraph.graph import END, START, StateGraph | ||||||||||||||||||||||||||||||||||||||||
| from opentelemetry import trace | ||||||||||||||||||||||||||||||||||||||||
| from opentelemetry.instrumentation.langchain import LangchainInstrumentor | ||||||||||||||||||||||||||||||||||||||||
| from opentelemetry.sdk.trace import TracerProvider | ||||||||||||||||||||||||||||||||||||||||
| from opentelemetry.sdk.trace.export import SimpleSpanProcessor | ||||||||||||||||||||||||||||||||||||||||
| from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| from waterfall_visualizer import visualize_trace_waterfall, print_raw_span_data | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def setup_tracing(): | ||||||||||||||||||||||||||||||||||||||||
| """Set up OpenTelemetry tracing exactly like in the test environment.""" | ||||||||||||||||||||||||||||||||||||||||
| span_exporter = InMemorySpanExporter() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| tracer_provider = TracerProvider() | ||||||||||||||||||||||||||||||||||||||||
| tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| trace.set_tracer_provider(tracer_provider) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| langchain_instrumentor = LangchainInstrumentor() | ||||||||||||||||||||||||||||||||||||||||
| langchain_instrumentor.instrument(tracer_provider=tracer_provider) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return span_exporter, langchain_instrumentor | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| async def run_github_issue_reproduction(): | ||||||||||||||||||||||||||||||||||||||||
| """Run the exact code from GitHub issue #3203.""" | ||||||||||||||||||||||||||||||||||||||||
| print("🚀 Running GitHub Issue #3203 Reproduction") | ||||||||||||||||||||||||||||||||||||||||
| print("=" * 60) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| span_exporter, langchain_instrumentor = setup_tracing() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| tracer = trace.get_tracer(__name__) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class TestAgentState(TypedDict): | ||||||||||||||||||||||||||||||||||||||||
| http_result: str | ||||||||||||||||||||||||||||||||||||||||
| span_result: str | ||||||||||||||||||||||||||||||||||||||||
| messages: list | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| async def http_call_node(state: TestAgentState) -> dict: | ||||||||||||||||||||||||||||||||||||||||
| """HTTP call node from the GitHub issue.""" | ||||||||||||||||||||||||||||||||||||||||
| print("📞 Executing http_call_node...") | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| data = {"a": 10, "b": 25} | ||||||||||||||||||||||||||||||||||||||||
| async with httpx.AsyncClient() as _: | ||||||||||||||||||||||||||||||||||||||||
| with tracer.start_as_current_span("POST") as span: | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("http.method", "POST") | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("http.url", "https://httpbin.org/post") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| sum_result = data.get("a", 0) + data.get("b", 0) | ||||||||||||||||||||||||||||||||||||||||
| http_result = f"HTTP call successful! Sum of {data.get('a')} + {data.get('b')} = {sum_result}" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("http.response.status_code", 200) | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("calculation.result", sum_result) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| print(f" ✅ {http_result}") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||
| http_result = f"HTTP call error: {str(e)}" | ||||||||||||||||||||||||||||||||||||||||
| print(f" ❌ {http_result}") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return {"http_result": http_result} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| async def opentelemetry_span_node(state: TestAgentState) -> dict: | ||||||||||||||||||||||||||||||||||||||||
| """OpenTelemetry span node from the GitHub issue.""" | ||||||||||||||||||||||||||||||||||||||||
| print("📊 Executing otel_span_node...") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| with tracer.start_as_current_span("test_agent_span") as span: | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("node.name", "opentelemetry_span_node") | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("agent.type", "test_agent") | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("operation.type", "span_creation") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| span.add_event("Starting span processing") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| await asyncio.sleep(0.01) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| http_result = state.get("http_result", "No HTTP result available") | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("previous.http_result", http_result) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| span.add_event("Processing HTTP result from previous node") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| span_result = f"OpenTelemetry span created successfully! Span ID: {span.get_span_context().span_id}" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| span.add_event("Span processing completed") | ||||||||||||||||||||||||||||||||||||||||
| span.set_attribute("processing.status", "completed") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| print(f" ✅ {span_result}") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return {"span_result": span_result} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def create_test_agent(): | ||||||||||||||||||||||||||||||||||||||||
| """Create a simple LangGraph agent with 2 nodes matching the GitHub issue exactly.""" | ||||||||||||||||||||||||||||||||||||||||
| print("🔧 Creating LangGraph agent...") | ||||||||||||||||||||||||||||||||||||||||
| builder = StateGraph(TestAgentState) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| builder.add_node("http_call", http_call_node) | ||||||||||||||||||||||||||||||||||||||||
| builder.add_node("otel_span", opentelemetry_span_node) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| builder.add_edge(START, "http_call") | ||||||||||||||||||||||||||||||||||||||||
| builder.add_edge("http_call", "otel_span") | ||||||||||||||||||||||||||||||||||||||||
| builder.add_edge("otel_span", END) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| agent = builder.compile() | ||||||||||||||||||||||||||||||||||||||||
| print(" ✅ Agent created successfully!") | ||||||||||||||||||||||||||||||||||||||||
| return agent | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| async def run_test_agent(): | ||||||||||||||||||||||||||||||||||||||||
| """Run the test agent with root span tracking.""" | ||||||||||||||||||||||||||||||||||||||||
| with tracer.start_as_current_span("test_agent_execution_root") as root_span: | ||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("agent.name", "test_agent") | ||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("agent.version", "1.0.0") | ||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("execution.type", "full_agent_run") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| root_span.add_event("Agent execution started") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| root_span.add_event("Creating agent graph") | ||||||||||||||||||||||||||||||||||||||||
| agent = create_test_agent() | ||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("agent.nodes_count", 2) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| initial_state = {"http_result": "", "span_result": "", "messages": []} | ||||||||||||||||||||||||||||||||||||||||
| root_span.add_event("Initial state prepared") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| print("🏃 Starting agent invocation...") | ||||||||||||||||||||||||||||||||||||||||
| root_span.add_event("Starting agent invocation") | ||||||||||||||||||||||||||||||||||||||||
| final_state = await agent.ainvoke(initial_state) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("execution.status", "completed") | ||||||||||||||||||||||||||||||||||||||||
| print("✅ Agent execution completed successfully!") | ||||||||||||||||||||||||||||||||||||||||
| return final_state | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("execution.status", "failed") | ||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("error.type", type(e).__name__) | ||||||||||||||||||||||||||||||||||||||||
| root_span.set_attribute("error.message", str(e)) | ||||||||||||||||||||||||||||||||||||||||
| root_span.add_event("Agent execution failed", {"error": str(e)}) | ||||||||||||||||||||||||||||||||||||||||
| print(f"❌ Agent execution failed: {e}") | ||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| final_state = await run_test_agent() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| spans = span_exporter.get_finished_spans() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| print("\n📊 EXECUTION RESULTS:") | ||||||||||||||||||||||||||||||||||||||||
| print(f" • HTTP Result: {final_state.get('http_result', 'N/A')}") | ||||||||||||||||||||||||||||||||||||||||
| print(f" • Span Result: {final_state.get('span_result', 'N/A')}") | ||||||||||||||||||||||||||||||||||||||||
| print(f" • Total Spans Captured: {len(spans)}") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| visualize_trace_waterfall(spans) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if "--debug" in sys.argv: | ||||||||||||||||||||||||||||||||||||||||
| print_raw_span_data(spans) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return spans | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||
| langchain_instrumentor.uninstrument() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||||||||||||||||||||||||||
| print("🔍 GitHub Issue #3203 - LangGraph Span Hierarchy Reproduction") | ||||||||||||||||||||||||||||||||||||||||
| print("This script demonstrates the exact issue described in the GitHub issue.") | ||||||||||||||||||||||||||||||||||||||||
| print("Run with --debug to see raw span data.\n") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| spans = asyncio.run(run_github_issue_reproduction()) | ||||||||||||||||||||||||||||||||||||||||
| print(f"\n✅ Demo completed successfully! Captured {len(spans)} spans.") | ||||||||||||||||||||||||||||||||||||||||
| print("\n🎉 GitHub Issue #3203 has been FIXED!") | ||||||||||||||||||||||||||||||||||||||||
| print("The visualization above shows the CORRECTED span hierarchy.") | ||||||||||||||||||||||||||||||||||||||||
| print("Note how POST and test_agent_span are now properly nested") | ||||||||||||||||||||||||||||||||||||||||
| print("under their respective task spans (http_call.task and otel_span.task)!") | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+179
to
+182
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Misleading success message before fix verification The script prints "GitHub Issue #3203 has been FIXED!" and describes the corrected hierarchy without actually verifying whether the fix is in place. This could be confusing if someone runs this script on a version without the fix. Consider dynamically checking the span hierarchy to determine if the issue is fixed: - print("\n🎉 GitHub Issue #3203 has been FIXED!")
- print("The visualization above shows the CORRECTED span hierarchy.")
- print("Note how POST and test_agent_span are now properly nested")
- print("under their respective task spans (http_call.task and otel_span.task)!")
+ # Verify if the hierarchy is correct
+ post_span = next((s for s in spans if s.name == "POST"), None)
+ http_call_task = next((s for s in spans if s.name == "http_call.task"), None)
+
+ if post_span and http_call_task and post_span.parent:
+ if post_span.parent.span_id == http_call_task.context.span_id:
+ print("\n🎉 GitHub Issue #3203 has been FIXED!")
+ print("The visualization above shows the CORRECTED span hierarchy.")
+ print("Note how POST and test_agent_span are now properly nested")
+ print("under their respective task spans (http_call.task and otel_span.task)!")
+ else:
+ print("\n⚠️ GitHub Issue #3203 is still present!")
+ print("The spans are not properly nested under their task spans.")
+ else:
+ print("\n⚠️ Unable to verify span hierarchy - expected spans not found")📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| except KeyboardInterrupt: | ||||||||||||||||||||||||||||||||||||||||
| print("\n❌ Demo interrupted by user") | ||||||||||||||||||||||||||||||||||||||||
| sys.exit(1) | ||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||
| print(f"\n❌ Demo failed: {e}") | ||||||||||||||||||||||||||||||||||||||||
| sys.exit(1) | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Missing import for waterfall_visualizer module
The script imports
waterfall_visualizer(line 18) but this module doesn't appear to be part of the package. This will cause an ImportError when running the script.🏁 Script executed:
Length of output: 580
Add the missing
waterfall_visualizermodule or correct the importThe script at
packages/opentelemetry-instrumentation-langchain/langgraph_with_otel_sdk.py(line 18) importswaterfall_visualizer, but no such file exists in the repository. This will raise anImportErrorat runtime.To resolve:
waterfall_visualizer.pyin the package (e.g., underpackages/opentelemetry-instrumentation-langchain/), definingvisualize_trace_waterfallandprint_raw_span_data.setup.pyorpyproject.toml.🤖 Prompt for AI Agents