AI Observability and Monitoring Guide
A practical guide to implementing comprehensive observability and monitoring for AI/LLM applications in production environments.
Why AI Observability Matters
Traditional monitoring approaches fall short for AI applications. LLMs introduce unique challenges:
- Non-deterministic outputs make debugging complex
- Token costs can spiral without proper tracking
- Latency variations affect user experience
- Prompt drift and quality degradation over time
- Security vulnerabilities through prompt injection
This guide provides actionable strategies and tools to address these challenges.
Core Metrics to Monitor
1. Performance Metrics
# Essential LLM performance metrics
class LLMMetrics:
def __init__(self):
self.metrics = {
'first_token_latency': None, # Time to first token (TTFT)
'tokens_per_second': None, # Generation speed
'total_latency': None, # End-to-end response time
'queue_depth': 0, # Pending requests
'error_rate': 0.0, # Failed completions
'timeout_rate': 0.0 # Timed out requests
}2. Cost Metrics
# Token usage and cost tracking
class CostTracker:
def __init__(self):
self.model_costs = {
'gpt-4': {'input': 0.03, 'output': 0.06}, # per 1K tokens
'claude-3-opus': {'input': 0.015, 'output': 0.075},
'gpt-3.5-turbo': {'input': 0.001, 'output': 0.002}
}
def calculate_cost(self, model, input_tokens, output_tokens):
rates = self.model_costs[model]
input_cost = (input_tokens / 1000) * rates['input']
output_cost = (output_tokens / 1000) * rates['output']
return input_cost + output_cost3. Quality Metrics
# Response quality monitoring
class QualityMonitor:
def track_metrics(self, prompt, response):
return {
'response_length': len(response),
'contains_refusal': self.check_refusal(response),
'hallucination_score': self.detect_hallucination(prompt, response),
'sentiment_score': self.analyze_sentiment(response),
'relevance_score': self.check_relevance(prompt, response)
}Implementation Strategies
1. Gateway-Based Monitoring (Helicone Pattern)
# Proxy configuration for monitoring
import httpx
class MonitoringProxy:
def __init__(self, target_url, helicone_key):
self.target_url = target_url
self.headers = {
'Helicone-Auth': f'Bearer {helicone_key}',
'Helicone-Cache-Enabled': 'true',
'Helicone-Retry-Enabled': 'true'
}
async def forward_request(self, request_data):
async with httpx.AsyncClient() as client:
response = await client.post(
'https://oai.hconeai.com/v1/chat/completions',
headers=self.headers,
json=request_data
)
return response.json()2. SDK-Based Tracing (LangFuse Pattern)
from langfuse import Langfuse
from langfuse.decorators import observe, langfuse_context
# Initialize client
langfuse = Langfuse(
public_key="pk-...",
secret_key="sk-...",
host="https://cloud.langfuse.com" # or self-hosted URL
)
@observe()
async def process_with_llm(user_input: str):
# Automatic tracing of function
messages = [{"role": "user", "content": user_input}]
# Track generation
generation = langfuse_context.update_current_trace(
name="chat-completion",
model="gpt-4",
input=messages,
metadata={"user_id": "123", "session_id": "abc"}
)
response = await call_llm(messages)
# Track output and usage
generation.end(
output=response.content,
usage={
"input": response.usage.prompt_tokens,
"output": response.usage.completion_tokens
}
)
return response3. Custom Telemetry with OpenTelemetry
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
import time
tracer = trace.get_tracer(__name__)
class LLMTelemetry:
@staticmethod
def trace_llm_call(func):
def wrapper(*args, **kwargs):
with tracer.start_as_current_span("llm_call") as span:
# Add metadata
span.set_attribute("llm.model", kwargs.get("model", "unknown"))
span.set_attribute("llm.temperature", kwargs.get("temperature", 0.7))
start_time = time.time()
try:
result = func(*args, **kwargs)
# Track success metrics
span.set_attribute("llm.tokens.input", result.usage.prompt_tokens)
span.set_attribute("llm.tokens.output", result.usage.completion_tokens)
span.set_attribute("llm.latency", time.time() - start_time)
return result
except Exception as e:
span.set_status(Status(StatusCode.ERROR, str(e)))
span.record_exception(e)
raise
return wrapperPlatform Selection Guide
Decision Matrix
| Platform | Best For | Key Strength | Limitation | Cost Model |
|---|---|---|---|---|
| LangSmith | LangChain users | Deep integration | Vendor lock-in | Enterprise |
| LangFuse | Self-hosting | Open source | Setup complexity | Free/Usage |
| Helicone | Quick setup | One-line integration | Gateway dependency | Volume |
| Arize AI | ML teams | Comprehensive | LLM learning curve | Enterprise |
| Datadog | Existing users | Unified platform | LLM features new | Subscription |
Quick Start Recommendations
- Startups/Small Teams: Helicone (quick setup) or LangFuse (control)
- Enterprise: LangSmith or Datadog (existing infrastructure)
- ML-Heavy: Arize AI with Phoenix OSS
- Cost-Conscious: Self-hosted LangFuse
Production Deployment Patterns
1. Multi-Environment Setup
# docker-compose.yml for LangFuse deployment
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: langfuse
POSTGRES_USER: langfuse
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
langfuse:
image: ghcr.io/langfuse/langfuse:latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://langfuse:${DB_PASSWORD}@postgres:5432/langfuse
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: https://langfuse.yourdomain.com
TELEMETRY_ENABLED: false
depends_on:
- postgres
volumes:
postgres_data:2. High-Availability Configuration
# Distributed tracing with fallback
class ResilientMonitoring:
def __init__(self):
self.primary = LangSmithClient()
self.fallback = LocalFileLogger()
self.buffer = AsyncBuffer(max_size=1000)
async def log_trace(self, trace_data):
try:
# Try primary
await self.primary.send(trace_data)
except Exception as e:
# Fallback to local buffer
await self.buffer.add(trace_data)
self.fallback.log(trace_data)
# Async retry
asyncio.create_task(self.retry_buffered())3. Cost Optimization Strategies
# Intelligent sampling for high-volume applications
class SmartSampler:
def __init__(self, base_rate=0.1):
self.base_rate = base_rate
self.error_rate = 1.0 # Always sample errors
self.slow_rate = 0.5 # Sample slow requests
self.thresholds = {
'slow_ms': 3000,
'expensive_tokens': 1000
}
def should_sample(self, request, response):
# Always sample errors
if response.status_code >= 400:
return True
# Sample slow requests
if response.latency_ms > self.thresholds['slow_ms']:
return random.random() < self.slow_rate
# Sample expensive requests
total_tokens = response.usage.get('total_tokens', 0)
if total_tokens > self.thresholds['expensive_tokens']:
return True
# Base sampling
return random.random() < self.base_rateSecurity Monitoring
Prompt Injection Detection
class SecurityMonitor:
def __init__(self):
self.injection_patterns = [
r"ignore previous instructions",
r"reveal system prompt",
r"execute.*command",
r"</?(system|prompt|instruction)>"
]
def scan_for_threats(self, prompt):
threats = []
for pattern in self.injection_patterns:
if re.search(pattern, prompt, re.IGNORECASE):
threats.append({
'type': 'prompt_injection',
'pattern': pattern,
'severity': 'high'
})
if threats:
self.alert_security_team(threats)
return threatsCompliance Monitoring
# PII detection and compliance
class ComplianceMonitor:
def __init__(self):
self.pii_patterns = {
'ssn': r'\b\d{3}-\d{2}-\d{4}\b',
'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
'phone': r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b',
'credit_card': r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b'
}
def audit_conversation(self, messages):
violations = []
for msg in messages:
for pii_type, pattern in self.pii_patterns.items():
if re.search(pattern, msg['content']):
violations.append({
'type': pii_type,
'message_id': msg.get('id'),
'timestamp': datetime.now()
})
return violationsAdvanced Patterns
1. Distributed Tracing Across Services
# Trace context propagation
class DistributedTracer:
def __init__(self):
self.tracer = trace.get_tracer(__name__)
def inject_context(self, headers):
"""Inject trace context into HTTP headers"""
span = trace.get_current_span()
if span:
headers['X-Trace-Id'] = span.get_span_context().trace_id
headers['X-Parent-Span-Id'] = span.get_span_context().span_id
return headers
def extract_context(self, headers):
"""Extract trace context from HTTP headers"""
trace_id = headers.get('X-Trace-Id')
parent_span_id = headers.get('X-Parent-Span-Id')
if trace_id and parent_span_id:
# Continue existing trace
ctx = trace.set_span_in_context(
NonRecordingSpan(
SpanContext(
trace_id=int(trace_id),
span_id=int(parent_span_id),
is_remote=True
)
)
)
return ctx
return None2. Real-time Alerting
# Anomaly detection and alerting
class AnomalyDetector:
def __init__(self, window_size=100):
self.window_size = window_size
self.latency_history = deque(maxlen=window_size)
self.cost_history = deque(maxlen=window_size)
def check_anomalies(self, metrics):
alerts = []
# Latency anomaly
self.latency_history.append(metrics['latency'])
if len(self.latency_history) == self.window_size:
mean = statistics.mean(self.latency_history)
stdev = statistics.stdev(self.latency_history)
if metrics['latency'] > mean + (3 * stdev):
alerts.append({
'type': 'latency_spike',
'value': metrics['latency'],
'threshold': mean + (3 * stdev),
'severity': 'warning'
})
# Cost anomaly
self.cost_history.append(metrics['cost'])
if len(self.cost_history) == self.window_size:
recent_avg = statistics.mean(list(self.cost_history)[-10:])
historical_avg = statistics.mean(self.cost_history)
if recent_avg > historical_avg * 2:
alerts.append({
'type': 'cost_surge',
'recent_avg': recent_avg,
'historical_avg': historical_avg,
'severity': 'high'
})
return alertsBest Practices Checklist
Initial Setup
- Choose monitoring platform based on team needs
- Implement basic metrics collection (latency, tokens, cost)
- Set up error tracking and alerting
- Configure security scanning for prompts
- Enable request/response logging with sampling
Optimization
- Implement intelligent sampling for high volume
- Set up caching for repeated queries
- Configure batch processing for analytics
- Enable compression for stored traces
- Implement data retention policies
Security & Compliance
- Enable PII detection and masking
- Implement prompt injection detection
- Set up audit logging for compliance
- Configure role-based access control
- Regular security reviews of prompts
Continuous Improvement
- Set up A/B testing for prompts
- Track model drift and quality metrics
- Implement feedback loops from users
- Regular cost optimization reviews
- Performance baseline updates
Common Pitfalls to Avoid
- Over-monitoring: Don’t track everything - focus on actionable metrics
- Ignoring costs: Token usage can explode without proper limits
- Missing context: Always include request context in traces
- Synchronous logging: Use async patterns to avoid latency
- No sampling: High-volume apps need intelligent sampling
Future Considerations (2025+)
- AI Agent Observability: Monitoring autonomous agent behaviors
- Federated Learning Metrics: Privacy-preserving monitoring
- Edge AI Monitoring: Distributed inference tracking
- Explainable AI Integration: Understanding model decisions
- Real-time Model Updates: Monitoring continuous learning