Capstone 11 — LLM Observability & Eval Dashboard
Langfuse went open-core. Arize Phoenix published the 2026 GenAI semconv mappings. Helicone and Braintrust both doubled down on per-user cost attribution. Traceloop's OpenLLMetry became the de-facto SDK instrumentation. The production shape is ClickHouse for traces, Postgres for metadata, Next.js for UI, and a small army of eval jobs (DeepEval, RAGAS, LLM-judge) running over sampled traces. Build one self-hosted, ingest from at least four SDK families, and demonstrate catching an injected regression in under five minutes.
Lesson 11 of 85Capstone25 hoursTypeScript (UI), Python / TypeScript (ingest + evals), SQL (ClickHouse)
Before this: Phase 11 (LLM engineering), Phase 13 (tools), Phase 17 (infrastructure), Phase 18 (safety)
On this page
Problem
Every AI team running production traffic in 2026 keeps an observability plane alongside the model. Cost attribution. Hallucination detection. Drift monitoring. Jailbreak signal. SLO dashboards. PII leak alerts. The open-source references — Langfuse, Phoenix, OpenLLMetry — converged on OpenTelemetry GenAI semantic conventions as the ingest schema. You can now instrument OpenAI, Anthropic, Google, LangChain, LlamaIndex, and vLLM with one SDK and ship compatible spans.
You will build a self-hosted dashboard that ingests from at least four SDK families, runs a small set of eval jobs over sampled traces, detects drift, and alerts. The measurement bar: given a deliberately injected regression (a prompt that starts producing PII), the dashboard catches it and fires an alert in under five minutes.
Concept
Ingest is OTLP HTTP. The SDK produces GenAI-semconv spans: gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.response.id, llm.prompts, llm.completions. Spans land in ClickHouse for columnar analytics; metadata (users, sessions, apps) lands in Postgres.
Evals run as batch jobs over sampled traces. DeepEval scores faithfulness, toxicity, and answer relevance. RAGAS scores retrieval metrics when the trace carries retrieval context. Custom LLM-judges run domain-specific checks (PII leak, off-policy response). Eval runs write back to the same ClickHouse as eval spans linked to the parent trace.
Drift detection watches embedding-space distributions over time (PSI or KL divergence on prompt embeddings) plus eval-score trends. Alerts feed Prometheus Alertmanager and then Slack / PagerDuty. The UI is Next.js 15 with Recharts.
Architecture
production apps:
OpenAI SDK + Anthropic SDK + Google GenAI SDK
LangChain + LlamaIndex + vLLM
|
v
OpenTelemetry SDK with GenAI semconv
|
v OTLP HTTP
collector (ingest, sample, fan-out)
|
+-------------+-----------+
v v v
ClickHouse Postgres S3 archive
(spans) (metadata) (raw events)
|
+---> eval jobs (DeepEval, RAGAS, LLM-judge)
| sampled or all-trace
| write eval spans back
|
+---> drift detector (PSI / KL on prompt embeddings)
|
+---> Prometheus metrics -> Alertmanager -> Slack / PagerDuty
|
v
Next.js 15 dashboard (Recharts)
Stack
- Ingest: OpenTelemetry SDKs + GenAI semantic conventions; OTLP HTTP transport
- Collector: OpenTelemetry Collector with tail-sampling processor (for cost control)
- Storage: ClickHouse for spans, Postgres for metadata, S3 for raw event archive
- Evals: DeepEval, RAGAS 0.2, Arize Phoenix evaluator pack, custom LLM-judge
- Drift: PSI / KL on pooled prompt embeddings (sentence-transformers) weekly
- Alerting: Prometheus Alertmanager -> Slack / PagerDuty
- UI: Next.js 15 App Router + Recharts + server actions
- SDKs supported out of the box: OpenAI, Anthropic, Google GenAI, LangChain, LlamaIndex, vLLM
ce-otel-drift
Build It
Collector config. OpenTelemetry Collector with the OTLP HTTP receiver, a tail-sampler keeping 100% of errored traces and 10% of successes, and exporters to ClickHouse and S3.
ClickHouse schema. Table
spanswith columns mirroring GenAI semconv:gen_ai_system,gen_ai_request_model,input_tokens,output_tokens,latency_ms,prompt_hash,trace_id,parent_span_id, plus JSON bag for long payloads. Add secondary indexes by user_id and app_id.SDK coverage test. Write a small client app using each SDK (OpenAI, Anthropic, Google, LangChain, LlamaIndex, vLLM) with OpenLLMetry auto-instrument. Verify each produces canonical GenAI spans that land in ClickHouse.
Eval jobs. A scheduled job reads last-15-min sampled traces and runs DeepEval faithfulness, toxicity, and answer relevance. Outputs are eval spans linked to the parent trace.
Custom LLM-judge. A PII-leak judge: given a response, call a guard LLM to score likelihood of PII leak. High-score responses land in a triage queue.
Drift detection. Weekly job computes PSI between this week's pooled prompt embeddings and the trailing 4-week baseline. If PSI above threshold, alert.
Dashboard. Next.js 15 with pages: overview (spans/sec, cost/user, p95 latency), traces (search + waterfall), evals (faithfulness trend, toxicity), drift (PSI over time), alerts.
Alerting chain. Prometheus exporter reads eval score aggregates and latency percentiles; Alertmanager routes to Slack for warnings and PagerDuty for critical breaches.
Regression probe. Inject a bug: the evaluated chatbot starts leaking fake SSNs 1% of the time. Measure time to detect (MTTD): from bug deployed to Slack alert. Detection is the part of MTTR the dashboard owns; the fix and redeploy come after.
Use It
$ curl -X POST https://my-otel-collector/v1/traces -d @trace.json
[collector] accepted 1 trace, 3 spans
[clickhouse] inserted 3 spans (app=chat, user=u_42)
[eval] DeepEval faithfulness 0.82, toxicity 0.03
[drift] weekly PSI 0.08 (below 0.2 threshold)
[ui] live at https://obs.example.com
Ship It
outputs/skill-llm-observability.md is the deliverable. Given an LLM application, the dashboard ingests its traces, runs evals, alerts on drift, and surfaces cost/user breakdown in Next.js.
| Weight | Criterion | How it is measured |
|---|---|---|
| 25 | Trace-schema coverage | Number of SDK families producing canonical GenAI spans (target: 6+) |
| 20 | Eval correctness | DeepEval / RAGAS scores vs hand-labeled set |
| 20 | Dashboard UX | MTTD on injected regression (under 5 minutes target) |
| 20 | Cost / scale | Sustained ingest at 1k spans/sec without backlog |
| 15 | Alerting + drift detection | Prometheus/Alertmanager chain exercised end to end |
| 100 |
Exercises
Add custom instrumentation for the Haystack framework. Verify canonical spans land in ClickHouse with faithful
gen_ai.*attributes.Swap DeepEval for Phoenix evaluators on the same traces. Measure score drift between the two eval engines.
Sharpen the drift detector: compute PSI per app-id rather than globally. Show per-app drift trails.
Add a "user impact" page: cost-per-user and failure-rate-per-user with sparklines.
Build a tail-sampling policy that keeps 100% of traces with toxicity > 0.5 plus a 10% stratified sample of the rest. Measure sampling bias introduced.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| GenAI semconv | "OTel LLM attributes" | 2025 OpenTelemetry spec for LLM span attributes (system, model, tokens) |
| Tail sampling | "Post-trace sample" | Collector decides to keep or drop a trace after it completes (can peek errors) |
| PSI | "Population stability index" | Drift metric comparing two distributions; > 0.2 typically signals meaningful drift |
| LLM-judge | "Eval as model" | An LLM scoring another LLM's output on a rubric (faithfulness, toxicity, PII) |
| Tail-sampling policy | "Keep-rule" | Rule that decides which traces to persist vs drop; errored + sample-rate |
| Eval span | "Linked eval trace" | Child span carrying an eval score linked to the original LLM call span |
| Cost per user | "Unit economics" | Dollar cost attributed to a user_id over a window; key product metric |
Further Reading
- Langfuse — the reference open-core observability platform
- Arize Phoenix — alternate reference with strong drift support
- OpenLLMetry (Traceloop) — auto-instrumentation SDK family
- OpenTelemetry GenAI semantic conventions — the ingest schema
- Helicone — alternate hosted observability
- Braintrust — alternate eval-first platform
- ClickHouse documentation — columnar span store
- DeepEval — evaluator library
Check yourself
Quick quiz
Which ingest schema do Langfuse, Phoenix, and OpenLLMetry converge on?
- Prometheus exposition format
- OpenTelemetry GenAI semantic conventions over OTLP HTTP
- Proprietary JSON per vendor
- Plain CSV log files
Show the answer
B. OpenTelemetry GenAI semantic conventions over OTLP HTTP
Why separate ClickHouse and Postgres in the storage tier?
- They are interchangeable and one is chosen at random
- Postgres is faster for span ingest
- ClickHouse handles columnar analytics over spans while Postgres holds users, sessions, and app metadata
- ClickHouse cannot store strings
Show the answer
C. ClickHouse handles columnar analytics over spans while Postgres holds users, sessions, and app metadata
What does the tail-sampling processor in the OpenTelemetry Collector do?
- Decides whether to keep a trace after it completes, using rules like keep errors plus sample successes
- Replays old traces into Postgres
- Streams every byte unconditionally
- Truncates spans below 100ms
Show the answer
A. Decides whether to keep a trace after it completes, using rules like keep errors plus sample successes
How does the dashboard detect drift across weeks?
- Counts unique trace IDs
- Computes PSI or KL divergence on pooled prompt embeddings and watches eval-score trends
- Manual eyeballing of the dashboard
- Reads the latest deploy timestamp
Show the answer
B. Computes PSI or KL divergence on pooled prompt embeddings and watches eval-score trends
What is the deliverable's time-to-detect (MTTD) target on an injected PII-leak regression?
- Under 1 hour
- Within 24 hours
- Under 5 minutes from bug deployed to Slack alert
- Within the next on-call shift
Show the answer
C. Under 5 minutes from bug deployed to Slack alert
Which SDK families must produce canonical GenAI spans to meet the trace-coverage rubric?
- OpenAI and Anthropic only
- Any one SDK is enough
- Only vLLM
- At least six: OpenAI, Anthropic, Google GenAI, LangChain, LlamaIndex, and vLLM
Show the answer
D. At least six: OpenAI, Anthropic, Google GenAI, LangChain, LlamaIndex, and vLLM
How are evaluation results linked back to the original LLM call?
- As a separate Postgres table with no trace ID
- As a CSV emailed nightly
- As Slack messages only
- As eval spans written as children of the parent trace in ClickHouse
Show the answer
D. As eval spans written as children of the parent trace in ClickHouse
Questions people ask
How do you track the cost of AI API calls per user or feature?
Log every call with its model, input and output tokens, latency and tags such as user, feature and team. Multiply tokens by the model price to get cost, then group by tag. Observability tools do this automatically and show spend by user, feature or day.
What is LLM observability?
It is the practice of recording what an AI system does on each request: the prompt, the response, tool calls, timings, cost and errors. Teams use these traces to debug bad answers, find slow steps, control spending and run quality checks on real traffic.
Lessons from AI Engineering from Scratch by Rohit Ghumare and contributors, MIT license. Notes for owners by Lozo. License