Careeroza
Observability and Tracing
What to Log for Every LLM Call
- request ID (
message._request_id), conversation ID, user ID (hashed) - model, prompt version, effort setting
- input, output and cache tokens; cost; latency;
stop_reason - retrieved document IDs and scores; tool calls with inputs and results
- user feedback (thumbs up/down), escalation to human
A trace groups everything one request did into nested spans (retrieve → model call → tool → model call). Tools: Langfuse, LangSmith, Arize Phoenix, Helicone, or plain OpenTelemetry into Grafana or Datadog.
Example
Example
A tiny tracer built with a context manager. Durations are fixed numbers so the output is repeatable; real code would use time.perf_counter().
from contextlib import contextmanager
SPANS, stack = [], []
FAKE_MS = {"retrieve": 42, "rerank": 95, "llm_call": 1840, "tool:get_order": 60, "llm_call#2": 910}
@contextmanager
def span(name, **attrs):
stack.append(name)
SPANS.append((len(stack) - 1, name, FAKE_MS.get(name, 0), attrs))
try:
yield
finally:
stack.pop()
with span("chat_request", conversation="c-981", prompt="support_v7"):
with span("retrieve", k=30):
pass
with span("rerank", kept=4):
pass
with span("llm_call", model="claude-sonnet-5-5", in_tok=3100, out_tok=60, stop="tool_use"):
pass
with span("tool:get_order", order_id="SE1042"):
pass
with span("llm_call#2", model="claude-sonnet-5-5", in_tok=3300, out_tok=140, stop="end_turn"):
pass
total = sum(ms for depth, _, ms, _ in SPANS if depth == 1)
for depth, name, ms, attrs in SPANS:
shown = total if depth == 0 else ms
print(" " * depth + f"{name:<{22 - 2 * depth}} {shown:>5} ms {attrs}")Output
chat_request 2947 ms {'conversation': 'c-981', 'prompt': 'support_v7'}
retrieve 42 ms {'k': 30}
rerank 95 ms {'kept': 4}
llm_call 1840 ms {'model': 'claude-sonnet-5-5', 'in_tok': 3100, 'out_tok': 60, 'stop': 'tool_use'}
tool:get_order 60 ms {'order_id': 'SE1042'}
llm_call#2 910 ms {'model': 'claude-sonnet-5-5', 'in_tok': 3300, 'out_tok': 140, 'stop': 'end_turn'}OpenTelemetry
from opentelemetry import trace
tracer = trace.get_tracer("shopease.assistant")
def answer(question: str) -> str:
with tracer.start_as_current_span("llm_call") as s:
resp = client.messages.create(model="claude-sonnet-5-5", max_tokens=1000,
messages=[{"role": "user", "content": question}])
s.set_attribute("gen_ai.request.model", "claude-sonnet-5-5")
s.set_attribute("gen_ai.usage.input_tokens", resp.usage.input_tokens)
s.set_attribute("gen_ai.usage.output_tokens", resp.usage.output_tokens)
s.set_attribute("request_id", resp._request_id)
return next(b.text for b in resp.content if b.type == "text")