Traditional observability tells you what happened. AI observability tells you why it happened and whether it was good.
LLMs are non-deterministic, expensive, and opaque. You need to see not just errors and latency, but quality, cost, and behavior patterns. This guide covers comprehensive observability for AI systems.
Diagram
flowchart TB
subgraph Pillars["Observability Pillars"]
L[Logs<br/>What happened]
M[Metrics<br/>How much]
T[Traces<br/>Where it flowed]
E[Evaluations<br/>How good]
end
subgraph Sources["Data Sources"]
API[API Gateway]
LLM[LLM Calls]
DB[Database]
User[User Actions]
end
subgraph Outputs["Outputs"]
D[Dashboards]
A[Alerts]
R[Reports]
I[Investigations]
end
API --> L & M & T
LLM --> L & M & T & E
DB --> L & M & T
User --> L & E
L & M & T & E --> D
M --> A
E --> R
L & T --> I
style E fill:#14b8a6,color:#fff
style A fill:#ef4444,color:#fff
| Pillar | What It Captures | AI Application |
|---|
| Logs | Events, errors | Prompts, responses, errors |
| Metrics | Aggregated measurements | Latency, cost, throughput |
| Traces | Request flow | End-to-end AI pipeline |
| Component | Purpose | Example |
|---|
| Quality metrics | Output correctness | Accuracy, relevance |
| User feedback | Actual satisfaction | Thumbs up/down |
| Automated evals | Continuous testing | LLM-as-judge |
| Level | Data | When |
|---|
| Always | Request ID, timestamp, model, tokens, latency | Every request |
| Debug | Full prompt, full response | Development |
| Audit | User input, output, decisions | Compliance |
| Error | Stack trace, context | Failures |
| Field | Type | Purpose |
|---|
| request_id | string | Trace correlation |
| timestamp | ISO 8601 | Timeline |
| user_id | string | User attribution |
| session_id | string | Session grouping |
| model | string | Model used |
| prompt_hash | string | Prompt version |
| tokens_in | integer | Input size |
| tokens_out | integer | Output size |
| latency_ms | integer | Performance |
| status | enum | Success/failure |
| error | object | Error details |
| Data Type | Strategy | Implementation |
|---|
| PII in prompts | Redaction | Regex patterns |
| Credentials | Never log | Pre-filter |
| User content | Hash or encrypt | At-rest encryption |
| Model outputs | Conditional | Based on sensitivity |
| Log Type | Retention | Reasoning |
|---|
| Operational | 30 days | Debugging |
| Audit | 1-7 years | Compliance |
| Quality | 90 days | Analysis |
| Error | 90 days | Patterns |
Typescript
interface LLMRequestLog {
request_id: string;
timestamp: string;
user_id: string;
session_id: string;
model: string;
prompt_hash: string;
tokens_in: number;
tokens_out: number;
latency_ms: number;
ttft_ms: number;
status: 'success' | 'error' | 'timeout' | 'rate_limited';
cost_usd: number;
error?: {
code: string;
message: string;
retryable: boolean;
};
metadata: Record<string, any>;
}
class AILogger {
private sensitivePatterns = [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Email
/\b\d{3}-\d{2}-\d{4}\b/g, // SSN
/\b\d{16}\b/g, // Credit card
];
async logRequest(log: LLMRequestLog, prompt?: string, response?: string) {
// Redact sensitive data
const safePrompt = prompt ? this.redact(prompt) : undefined;
const safeResponse = response ? this.redact(response) : undefined;
// Hash prompt for versioning without storing full text
const promptHash = await this.hashContent(prompt || '');
const enrichedLog: LLMRequestLog = {
...log,
prompt_hash: promptHash,
};
// Log to appropriate destinations
await Promise.all([
this.logToOperational(enrichedLog),
log.status === 'error' && this.logToErrors(enrichedLog, safePrompt, safeResponse),
this.shouldAudit(log) && this.logToAudit(enrichedLog, safePrompt, safeResponse),
]);
// Update metrics
this.metrics.recordRequest(enrichedLog);
}
private redact(text: string): string {
let redacted = text;
for (const pattern of this.sensitivePatterns) {
redacted = redacted.replace(pattern, '[REDACTED]');
}
return redacted;
}
private shouldAudit(log: LLMRequestLog): boolean {
// Audit high-risk operations
return log.metadata?.is_decision === true ||
log.metadata?.affects_user_data === true;
}
}
| Metric | Description | Alert Threshold |
|---|
| Request rate | Requests per second | Anomaly detection |
| Error rate | Failures / total | Over 2% |
| Latency p50 | Median response time | Over 2x baseline |
| Latency p95 | Tail latency | Over 3x baseline |
| Latency p99 | Worst case | Over 5x baseline |
| Metric | Description | Target |
|---|
| Token usage | Tokens per request | Budget limit |
| Cost per request | Dollar amount | Budget limit |
| TTFT | Time to first token | Under 500ms |
| Model distribution | Requests by model | Expected ratio |
| Cache hit rate | Cached / total | Over 30% |
| Metric | Description | Target |
|---|
| User rating | Thumbs up ratio | Over 85% |
| Regeneration rate | Retry requests | Under 15% |
| Edit rate | User modifications | Under 30% |
| Task completion | Successful flows | Over 90% |
| Hallucination rate | Factual errors | Under 5% |
| Metric | Description | Target |
|---|
| Cost per user | Total cost / users | Budget |
| Value delivered | Tasks completed | Growing |
| Engagement | Sessions per user | Growing |
| Conversion | Feature adoption | Target % |
| Component | What to Capture | Purpose |
|---|
| Span | Single operation | Individual steps |
| Trace | Full request flow | End-to-end view |
| Context | Metadata | Correlation |
| Events | Notable occurrences | Debugging |
| Span | Duration | Key Attributes |
|---|
| Request received | Start | User ID, input |
| Preprocessing | 10-50ms | Transformations |
| Retrieval | 50-200ms | Query, results count |
| Prompt construction | 5-20ms | Template, tokens |
| LLM call | 500-5000ms | Model, tokens, cost |
| Post-processing | 10-50ms | Transformations |
| Response sent | End | Output size |
Typescript
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('ai-service');
async function handleAIRequest(request: AIRequest) {
return tracer.startActiveSpan('ai.request', async (span) => {
try {
span.setAttributes({
'user.id': request.userId,
'session.id': request.sessionId,
'input.length': request.input.length,
});
// Preprocessing span
const processed = await tracer.startActiveSpan('ai.preprocess', async (prepSpan) => {
const result = await preprocess(request.input);
prepSpan.setAttributes({ 'tokens.count': result.tokenCount });
return result;
});
// Retrieval span
const docs = await tracer.startActiveSpan('ai.retrieval', async (retSpan) => {
const results = await vectorSearch(processed.embedding);
retSpan.setAttributes({
'retrieval.query': processed.query,
'retrieval.results_count': results.length,
'retrieval.top_score': results[0]?.score || 0,
});
return results;
});
// LLM call span - the most important one
const response = await tracer.startActiveSpan('ai.llm_call', async (llmSpan) => {
const startTime = Date.now();
const result = await callLLM({
model: request.model,
prompt: buildPrompt(processed, docs),
});
const latency = Date.now() - startTime;
llmSpan.setAttributes({
'llm.model': request.model,
'llm.tokens_in': result.usage.promptTokens,
'llm.tokens_out': result.usage.completionTokens,
'llm.ttft_ms': result.timeToFirstToken,
'llm.latency_ms': latency,
'llm.cost_usd': calculateCost(request.model, result.usage),
'llm.finish_reason': result.finishReason,
});
return result;
});
span.setStatus({ code: SpanStatusCode.OK });
return response;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
| Strategy | Rate | Use Case |
|---|
| All traces | 100% | Development |
| Head sampling | 10-50% | Low volume |
| Tail sampling | Errors + slow | High volume |
| Adaptive | Dynamic | Production |
| Challenge | Solution |
|---|
| Cross-service correlation | Trace context propagation |
| Async operations | Parent span linking |
| External API calls | Span wrapping |
| Background jobs | Job ID correlation |
| Category | Urgency | Response Time |
|---|
| Critical | Page immediately | Under 5 min |
| High | Page during hours | Under 1 hour |
| Medium | Ticket | Under 24 hours |
| Low | Review weekly | Under 1 week |
| Alert | Condition | Severity |
|---|
| Error rate spike | Over 5% for 5 min | Critical |
| Latency spike | p95 over 5x for 10 min | High |
| Cost anomaly | Over 2x daily average | High |
| Model unavailable | Provider errors over 50% | Critical |
| Quality drop | User rating under 70% | Medium |
| Practice | Why | Implementation |
|---|
| Actionable alerts | Reduce fatigue | Clear runbook |
| Grouped alerts | Reduce noise | Correlation rules |
| Severity levels | Prioritization | Tiered routing |
| Auto-resolution | Reduce toil | Self-healing |
| Alert | First Response | Escalation |
|---|
| Error spike | Check logs, recent deploys | Rollback |
| Latency spike | Check model provider | Fallback model |
| Cost spike | Check usage patterns | Rate limit |
| Quality drop | Check prompt changes | Revert |
| Step | Action | Tools |
|---|
| 1. Identify | Find the problem | Alerts, user reports |
| 2. Reproduce | Recreate consistently | Request replay |
| 3. Isolate | Find the component | Tracing |
| 4. Analyze | Understand root cause | Logs, context |
| 5. Fix | Implement solution | Code change |
| 6. Verify | Confirm resolution | Testing |
| Issue | Symptoms | Investigation |
|---|
| Hallucination | Wrong facts | Check retrieval, prompt |
| Slow response | High latency | Check model, tokens |
| Format error | Invalid output | Check prompt, examples |
| Refusal | Model won't answer | Check safety filters |
| Inconsistency | Variable output | Check temperature, prompt |
| Tool | Purpose | When to Use |
|---|
| Request replay | Reproduce issues | Specific failures |
| Prompt playground | Test variations | Prompt debugging |
| A/B comparison | Compare approaches | Optimization |
| Shadow mode | Safe testing | New changes |
| Component | Purpose | Examples |
|---|
| Log aggregation | Collect, search | Datadog, Splunk |
| Metrics | Time-series data | Prometheus, Datadog |
| Tracing | Request flow | Jaeger, Honeycomb |
| AI-specific | LLM monitoring | LangSmith, Helicone |
| Dashboards | Visualization | Grafana, Datadog |
| Tool | Specialty | Best For |
|---|
| LangSmith | LangChain tracing | LangChain apps |
| Helicone | LLM gateway | Cost tracking |
| Weights & Biases | ML tracking | Model training |
| Arize | Model monitoring | Drift detection |
| Component | Build | Buy | Recommendation |
|---|
| Basic logging | Easy | Cheap | Buy |
| Metrics | Medium | Cheap | Buy |
| Tracing | Hard | Medium | Buy |
| AI-specific | Hard | Medium | Evaluate |
| Custom dashboards | Medium | Included | Customize |
| Panel | Metrics | Audience |
|---|
| Health | Error rate, uptime | Leadership |
| Cost | Daily/monthly spend | Finance |
| Usage | Active users, requests | Product |
| Quality | User satisfaction | Leadership |
| Panel | Metrics | Purpose |
|---|
| Latency | p50, p95, p99 | Performance |
| Errors | Rate, types, top errors | Debugging |
| Models | Distribution, performance | Optimization |
| Retrieval | Hit rate, relevance | RAG health |
| Panel | Metrics | Purpose |
|---|
| Active alerts | Current issues | Triage |
| Recent changes | Deploys, config | Correlation |
| Error trends | Last hour | Patterns |
| Quick actions | Rollback, restart | Response |
- 1Log everything, sample wisely - Capture full context for debugging, but sample for storage.
- 2AI needs quality metrics - Error rate is not enough. Track ratings, regenerations, and edits.
- 3Trace the full pipeline - Every step from input to output should be traceable.
- 4Alert on business impact - Cost spikes and quality drops matter as much as errors.
- 5Invest in debugging tools - Request replay and prompt playgrounds save hours.
- 6Dashboards for each audience - Executives, engineers, and on-call need different views.
You cannot improve what you cannot measure. In AI systems, measurement is especially hard because quality is subjective. Build observability that captures not just what happened, but whether it was good.