DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

LLPY-09: Phoenix y OpenTelemetry - Observabilidad Completa

🎯 El Desafío de Debugging en Sistemas RAG

Imagina que tu sistema RAG está en producción:

  • ✅ API recibe 100+ req/s
  • ✅ Pipeline complejo: Embedding → Qdrant → Reranking → LLM
  • ✅ Múltiples servicios integrados

Pero hay un problema: ¿Cómo debuggeas cuando algo sale mal?

Preguntas sin Respuesta

  1. Performance: "¿Por qué esta query tardó 5 segundos cuando debería tomar 2?"
  2. Quality: "¿Por qué el LLM generó una respuesta incorrecta?"
  3. Errors: "¿Dónde falló exactamente en el pipeline?"
  4. Cost: "¿Cuántos tokens estoy consumiendo por día?"
  5. User Experience: "¿Qué % de usuarios obtienen respuestas relevantes?"

Sin observabilidad: debugging a ciegas con print statements y logs dispersos.

La Complejidad del RAG Pipeline

User Query: "¿Cuántos días de vacaciones?"
   ↓
   ├─ [1] Embedding generation (30ms) ✅
   │   └─ Model: multilingual-e5-small, Dimensions: 384
   ↓
   ├─ [2] Qdrant search (35ms) ✅
   │   └─ Found 10 documents, scores: 0.85-0.91
   ↓
   ├─ [3] Reranking (20ms) ✅
   │   └─ Model: ms-marco-MiniLM, Top-5 kept
   ↓
   ├─ [4] LLM generation (1800ms) ⚠️ SLOW!
   │   └─ OpenAI GPT-3.5-turbo, 1500 tokens
   ↓
   └─ [5] Evaluation (async, 2500ms)
       └─ Relevance: 0.92, Hallucination: 0.05

TOTAL: 1885ms + eval async
Enter fullscreen mode Exit fullscreen mode

¿Cómo sabes si cada paso funcionó correctamente?

📊 La Magnitud del Problema

Requisitos de Observabilidad para RAG

  1. 📊 Tracing: Visualizar cada paso del pipeline end-to-end
  2. 🔍 Instrumentation: Auto-track llamadas a LLM APIs (OpenAI, Gemini)
  3. 📈 Metrics: Latency, throughput, error rates por servicio
  4. 🎯 Evaluations: Calidad automática de respuestas (LLM-as-judge)
  5. 🔗 Context Propagation: Correlacionar logs/spans de una misma request
  6. 💰 Cost Tracking: Tokens consumidos, costo por query
  7. 🐛 Error Tracking: Stack traces con contexto completo
  8. 📊 Dashboards: Visualización en tiempo real

Desafíos Técnicos

  1. 🕸️ Distributed Tracing: Correlacionar operaciones asíncronas
  2. 🔄 Async Evaluation: No bloquear respuesta del usuario
  3. 📦 Complex Payloads: Serializar metadatos complejos para OTel
  4. ⚡ Low Overhead: Tracing no debe agregar >10ms de latency
  5. 🎯 Sampling: En producción, no trackear todo (costo + overhead)

💡 La Solución: Phoenix + OpenTelemetry

¿Qué es Phoenix?

Phoenix (por Arize AI) es una plataforma de observabilidad especializada en LLMs y RAG:

  • 🎯 Purpose-built: Diseñado específicamente para aplicaciones LLM
  • 📊 Tracing: Visualización de pipelines RAG completos
  • 🤖 LLM-as-Judge: Evaluación automática de respuestas
  • 📈 Analytics: Métricas de calidad, costo, performance
  • 🔍 Debugging: Drill-down en queries problemáticas
  • 💰 Open Source: Self-hosted o cloud (Phoenix Cloud)

¿Qué es OpenTelemetry?

OpenTelemetry es el estándar de observabilidad open source:

  • 📏 Specification: Define cómo instrumentar aplicaciones
  • 📦 SDKs: Librerías para Python, JS, Go, Java, etc.
  • 🔌 Exporters: Envía datos a backends (Phoenix, Datadog, New Relic, etc.)
  • 🎯 Vendor-neutral: No lock-in a un provider específico

Arquitectura Phoenix + OpenTelemetry

┌─────────────────────────────────────────────────────────┐
│                    Lus Laboris API                      │
│                                                         │
│  ┌──────────────────────────────────────────────┐     │
│  │  PhoenixMonitoringService                    │     │
│  │  - Tracer Provider (OpenTelemetry)           │     │
│  │  - Instrumentors (OpenAI auto-instrumented)  │     │
│  │  - Custom Spans (manual tracking)            │     │
│  └───────────────┬──────────────────────────────┘     │
│                  │                                      │
│          OpenTelemetry SDK                              │
│                  │                                      │
└──────────────────┼──────────────────────────────────────┘
                   │
                   │ gRPC (4317) o HTTP (6006)
                   │
           ┌───────▼────────┐
           │  Phoenix       │
           │  Collector     │
           │                │
           │  - Receives    │
           │    traces      │
           │  - Stores      │
           │    spans       │
           │  - Runs        │
           │    evals       │
           └───────┬────────┘
                   │
           ┌───────▼────────┐
           │  Phoenix UI    │
           │  (Port 6006)   │
           │                │
           │  - Traces      │
           │  - Metrics     │
           │  - Evals       │
           │  - Analytics   │
           └────────────────┘
Enter fullscreen mode Exit fullscreen mode

🚀 Configuración Paso a Paso

1. Variables de Entorno

# .env

# Phoenix Configuration
API_PHOENIX_ENABLED=true
API_PHOENIX_ENDPOINT=http://localhost:6006      # Phoenix HTTP
API_PHOENIX_GRPC_ENDPOINT=localhost:4317        # Phoenix gRPC (más rápido)
API_PHOENIX_USE_GRPC=true                       # Preferir gRPC
API_PHOENIX_API_KEY=                             # Vacío para local, requerido para cloud
API_PHOENIX_PROJECT_NAME=lus-laboris-api

# Environment
API_ENVIRONMENT=development  # Options: development, production, testing
Enter fullscreen mode Exit fullscreen mode

Configuraciones por ambiente:

Variable Development Production
API_PHOENIX_ENABLED true true
API_PHOENIX_ENDPOINT http://localhost:6006 https://app.phoenix.arize.com
API_PHOENIX_USE_GRPC true true
API_PHOENIX_API_KEY (vacío) phx_... (cloud API key)
API_ENVIRONMENT development production

2. Docker Compose con Phoenix

# src/lus_laboris_api/docker-compose.yml

services:
  # Qdrant vector database
  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_storage:/qdrant/storage
    networks:
      - api-network

  # Phoenix observability
  phoenix:
    image: arizephoenix/phoenix:latest
    container_name: phoenix
    ports:
      - "6006:6006"  # HTTP UI
      - "4317:4317"  # gRPC collector
    environment:
      - PHOENIX_PORT=6006
      - PHOENIX_GRPC_PORT=4317
    networks:
      - api-network

  # Lus Laboris API
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - API_PHOENIX_ENDPOINT=http://phoenix:6006
      - API_PHOENIX_GRPC_ENDPOINT=phoenix:4317
      - API_PHOENIX_USE_GRPC=true
      - API_QDRANT_URL=http://qdrant:6333
    depends_on:
      - qdrant
      - phoenix
    networks:
      - api-network

volumes:
  qdrant_storage:

networks:
  api-network:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Iniciar stack completo:

cd src/lus_laboris_api
docker-compose up -d

# Verificar que Phoenix está up
curl http://localhost:6006/healthz

# Acceder a Phoenix UI
open http://localhost:6006
Enter fullscreen mode Exit fullscreen mode

3. Settings con Pydantic

# src/lus_laboris_api/api/config.py

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    """Application settings"""

    # Phoenix Monitoring Configuration
    api_phoenix_enabled: bool = True
    api_phoenix_endpoint: str | None = None
    api_phoenix_grpc_endpoint: str | None = "localhost:4317"
    api_phoenix_use_grpc: bool = True
    api_phoenix_api_key: str | None = None
    api_phoenix_project_name: str = "lus-laboris-api"

    # Environment Configuration
    api_environment: str = "development"  # development, production, testing

    class Config:
        env_file = ".env"
        case_sensitive = False

