Skip to content

[bug] OpenInferenceSpanProcessor maps OpenLLMetry/LangChain tool spans to TOOL but drops tool.name and leaves the raw Traceloop input/output envelopes #3241

Description

@ambarish-ntnx

Describe the bug

OpenInferenceSpanProcessor (from openinference-instrumentation-openllmetry)
converts every non-llm OpenLLMetry/Traceloop span through a single
_map_generic_span() path that copies only traceloop.entity.input /
traceloop.entity.output verbatim. For LangChain tool spans this produces
a span that is classified TOOL (correct) but is missing the tool semantics
Phoenix needs to render it:

  • no tool.name (and no tool.parameters) attribute is set, even though the
    tool name is available to the processor (it is present in the span name and
    in traceloop.entity.output's kwargs.name);
  • input.value is the raw Traceloop envelope
    {"input_str": ..., "tags": [...], "metadata": {...}, "inputs": {<args>}, "kwargs": {...}}
    rather than the resolved tool arguments;
  • output.value is the wrapped envelope
    {"output": <result>, "kwargs": {...}} rather than the tool result.

As a result, Phoenix renders a nested, hard-to-read JSON blob for the tool's
input and output instead of clean arguments and a clean result, and the span
has no tool name.

Root cause

In _span_processor.py, OpenInferenceSpanProcessor.on_end() sends every span
whose traceloop.span.kind is not "llm" through _map_generic_span(), then
clears and replaces all attributes:

kind = attrs.get(SpanAttributes.TRACELOOP_SPAN_KIND)
if kind and kind.lower() != "llm":
    generic = _map_generic_span(attrs)
    attrs.clear()
    attrs.update(generic)
    return

_map_generic_span() emits only openinference.span.kind,
input.value/input.mime_type (from traceloop.entity.input), and
output.value/output.mime_type (from traceloop.entity.output). It never
maps the tool name to tool.name, nor unwraps the LangChain tool argument
envelope or the tool result envelope into the OpenInference tool semantics.

To Reproduce

Minimal, self-contained script (LangChain tool + OpenLLMetry instrumentor +
the OpenInference bridge, exported to a Phoenix collector):

import os
import time

from openinference.instrumentation.openllmetry import OpenInferenceSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
from opentelemetry.sdk import trace as trace_sdk
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from langchain_core.tools import tool

ENDPOINT = os.getenv("PHOENIX_OTLP_ENDPOINT", "http://localhost:6006/v1/traces")

tracer_provider = trace_sdk.TracerProvider()
tracer_provider.add_span_processor(OpenInferenceSpanProcessor())
tracer_provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint=ENDPOINT))
)
LangchainInstrumentor().instrument(tracer_provider=tracer_provider)


@tool
def get_weather(city: str) -> str:
    """Return the current weather for a city as a JSON string."""
    return '{"city": "%s", "temp_c": 21, "condition": "sunny"}' % city


get_weather.invoke({"city": "Paris"})
tracer_provider.force_flush()
time.sleep(2)

Run it and open the resulting get_weather.tool span in Phoenix.

Expected behavior

  • tool.name is populated (e.g. from the span name / traceloop.entity.name).
  • input.value holds the resolved tool arguments (the inputs mapping).
  • output.value holds the tool result, with mime_type reflecting whether the
    result is JSON or plain text.
  • Optionally tool.parameters is populated.

Desktop (please complete the following information):

  • OS: macOS / Linux
  • Version:
    • openinference-instrumentation-openllmetry: 0.1.9
    • opentelemetry-instrumentation-langchain: 0.52.6
    • openinference-semantic-conventions: 0.1.29
    • langchain-core: 1.3.2
    • arize-phoenix: 16.3.0
    • Python: 3.12

Additional context

The fix point is the non-LLM branch in
OpenInferenceSpanProcessor.on_end() / _map_generic_span(): when the span is
a tool, set tool.name and unwrap traceloop.entity.input/output into clean
arguments/result rather than copying the raw envelopes. LLM-span handling is
unaffected.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions