Skip to content

Instrumentation

XeroML SDKs offer two instrumentation strategies:

  1. Native integrations — automatic instrumentation for OpenAI, LangChain, Vercel AI SDK, and others. No manual code changes required.
  2. 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)

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 automatically
result = 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)

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() explicitly
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 ID
trace_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
# or
xeroml.shutdown() # Flush and tear down the client

TypeScript:

import { flushXeroML } from "@xeroml/tracing";
// ... instrumented code ...
await flushXeroML();