settings = Settings()
Enter fullscreen mode Exit fullscreen mode

4. PhoenixMonitoringService

El servicio se inicializa al arrancar la aplicación y configura el stack completo de observabilidad:

¿Qué hace PhoenixMonitoringService?

  1. Lee configuración de settings

    • Project name, endpoints (gRPC/HTTP), API keys
    • Detecta ambiente (development vs production)
  2. Conecta a Phoenix con estrategia inteligente

    • Prioridad 1: gRPC (2-3x más rápido que HTTP)
    • Fallback: HTTP si gRPC no está disponible
    • Graceful degradation: Si falla, API sigue funcionando
  3. Registra tracer provider con phoenix.otel.register()

    • project_name: Para separar proyectos en Phoenix UI
    • auto_instrument=True: Tracking automático de librerías
    • batch=True en producción: Agrupa spans para eficiencia
  4. Auto-instrumenta OpenAI con OpenInference

    • OpenAI: Auto-tracked (tokens, latency, errors)
    • Gemini: Manual tracking (no hay instrumentor aún)
  5. Crea tracer para tracking manual con OpenTelemetry

Configuración según ambiente:

Aspecto Development Production
Span Processor SimpleSpanProcessor (inmediato) BatchSpanProcessor (agrupa 512 spans)
Transport gRPC o HTTP gRPC preferido (performance)
Batch false (debugging) true (eficiencia)

Beneficios clave:

  • gRPC: <2ms overhead vs 5-10ms con HTTP
  • Auto-instrumentation: OpenAI tracked sin código extra
  • Graceful: Si Phoenix falla, API no se rompe
  • Environment-aware: Optimizado por ambiente

🔍 Tracking del Pipeline RAG

1. Session Management

def create_session(self, user_id: str | None = None) -> str:
    """Create a monitoring session for a user request"""
    session_id = str(uuid.uuid4())

    self.session_tracker[session_id] = {
        "session_id": session_id,
        "user_id": user_id,
        "start_time": datetime.now(),
        "actions": [],
        "llm_calls": [],
        "metrics": {},
    }

    logger.info(f"Created monitoring session: {session_id}")
    return session_id

def end_session(self, session_id: str) -> dict[str, Any]:
    """End session and calculate final metrics"""
    if session_id not in self.session_tracker:
        return {}

    session_data = self.session_tracker[session_id]
    session_data["end_time"] = datetime.now()
    session_data["duration"] = (
        session_data["end_time"] - session_data["start_time"]
    ).total_seconds()

    # Calculate final metrics
    session_metrics = self._calculate_session_metrics(session_data)
    session_data["final_metrics"] = session_metrics

    logger.info(f"Session {session_id} ended. Duration: {session_data['duration']:.2f}s")

    # Cleanup
    del self.session_tracker[session_id]

    return session_data
Enter fullscreen mode Exit fullscreen mode

2. Track Embedding Generation

Este método registra el paso de generación de embeddings en el pipeline:

¿Qué registra?

  • Modelo usado (multilingual-e5-small)
  • Longitud del texto de entrada
  • Tiempo de generación (típicamente ~30ms)
  • Session ID para correlación

Cómo funciona:

  1. Crea un span de OpenTelemetry llamado "embedding_generation"
  2. Añade attributes (modelo, tiempo, longitud de texto)
  3. Registra en session tracker local
  4. Marca como Status.OK si todo sale bien

Visible en Phoenix UI: Como un span dentro del trace completo del request.

3. Track Vectorstore Search

Registra la búsqueda en Qdrant (vectorstore):

¿Qué registra?

  • Longitud de la query
  • Cantidad de documentos retornados (típicamente 5-10)
  • Tiempo de búsqueda (~30-35ms)

Visible en Phoenix: Span "vectorstore_search" que muestra performance de Qdrant.

4. Track Reranking

Registra el paso de reranking con cross-encoder:

¿Qué registra?

  • Cantidad de documentos rerankeados (10 → 5)
  • Tiempo de reranking (~20ms)

Visible en Phoenix: Span "document_reranking" que muestra overhead del reranking.

