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.
Describe the bug
OpenInferenceSpanProcessor(fromopeninference-instrumentation-openllmetry)converts every non-
llmOpenLLMetry/Traceloop span through a single_map_generic_span()path that copies onlytraceloop.entity.input/traceloop.entity.outputverbatim. For LangChain tool spans this producesa span that is classified
TOOL(correct) but is missing the tool semanticsPhoenix needs to render it:
tool.name(and notool.parameters) attribute is set, even though thetool name is available to the processor (it is present in the span name and
in
traceloop.entity.output'skwargs.name);input.valueis the raw Traceloop envelope{"input_str": ..., "tags": [...], "metadata": {...}, "inputs": {<args>}, "kwargs": {...}}rather than the resolved tool arguments;
output.valueis 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 spanwhose
traceloop.span.kindis not"llm"through_map_generic_span(), thenclears and replaces all attributes:
_map_generic_span()emits onlyopeninference.span.kind,input.value/input.mime_type(fromtraceloop.entity.input), andoutput.value/output.mime_type(fromtraceloop.entity.output). It nevermaps the tool name to
tool.name, nor unwraps the LangChain tool argumentenvelope 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):
Run it and open the resulting
get_weather.toolspan in Phoenix.Expected behavior
tool.nameis populated (e.g. from the span name /traceloop.entity.name).input.valueholds the resolved tool arguments (theinputsmapping).output.valueholds the tool result, withmime_typereflecting whether theresult is JSON or plain text.
tool.parametersis populated.Desktop (please complete the following information):
openinference-instrumentation-openllmetry: 0.1.9opentelemetry-instrumentation-langchain: 0.52.6openinference-semantic-conventions: 0.1.29langchain-core: 1.3.2arize-phoenix: 16.3.0Additional context
The fix point is the non-LLM branch in
OpenInferenceSpanProcessor.on_end()/_map_generic_span(): when the span isa tool, set
tool.nameand unwraptraceloop.entity.input/outputinto cleanarguments/result rather than copying the raw envelopes. LLM-span handling is
unaffected.