""" Clase 16 — Observabilidad con Arize Phoenix + OpenTelemetry. Sigue el patrón oficial de la documentación de Phoenix: https://docs.arize.com/phoenix/ - PHOENIX_COLLECTOR_ENDPOINT apunta al endpoint BASE (sin /v1/traces). - auto_instrument=True para que Phoenix parchee LangChain/LangGraph/OpenAI al importarlos. - register() debe llamarse ANTES de importar langchain_openai / langgraph. Si TRACING_ENABLED=false, es un no-op silencioso (para HF Spaces, Streamlit Cloud free, etc., donde no hay Phoenix corriendo). Uso: from ops.observability import init_tracing init_tracing() # al arrancar la app from langchain_openai import ChatOpenAI # importar DESPUÉS """ from __future__ import annotations import os import logging logger = logging.getLogger(__name__) _GREEN = "\033[32m" _YELLOW = "\033[33m" _DIM = "\033[2m" _RESET = "\033[0m" # Endpoint BASE de Phoenix (sin /v1/traces — se lo añade el cliente). DEFAULT_COLLECTOR_ENDPOINT = "http://localhost:6006" _already_initialized = False def _tracing_enabled() -> bool: val = os.getenv("TRACING_ENABLED", "true").strip().lower() return val in ("1", "true", "yes", "on") def _resolve_endpoint() -> str: """ Orden de precedencia: 1. PHOENIX_COLLECTOR_ENDPOINT (nombre canónico de la doc) 2. PHOENIX_ENDPOINT (alias de compatibilidad con versiones previas) 3. default localhost:6006 """ return ( os.getenv("PHOENIX_COLLECTOR_ENDPOINT") or os.getenv("PHOENIX_ENDPOINT") or DEFAULT_COLLECTOR_ENDPOINT ).rstrip("/") def init_tracing(service_name: str = "docops-agent") -> bool: """ Registra el tracer de Phoenix con autoinstrumentación. IMPORTANTE: llama a esta función ANTES de importar langchain_openai, langgraph, etc. Si se importan antes, el patch no se aplica. Returns: True si tracing quedó activo, False si está deshabilitado o falló. """ global _already_initialized if _already_initialized: return True if not _tracing_enabled(): print(f"{_DIM}[tracing] TRACING_ENABLED=false → tracing deshabilitado{_RESET}") return False endpoint = _resolve_endpoint() # Normaliza: si alguien pasó el endpoint con /v1/traces, lo recortamos, # porque phoenix.otel.register espera el base URL. if endpoint.endswith("/v1/traces"): endpoint = endpoint[: -len("/v1/traces")] # Expórtalo en el environment — algunos integradores lo leen directamente. os.environ["PHOENIX_COLLECTOR_ENDPOINT"] = endpoint try: from phoenix.otel import register except ImportError as e: print( f"{_YELLOW}[tracing] ⚠️ Dependencia faltante (módulo: {e.name}).\n" f" Las trazas NO llegarán a Phoenix.\n" f" Instala: pip install arize-phoenix-otel " f"openinference-instrumentation-langchain{_RESET}" ) return False try: register( project_name=service_name, endpoint=f"{endpoint}/v1/traces", # register SÍ quiere el path OTLP auto_instrument=True, # parchea langchain, langgraph, openai… ) except Exception as e: print( f"{_YELLOW}[tracing] No pude inicializar Phoenix en {endpoint} " f"({type(e).__name__}: {e}). La app sigue sin tracing.{_RESET}" ) return False _already_initialized = True print( f"{_GREEN}[tracing] Tracing activo → {endpoint} " f"(project={service_name}, auto_instrument=True){_RESET}" ) return True def trace_url() -> str: """Devuelve la URL de la UI de Phoenix (el base, sin /v1/traces).""" return _resolve_endpoint()