5. Track LLM Call

Registra llamadas a LLMs (OpenAI o Gemini):

¿Qué registra?

  • Provider y modelo (openai/gpt-3.5-turbo o gemini/gemini-1.5-flash)
  • Longitud de prompt y respuesta
  • Tokens consumidos (para cálculo de costos)
  • Métricas básicas de calidad (coherence, relevance, completeness)

Auto-instrumentation:

  • OpenAI: Tracked automáticamente por OpenInference (tokens, latency, errors)
  • Gemini: Tracking manual porque no hay instrumentor aún

Visible en Phoenix: Span "llm_call_openai_gpt-3.5-turbo" que domina el tiempo total (~1.8s de 2s total).

🎯 Integración con RAG Pipeline

En el RAGService

# src/lus_laboris_api/api/services/rag_service.py

from .phoenix_service import phoenix_service

async def answer_question(self, question: str, session_id: str | None = None):
    """Complete RAG pipeline with Phoenix tracking"""

    start_time = time.time()

    # 1. Create session
    if not session_id:
        session_id = phoenix_service.create_session()

    try:
        # 2. Retrieve documents (tracked internally)
        documents, retrieval_metadata = self._retrieve_documents(question, session_id)

        # 3. Generate response (tracked internally)
        answer = await self._generate_response(question, documents, session_id)

        # 4. Calculate total time
        processing_time = time.time() - start_time

        # 5. Enqueue evaluation (async, non-blocking)
        context = self._build_context(documents)
        evaluation_service.enqueue_evaluation(
            session_id=session_id,
            question=question,
            context=context,
            answer=answer,
            documents=documents,
            metadata={
                "processing_time": processing_time,
                "llm_provider": self.llm_provider,
                "llm_model": self.llm_model,
            }
        )

        # 6. Return response
        return {
            "success": True,
            "question": question,
            "answer": answer,
            "processing_time_seconds": round(processing_time, 3),
            "session_id": session_id,
            ...
        }

    except Exception as e:
        logger.exception(f"[{session_id}] Failed to answer question")
        return {
            "success": False,
            "error": str(e),
            "session_id": session_id,
        }
    finally:
        # End session
        phoenix_service.end_session(session_id)
Enter fullscreen mode Exit fullscreen mode

Flujo completo:

  1. Create session
  2. Track embedding → Track Qdrant search → Track reranking → Track LLM
  3. Enqueue async evaluation
  4. Return response to user
  5. End session (calculates metrics)

📊 Visualización en Phoenix UI

1. Acceder a Phoenix

# Local
open http://localhost:6006

# Cloud
open https://app.phoenix.arize.com
Enter fullscreen mode Exit fullscreen mode

2. Traces View

Muestra todos los requests con breakdown detallado:

┌────────────────────────────────────────────────────────┐
│ Trace: session_abc123 (1.8s)                          │
│                                                        │
│ ├─ embedding_generation (30ms)                        │
│ │  ├─ model: multilingual-e5-small                   │
│ │  └─ text_length: 45                                │
│                                                        │
│ ├─ vectorstore_search (35ms)                          │
│ │  ├─ results_count: 10                              │
│ │  └─ search_time: 0.035s                            │
│                                                        │
│ ├─ document_reranking (20ms)                          │
│ │  ├─ documents_count: 10                            │
│ │  └─ reranking_time: 0.020s                         │
│                                                        │
│ └─ llm_call_openai_gpt-3.5-turbo (1.7s)               │
│    ├─ provider: openai                                │
│    ├─ model: gpt-3.5-turbo                           │
│    ├─ prompt_length: 1450                            │
│    ├─ response_length: 320                           │
│    ├─ quality.coherence: 0.85                        │
│    ├─ quality.relevance: 0.92                        │
│    └─ quality.completeness: 0.78                     │
│                                                        │
│ TOTAL: 1.815s                                          │
└────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

3. LLM Calls Dashboard

