Instrumentation
XeroML SDKs offer two instrumentation strategies:
- Native integrations — automatic instrumentation for OpenAI, LangChain, Vercel AI SDK, and others. No manual code changes required.
- Custom instrumentation — manually define observations using context managers, decorators, or explicit observation creation.
This guide covers custom instrumentation for cases where native integrations don’t cover your full pipeline.
Context Manager
Creates a span and sets it as the active observation in the OpenTelemetry context. Any observations created inside the block automatically become children.
from xeroml import get_client
xeroml = get_client()
with xeroml.start_as_current_observation( name="my-pipeline", type="span", input={"query": user_query},) as obs: # Child observations created here are automatically nested result = run_retrieval(user_query) answer = call_llm(user_query, result) obs.update(output=answer)import { startActiveObservation } from "@xeroml/tracing";
const result = await startActiveObservation( { name: "my-pipeline", type: "span", input: { query: userQuery } }, async (obs) => { const docs = await runRetrieval(userQuery); const answer = await callLLM(userQuery, docs); obs.update({ output: answer }); return answer; });Observe Decorator / Wrapper
Wraps a function and automatically captures its inputs, outputs, timing, and any exceptions. The cleanest way to instrument existing functions without modifying their internal logic.
from xeroml import observe
@observe(name="process-query", type="span")def process_query(query: str, context: list[str]) -> str: prompt = build_prompt(query, context) return call_llm(prompt)
# Function is called normally — XeroML captures everything automaticallyresult = process_query("What is XeroML?", retrieved_docs)Disable IO capture for performance-sensitive paths:
@observe(capture_input=False, capture_output=False)def sensitive_function(secret_data: str) -> str: return process(secret_data)import { observe } from "@xeroml/tracing";
const processQuery = observe( { name: "process-query", type: "span" }, async (query: string, context: string[]) => { const prompt = buildPrompt(query, context); return await callLLM(prompt); });
const result = await processQuery("What is XeroML?", retrievedDocs);Manual Observations
Provides explicit lifecycle control. Unlike context managers, manual observations don’t alter the active OpenTelemetry context — the previous span remains active. Use this for parallel work or self-contained observations.
from xeroml import get_client
xeroml = get_client()
# Start observation (does not change active context)obs = xeroml.start_observation(name="background-task", type="span")
result = do_work()
# Must call end() explicitlyobs.end(output=result)import { startObservation } from "@xeroml/tracing";
const obs = startObservation({ name: "background-task", type: "span" });
const result = await doWork();
obs.end({ output: result });Updating Observations
Add information to an observation as your code executes — useful when output isn’t available at observation start:
with xeroml.start_as_current_observation(name="retrieval", type="span") as obs: docs = retrieve(query) obs.update( output={"num_docs": len(docs)}, metadata={"retriever": "pinecone", "top_score": docs[0].score} )Use update_current_span() to update the active observation from anywhere in the call stack:
from xeroml import update_current_observation
def inner_function(): # Update whatever span is currently active update_current_observation(metadata={"step": "post-processing"})Adding Attributes with Propagation
Use propagate_attributes() to attach user IDs, session IDs, tags, and other context to all observations in a scope:
from xeroml import propagate_attributes
with propagate_attributes( user_id="user-123", session_id="session-abc", tags=["feature:chat", "model:gpt-4o"], metadata={"region": "us-east-1"},): # All observations created here inherit these attributes result = handle_request(user_message)Trace IDs
Generate deterministic trace IDs to correlate XeroML traces with external systems:
from xeroml import create_trace_id
# Deterministic — same seed always produces the same IDtrace_id = create_trace_id(seed="order-12345-request-6789")
with propagate_attributes(trace_id=trace_id): result = process_order(order_id)Client Lifecycle
from xeroml import get_client
xeroml = get_client()
# ... instrumented code ...
xeroml.flush() # Wait for all pending data to be sent# orxeroml.shutdown() # Flush and tear down the clientTypeScript:
import { flushXeroML } from "@xeroml/tracing";
// ... instrumented code ...
await flushXeroML();