When a customer says "the bot was slow and wrong at 10:42", you need one trace showing: the query rewrite, the search (and what it returned), each model call (model, tokens, latency), each tool call, and the final answer.
1. Set up the SDK
pip install opentelemetry-sdk opentelemetry-exporter-otlp
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
provider = TracerProvider(resource=Resource.create({"service.name": "support-bot"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) # OTEL_EXPORTER_OTLP_ENDPOINT
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("support-bot")
For Application Insights, azure-monitor-opentelemetry (configure_azure_monitor()) sets this up in one call.
2. Spans for each step
def answer(question):
with tracer.start_as_current_span("rag.request") as root:
root.set_attribute("app.user_tier", user.tier)
with tracer.start_as_current_span("retrieval.search") as s:
hits = search(question, k=5)
s.set_attribute("retrieval.top_k", 5)
s.set_attribute("retrieval.top_score", hits[0].score if hits else 0)
s.set_attribute("retrieval.doc_ids", [h.id for h in hits])
with tracer.start_as_current_span("chat " + MODEL) as s:
s.set_attribute("gen_ai.operation.name", "chat")
s.set_attribute("gen_ai.request.model", MODEL)
s.set_attribute("gen_ai.request.temperature", 0.2)
resp = llm.chat(build_prompt(question, hits))
s.set_attribute("gen_ai.response.model", resp.model)
s.set_attribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens)
s.set_attribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens)
s.set_attribute("gen_ai.response.finish_reasons", [resp.choices[0].finish_reason])
return resp
The gen_ai.* names follow the OpenTelemetry GenAI semantic conventions (still evolving; check the current spec). Libraries such as OpenLLMetry or the OpenTelemetry instrumentations for OpenAI SDKs can create model-call spans automatically.
3. Prompts and answers: privacy first
Prompt and response text often contain personal data. Options:
- Don't record content by default; record ids, token counts and scores.
- Record content only for sampled traces, to a store with short retention and restricted access.
- Redact emails, phone numbers and IDs before recording.
4. Useful dashboards
- p50/p95 latency per step (retrieval vs model vs tools).
- Tokens and cost per request, per tenant.
- Error rate by
error.type(429, content filter, timeout). - Low retrieval scores over time (content gaps).
5. Connect to user feedback
Return the trace id with every answer and store it with thumbs-up/down feedback. A thumbs-down then links straight to the full trace.
Tip
Propagate trace context into tool calls and downstream APIs (W3C
traceparentheader). Then a slow answer caused by a slow CRM API shows up in the same trace instead of in a different team's dashboard.