┌─────────────────────────────────────────────────────┐
│ LLM Calls Summary (Last 24h)                        │
│                                                     │
│ Total Calls: 1,234                                  │
│ OpenAI: 856 (69%)                                   │
│ Gemini: 378 (31%)                                   │
│                                                     │
│ Avg Latency: 1.8s                                   │
│ p50: 1.5s | p95: 2.8s | p99: 4.2s                  │
│                                                     │
│ Total Tokens: 2.3M                                  │
│ Input: 1.5M | Output: 800K                         │
│                                                     │
│ Estimated Cost: $3.45                               │
│ OpenAI: $2.80 | Gemini: $0.65                      │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

4. Evaluations View

┌─────────────────────────────────────────────────────┐
│ Evaluation Results (Last 100 queries)              │
│                                                     │
│ Relevance (avg):    0.89 ████████████████░░ 89%   │
│ Correctness (avg):  0.85 ██████████████░░░░ 85%   │
│ Completeness (avg): 0.78 █████████████░░░░░ 78%   │
│ Hallucination rate:  5%  ░░░░░░░░░░░░░░░░░░  5%   │
│ Grounding (avg):    0.92 ████████████████▓░ 92%   │
│                                                     │
│ Top Issues:                                         │
│ - 8 queries with low completeness (<0.6)           │
│ - 3 potential hallucinations detected              │
│ - 2 queries with poor grounding (<0.7)             │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

🤖 LLM-as-a-Judge Evaluations

Evaluation Service: Calidad Automática

El EvaluationService evalúa la calidad de cada respuesta usando un LLM como juez:

¿Cómo funciona?

  1. Enqueue evaluation (no bloqueante)

    • Usuario recibe respuesta inmediatamente
    • Evaluación se ejecuta en background thread
  2. Evalúa múltiples dimensiones con prompts específicos:

    • Relevance: ¿La respuesta es relevante a la pregunta? (0-1)
    • Correctness: ¿Es factualmente correcta según el contexto? (0-1)
    • Hallucination: ¿Contiene info no presente en el contexto? (0-1)
    • Grounding: Inverso de hallucination (1 - hallucination)
  3. Envía resultados a Phoenix con phoenix_service.track_evaluation()

    • Métricas visibles en Phoenix UI
    • Historial completo para análisis de tendencias

Métricas típicas:

  • Relevance: ~0.89 (89%)
  • Correctness: ~0.85 (85%)
  • Hallucination: ~0.05 (5%)
  • Grounding: ~0.92 (92%)

Ventajas:

  • Non-blocking: No impacta latencia del usuario
  • Automated: Evalúa 100% de respuestas sin intervención humana
  • Continuous: Detecta degradación de calidad en tiempo real
  • Actionable: Identifica queries problemáticas automáticamente

🎯 Casos de Uso Reales

Para Debugging de Latency:

"¿Por qué esta query tardó 5 segundos?"

Phoenix UI:

  1. Abrir Phoenix → Traces
  2. Filtrar por processing_time > 5s
  3. Ver breakdown:
    • Embedding: 30ms ✅
    • Qdrant: 35ms ✅
    • Reranking: 20ms ✅
    • LLM: 4.8s ⚠️ CULPABLE

Acción: LLM es el cuello de botella. Opciones:

  • Cambiar a Gemini 1.5 Flash (más rápido)
  • Reducir max_tokens
  • Implementar caching de respuestas

Para Debugging de Calidad:

"¿Por qué el LLM generó una respuesta incorrecta?"

Phoenix UI:

  1. Abrir Phoenix → Evaluations
  2. Filtrar por correctness < 0.6
  3. Ver query específica:
    • Question: "¿Cuántos días de preaviso?"
    • Context: [5 documentos irrelevantes]
    • Answer: "15 días" (incorrecto)
    • Correctness: 0.4

Acción: El problema es retrieval, no LLM. Opciones:

  • Mejorar query embedding
  • Ajustar top_k o reranking
  • Revisar metadatos de documentos

Para Cost Optimization:

"¿Cuánto estoy gastando en OpenAI?"

Phoenix UI:

  1. Abrir Phoenix → LLM Calls
  2. Ver dashboard:
    • Total tokens (24h): 2.3M
    • GPT-3.5-turbo: $2.80/día
    • GPT-4: $12.50/día (70 calls)

Acción: GPT-4 es muy costoso. Opciones:

  • Usar GPT-4 solo para queries complejas
  • Implementar clasificador (simple → GPT-3.5, complex → GPT-4)
  • Migrar a Gemini 1.5 Pro (60% más barato)

Para Quality Monitoring:

"¿Qué % de respuestas tienen hallucinations?"

Phoenix UI:

  1. Abrir Phoenix → Evaluations
  2. Ver dashboard:
    • Hallucination rate: 5% (61 de 1,234 queries)

Acción: 5% es aceptable, pero revisar casos:

  • 3 queries con hallucination score >0.8
  • Común cuando context es irrelevante
  • Agregar guardrail: "No tengo información suficiente"

🚀 El Impacto Transformador

Antes de Phoenix:

  • 🐛 Debugging: print() statements en 10 archivos diferentes
  • ⏱️ Performance: "Parece lento, pero no sé por qué"
  • 💰 Cost: "Creo que gasto $50/mes, pero no estoy seguro"
  • 🎯 Quality: "Los usuarios dicen que a veces está mal, pero no sé cuándo"
  • 📊 Metrics: CSV files con logs dispersos

Después de Phoenix:

  • 🐛 Debugging: Ver trace completo en Phoenix UI en 10 segundos
  • ⏱️ Performance: "LLM tarda 1.8s avg, p99 es 4.2s"
  • 💰 Cost: "$3.45/día, OpenAI usa $2.80, Gemini $0.65"
  • 🎯 Quality: "89% relevance, 5% hallucination rate"
  • 📊 Metrics: Dashboard en tiempo real con drill-down completo

Métricas de Mejora:

Aspecto Sin Phoenix Con Phoenix Mejora
MTTR (Mean Time To Resolution) 2-4 horas 10-30 minutos -85%
Quality Detection Manual sampling (10%) Automated (100%) +900%
Cost Visibility Weekly estimates Real-time tracking N/A
Performance Insights Guessing Precise breakdown N/A

💡 Lecciones Aprendidas

1. gRPC es 2-3x Más Rápido que HTTP

Phoenix con gRPC tiene latency de <2ms para enviar spans. HTTP puede agregar 5-10ms.

2. Batch Processing en Producción es Crítico

En dev, usar SimpleSpanProcessor (inmediato). En prod, usar BatchSpanProcessor (agrupa 512 spans antes de enviar).

3. Session IDs son el Glue

Correlacionar todos los spans de una request con un session_id único permite drill-down completo en Phoenix.

4. Async Evaluation no Bloquea

Evaluar calidad en background (evaluationservice) no impacta latencia de respuesta al usuario.

5. Auto-Instrumentation > Manual

OpenInference auto-instrumenta OpenAI (tracks tokens, latency, errors). Gemini requiere tracking manual porque no tiene instrumentor aún.

6. Serialize Complex Metadata

OpenTelemetry solo acepta primitives (str, int, float, bool). Dicts/lists deben serializarse a JSON string.

🎯 El Propósito Más Grande

Phoenix + OpenTelemetry no es solo "nice to have" - es el sistema nervioso del RAG pipeline. Sin observabilidad:

  • Debugging es adivinanza
  • Performance es misterio
  • Quality es anécdota
  • Cost es estimación

Con Phoenix + OpenTelemetry:

  • Visibility: Ver cada paso del pipeline en tiempo real
  • Debuggability: Drill-down en queries problemáticas en segundos
  • Quality: Evaluación automática de 100% de respuestas
  • Optimization: Identificar cuellos de botella con precisión
  • Cost Control: Track tokens y costos en tiempo real
  • Continuous Improvement: Métricas históricas para optimización

Estamos convirtiendo un sistema RAG de "caja negra" a cristal transparente, donde cada operación es visible, medible, y optimizable.


🔗 Recursos y Enlaces

Repositorio del Proyecto

Documentación Técnica

Recursos Externos


Próximo Post: LLPY-10 - Autenticación JWT con RSA

En el siguiente post exploraremos cómo implementar autenticación segura con JWT y claves RSA, generación de tokens, validación en endpoints, y integración con GCP Secret Manager.

Top comments (0)