LlamaIndex
The OpenInference LlamaIndex instrumentation (the one Arize Phoenix uses) hooks into LlamaIndex’s own instrumentation and writes a span for each agent workflow step, tool call and model call, with openinference.span.kind and llm.* attributes. Spanlight reads them for the kind, the model, the token counts and the messages. Send them to Spanlight’s OTLP endpoint with an OpenTelemetry tracer provider.
Before you start
- A running Spanlight instance that your application can reach
- A project API key with the ingest:write scope (the default). Creating one needs the member, admin or owner role
- Python 3.13 (the version this page was tested with)
Install
Section titled “Install”pip install "openinference-instrumentation-llama-index==4.6.0" "llama-index-core==0.14.25" "llama-index-llms-openai==0.8.3" "openai==2.54.0" "opentelemetry-sdk==1.45.1" "opentelemetry-exporter-otlp-proto-http==1.45.1"Point the OpenTelemetry exporter at Spanlight with the standard variables:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:8080/v1/otlp/tracesexport OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobufexport OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20spl_live_..."Use the traces-specific endpoint variable with the full path. Configure an exporter explains why. OpenInference records the messages on the span by default, so no other setting is needed.
Set up tracing
Section titled “Set up tracing”Create a tracer provider that exports over OTLP, pass it to the instrumentor, then run your agent as before:
import asyncio
from llama_index.core.agent.workflow import FunctionAgentfrom llama_index.llms.openai import OpenAIfrom openinference.instrumentation.llama_index import LlamaIndexInstrumentorfrom opentelemetry import tracefrom opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporterfrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider()provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))trace.set_tracer_provider(provider)LlamaIndexInstrumentor().instrument(tracer_provider=provider)
def get_weather(city: str, unit: str = "celsius") -> str: """Get the current weather for a city.""" return "18 degrees and sunny"
agent = FunctionAgent( name="weather_agent", tools=[get_weather], llm=OpenAI(model="gpt-4.1-mini"), system_prompt="Answer weather questions with the get_weather tool.", streaming=False,)asyncio.run(agent.run("What is the weather in Paris and Tokyo? Use the tool."))provider.force_flush()A long-running server does not need provider.force_flush(); a short script does, or its last spans are lost.
Check it worked
Section titled “Check it worked”Open Traces in the dashboard and pick the project that owns your key. The recorded run gives a trace named FunctionAgent.run with 18 spans: the agent’s workflow steps (BaseWorkflowAgent.run_agent_step, BaseWorkflowAgent.call_tool and others), an OpenAI.achat span for each model call and a FunctionTool.acall span for each tool call. Open an OpenAI.achat span: it shows the messages, the answer, the token counts and the cost. The trace’s token total is the sum of its two model calls.
What is recorded
Section titled “What is recorded”| Span in the recorded run | OpenInference span kind | Spanlight kind |
|---|---|---|
FunctionAgent.run, BaseWorkflowAgent.* steps |
CHAIN |
chain |
FunctionTool.acall |
TOOL |
tool |
OpenAI.achat |
LLM |
llm |
OpenAI._prepare_chat_with_tools |
LLM |
chain, with no model |
On each model call:
- Provider and model from
llm.systemandllm.model_name. LlamaIndex sends the model name the agent asked for, such asgpt-4.1-mini, not the snapshot OpenAI billed; Spanlight prices it at that model’s price. - Input, output and cached tokens from
llm.token_count.prompt,llm.token_count.completionandllm.token_count.prompt_details.cache_read. Only model calls carry token counts, so each call is counted once and the workflow steps add nothing to the totals. - Messages and answer from the flattened
llm.input_messages.*andllm.output_messages.*attributes, including the tool calls the model asked for and the tool results sent back. - Finish reason from
llm.finish_reason:tool_callson the turn that called the tool,stopon the answer. - Cost, computed by Spanlight from the model and the token counts; see How cost is calculated.
The prompt-building step is not a model call. LlamaIndex labels OpenAI._prepare_chat_with_tools an LLM span, but it only assembles the request: it has no messages and no token counts. Spanlight stores it as a chain with no model, so it is not listed as a model call and does not mark the trace as having an unknown cost. Its model stays in the span’s attributes as llm.model_name. See Span kind.
Workflow and tool spans keep input.value and output.value as their input and output.
Which attribute fills which field is listed in OpenInference.
What is not recorded
Section titled “What is not recorded”- Time to first token. The recorded run did not stream. On a stream, OpenInference sends the first token as a span event, which Spanlight does not read.
- The billed model snapshot. See the model note above.
- Cache writes. The recorded calls went to an OpenAI stand-in, which reports none.
- Streaming agents, query engines, retrievers and other LLM integrations. They were not recorded, so this page makes no claim about them.
- Exporter-computed cost. Spanlight never reads one; it prices every span itself.
OTLP compatibility shows the recorded scenario field by field.
Troubleshooting
Section titled “Troubleshooting”- No trace arrives. Call
instrument()with the tracer provider before you build the agent, and flush the provider before a script exits. Then work through No trace arrives. - The exporter gets
422 VALIDATION_ERROR. It sent to/v1/traces. SetOTEL_EXPORTER_OTLP_TRACES_ENDPOINTto the full/v1/otlp/tracesURL. - An
OpenAI._prepare_chat_with_toolsspan shows as a chain. That is intended; see What is recorded.