Skip to content

[BUG] Missing tool attributes in LangChain instrumentation #130

@NikitaVoitov

Description

@NikitaVoitov

Summary

Tool spans (execute_tool operations) are missing several attributes defined in the Otel GenAI Semantic Conventions. This reduces observability into tool execution and breaks compatibility with GenAI semconv. Sometimes even tool name is written into model attribute.

Environment

  • Package: splunk-otel-python-contrib (LangChain instrumentation)
  • LangChain Version: 0.3.x
  • Python Version: 3.13
  • OTel SDK Version: 1.38.0

Missing Attributes

Attribute Status Expected Notes
gen_ai.tool.name ⚠️ Inconsistent Tool function name Recommended
gen_ai.tool.type ❌ Missing "function" Recommended if available
gen_ai.tool.call.id ❌ Missing call_xxx from LLM Recommended if available
gen_ai.tool.description ❌ Missing Tool docstring Recommended if available
gen_ai.tool.call.arguments ❌ Missing JSON string Opt-In
gen_ai.tool.call.result ❌ Missing Tool output Opt-In
gen_ai.provider.name ❌ Missing openai, anthropic, etc. Not inherited from parent LLM

Root Cause Analysis

1. Tool Call ID Extraction Limited

LangGraph's ToolNode passes tool_call_id in multiple locations:

  • extra.tool_call_id
  • metadata.tool_call_id
  • inputs["tool_call_id"]

The current handler only checks serialized["id"], which is the tool's module path (e.g., ["langchain", "tools", "search"]), not the actual call ID.

2. Provider Not Inherited

When an LLM triggers a tool call, the tool span should inherit the provider from the parent LLM context. Currently, this inheritance chain is broken, resulting in missing gen_ai.provider.name on tool spans.

3. Tool Attributes Not Emitted

The span emitter lacks dedicated handling for ToolCall entities, missing:

  • Tool-specific semantic convention attributes
  • Proper cleanup and span ending for tool calls

Steps to Reproduce

from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

@tool
def get_weather(city: str) -> str:
    """Get weather for a city."""
    return f"Weather in {city}: Sunny"

llm = ChatOpenAI(model="gpt-4o-mini")
agent = create_react_agent(llm, [get_weather])

# Invoke agent
result = agent.invoke({"messages": [("user", "What's the weather in Paris?")]})

Expected Behavior

Tool span should include:

{
    "name": "tool get_weather",
    "attributes": {
        "gen_ai.tool.name": "get_weather",
        "gen_ai.tool.call.id": "call_exRoiBufYlt2lSgdoF99A4Ei",
        "gen_ai.tool.type": "function",
        "gen_ai.tool.description": "Get weather for a city.",
        "gen_ai.tool.call.arguments": "{\"city\": \"Paris\"}",
        "gen_ai.tool.call.result": "Weather in Paris: Sunny",
        "gen_ai.provider.name": "openai",
        "gen_ai.operation.name": "execute_tool"
    }
}

Actual Behavior

Tool span is missing most attributes:

{
    "name": "tool get_weather",
    "attributes": {
        "gen_ai.operation.name": "execute_tool"
    }
}

Evidence

Before Fix - Trace ID: 7751b1717e9b891d42cb4b383865d0e0

📊 Tool span BEFORE fix (missing attributes)

Tool Span (tool get_weather):

{
  "spanId": "9e3f079697e5d7aa",
  "operationName": "tool get_weather",
  "tags": {
    "gen_ai.request.model": "get_weather",   // ❌ WRONG - tool name in model field
    "gen_ai.operation.name": "execute_tool",
    "gen_ai.evaluation.sampled": true
    // ❌ MISSING: gen_ai.tool.name, etc.
  }
}

After Fix - Trace ID: b0f9b91c417f4a2636b037ef88ea2adf

📊 Tool span AFTER fix (all attributes present)

Tool Span (tool get_weather):

{
  "spanId": "824033c4aaff8b8",
  "parentId": "e21fa23e16f4ebe9",
  "operationName": "tool get_weather",
  "tags": {
    "gen_ai.tool.name": "get_weather",                                             
    "gen_ai.tool.call.id": "call_exRoiBufYlt2lSgdoF99A4Ei",                        
    "gen_ai.tool.type": "function",                                                 
    "gen_ai.tool.description": "Get current weather for a city. Returns temperature and conditions.",  
    "gen_ai.operation.name": "execute_tool",
    "gen_ai.evaluation.sampled": true
  }
}

Parent Chat Span (chat gpt-4o-mini):

{
  "spanId": "e21fa23e16f4ebe9",
  "operationName": "chat gpt-4o-mini",
  "tags": {
    "gen_ai.provider.name": "openai",              //  Can be inherited by tool
    "gen_ai.request.model": "gpt-4o-mini",
    "gen_ai.response.model": "gpt-4o-mini-2024-07-18",
    "gen_ai.usage.input_tokens": 66,
    "gen_ai.usage.output_tokens": 15
  }
}

Impact

  • Observability Gap: Cannot track which tools are being called and with what arguments
  • Debugging Difficulty: Missing tool_call_id makes it hard to correlate tool execution with LLM responses
  • Provider Attribution: Tool spans don't show which LLM provider triggered them

Proposed Solution

  1. Extract tool_call_id from multiple sources (priority order):

    • extra.tool_call_id
    • metadata.tool_call_id
    • inputs["tool_call_id"]
    • serialized["id"] (fallback, only if it looks like a call ID)
  2. Inherit provider from parent LLM context using parent traversal

  3. Add dedicated tool lifecycle methods in span emitter:

    • _finish_tool_call() with all tool attributes
    • _error_tool_call() for error handling
  4. Capture tool description from serialized["description"] (LangChain provides this)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions