Instrument the Application
Attach the Splunk Agent Observability Callback
The agent runs its LangGraph workflow asynchronously, so you’ll attach Splunk Agent Observability’s async callback handler. Because the callback is passed at the graph level, it propagates to every node automatically, with no per-tool instrumentation required.
Add the imports
We’ve already added the following imports to the
~/workshop/healthcare-assistant/2-app-with-instrumentation/agent.py file, which are required
to collect traces:
import os
from splunk_ao import splunk_ao_context
from splunk_ao.handlers.langchain import SplunkAOAsyncCallbackWrap the graph invocation in a Splunk AO context
We’ve updated the ~/workshop/healthcare-assistant/2-app-with-instrumentation/agent.py file
to update the _process_query_async function to open a splunk_ao_context, start a session keyed to the agent’s session_id,
and attach a fresh SplunkAOAsyncCallback to the run config:
async def _process_query_async(self, messages: List[Dict[str, str]]) -> str:
if not self.tools:
self.load_tools()
self.graph = self._build_graph()
langchain_messages: List[BaseMessage] = []
for msg in messages:
if msg["role"] == "user":
langchain_messages.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant":
langchain_messages.append(AIMessage(content=msg["content"]))
with splunk_ao_context(
project=os.getenv("SPLUNK_AO_PROJECT"),
agent_stream=os.getenv("SPLUNK_AO_AGENT_STREAM"),
):
splunk_ao_context.start_session(external_id=self.session_id)
# One callback per request keeps each user turn in its own trace.
callback = SplunkAOAsyncCallback()
run_config = {**self.langgraph_config, "callbacks": [callback]}
result = await self.graph.ainvoke(
{"messages": langchain_messages},
run_config,
)
if result["messages"]:
return result["messages"][-1].content
return "No response generated"Why a single callback per request?
SplunkAOAsyncCallback per call to _process_query_async keeps each user turn
in its own trace. Because it’s attached to the LangGraph run config, every node’s LLM and
tool call becomes a nested span under that same trace, giving you the end-to-end view of a
turn instead of a pile of disconnected spans.Why does this app use SplunkAOAsyncCallback rather than SplunkAOCallback?
