|

API 200 y servicio lento: trazas con OpenTelemetry

Una API puede devolver HTTP 200 en 2.000 ms y aun así el usuario sentir que el sitio está roto. El código de estado confirma que la petición terminó sin error; no dice nada sobre dónde se consumió el tiempo. En una arquitectura con gateway, varios servicios, base de datos y APIs externas, la respuesta correcta llega tarde y el problema queda invisible para los logs de la aplicación y para el monitor de CPU del servidor.

¿Por qué una API puede devolver 200 y aun así el servicio ir lento?

Porque el tiempo total es la suma de tramos que se ejecutan en sistemas distintos, y el código de estado sólo describe el resultado final. Un caso ilustrativo del artículo que originó esta nota: el gateway tarda 20 ms, el servicio de búsqueda 40 ms, la consulta a la base 80 ms, una API externa 1.800 ms y el formateo de la respuesta 60 ms. El cliente ve “200, 2.000 ms” y nada más.

EtapaTiempo
API Gateway20 ms
Procesamiento interno del servicio40 ms
Consulta a base de datos80 ms
Llamada a API externa1.800 ms
Formateo de resultados60 ms

La fuente aclara que esos tramos se suponen secuenciales y sin solapamiento, y que el ejemplo es hipotético. Aun así, el punto se sostiene: logs sin errores, CPU normal y conexión a base de datos estable son compatibles con una latencia que el usuario percibe como caída. Cuando la causa está tres saltos más allá del servicio que alertó, el monitoreo por servidor no alcanza.

¿Qué es la trazabilidad distribuida y por qué importa en microservicios?

La trazabilidad distribuida es la técnica que reconstruye el recorrido completo de una petición a través de todos los procesos que toca. En lugar de mirar cada servidor por separado, asigna un identificador común a la petición y registra cada operación con su inicio, su fin y su relación con la anterior. Así se ve en qué salto se fue el tiempo, incluso entre servicios escritos en lenguajes distintos y operados por equipos distintos. Te puede servir nuestra guía de observabilidad en FastAPI.

Importa en microservicios porque ahí el trabajo se reparte: un checkout puede involucrar quince procesos y el lento estar lejos del que disparó la alerta. Sin un identificador compartido, esa reconstrucción se hace a mano, copiando timestamps entre pestañas.

Logs, métricas y trazas: tres preguntas distintas

Los tres pilares de la observabilidad no compiten entre sí; responden cosas diferentes. La documentación de OpenTelemetry describe las trazas como el camino de una petición por la aplicación, y la fuente original lo resume en una tabla que conviene retener:

SeñalQué respondeQué aporta
Logs¿Qué pasó en este proceso?Mensajes de error, resultados de una operación
Métricas¿Cómo está el sistema, en números?Cantidad de requests, tasa de error, latencia P95
Trazas¿Por dónde pasó esta petición y cuánto tardó cada paso?Orden de llamadas entre servicios y duración por salto

El valor aparece al correlacionarlos. Una métrica dice que el P95 subió; una traza muestra qué llamada lo causó; el log asociado a ese trace ID agrega el detalle que la traza no captura.

Cómo funcionan Trace, Span y la propagación de contexto

Un trace es el flujo completo de una petición y se identifica con un trace ID que comparten todos los servicios involucrados. Un span es una operación individual dentro de ese flujo, con nombre, timestamps de inicio y fin, un span ID propio y, salvo el span raíz, un identificador del span padre. Los spans se anidan y forman un árbol: ahí está la cascada que se ve en las herramientas de tracing. Esto se conecta con lo que analizamos en nuestra guía de monitoreo en Kubernetes con OpenTelemetry.

Los attributes agregan metadatos al span, como el método HTTP, la ruta o el código de respuesta. La documentación oficial recomienda usar convenciones semánticas de atributos para estandarizar los metadatos comunes entre sistemas.

Lo que hace posible el tracing entre procesos es la propagación de contexto. OpenTelemetry usa el estándar W3C Trace Context, que viaja en el header traceparent con este formato:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Los campos son la versión, el trace ID de 16 bytes, el span ID del llamador de 8 bytes y las flags. Un detalle que suele pasarse por alto: en el header viajan sólo el contexto (trace ID, span ID, flags y estado), no los attributes ni los eventos. Si un servicio externo no está instrumentado, no hay forma de ver su trabajo interno; lo único observable es el span del cliente que lo llamó, que igual sirve para medir cuánto tardó esa llamada.

Cómo se instrumenta una API en Python

Con el SDK de OpenTelemetry y la instrumentación automática de FastAPI, un ejemplo mínimo queda así (adaptado de la fuente, que simula las demoras con time.sleep para el ejemplo). También analizamos este escenario en nuestro artículo sobre Bloomberg.

resource = Resource.create({"service.name": "search-api"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
FastAPIInstrumentor.instrument_app(app)

@app.get("/search")
def search():
 with tracer.start_as_current_span("database.query"):
 time.sleep(0.08)
 with tracer.start_as_current_span("external_api.call"):
 time.sleep(1.8)
 return {"status": "success", "results": []}

En producción se cambia el exportador de consola por uno que envíe los datos a un Collector, y se agrega auto-instrumentación para las librerías de HTTP y de base de datos en lugar de abrir spans a mano.

Por qué el Collector va en el medio

Un servicio puede exportar sus trazas directamente al backend, pero cuando hay decenas de servicios, cada uno termina gestionando envío, filtrado y reintentos. El OpenTelemetry Collector centraliza esa tarea con cuatro piezas: receivers que reciben, processors que filtran o transforman, exporters que entregan y un pipeline que los conecta. La contrapartida es operativa: hay que monitorear su disponibilidad, su capacidad de procesamiento y el backpressure.

¿Cómo evitar que el almacenamiento de trazas salga carísimo?

Con sampling: guardar una fracción de las trazas en lugar de todas. El head sampling decide al inicio de la petición, cuando todavía no se sabe si va a fallar o a tardar. El tail sampling decide al final, con los spans ya recolectados, y por eso puede retener el 100% de los requests con error y de los que superaron un umbral de latencia. A cambio, consume más memoria y requiere que los spans de una misma traza lleguen juntos al punto de decisión cuando hay varios colectores.

SituaciónQué conviene evaluar
Servicio chico, poco tráficoRecolección total o sampling simple
Tráfico alto, costo sensibleHead sampling
Foco en errores y requests lentosTail sampling
Requisitos mixtosPolítica jerárquica combinada

La fuente original es explícita: lo importante no es el porcentaje, sino definir qué requests necesitás poder analizar y construir la política a partir de eso.

¿Qué datos sensibles no conviene guardar en los spans?

Tokens de acceso, API keys, contraseñas, identificadores de sesión, datos personales y cuerpos de request sensibles. Todo eso puede terminar en un attribute sin que nadie lo note, y el sistema de observabilidad concentra información de producción con accesos amplios. La recomendación operativa es no recolectar por defecto, registrar sólo los attributes necesarios, filtrar o enmascarar en la recolección y limitar el acceso y la retención.

El caso de Baggage merece atención aparte: es el mecanismo para pasar información entre servicios a través de headers, de modo que puede salir hacia sistemas externos. No conviene poner secretos ahí, y cualquier valor recibido desde afuera debe tratarse como input no confiable.

Un criterio para verificar antes de rediseñar

Propuesta editorial, no un resultado de la fuente: antes de instrumentar todo, tomá un endpoint con latencia alta y hacé dos comprobaciones. Primero, compará la métrica de P95 con la cascada de una traza lenta del mismo intervalo; si el span más costoso es una llamada saliente a un servicio externo, el problema está en el proveedor o en la red, no en tu código. Segundo, repetí la medición con la llamada externa deshabilitada o apuntando a un stub local. Si el tiempo total cae a la suma de los tramos internos, confirmaste la hipótesis; si no, el cuello está en otro lado y conviene revisar base de datos y conexiones. Es una prueba acotada y no concluyente sobre la causa raíz, pero alcanza para decidir dónde mirar primero.

Las fuentes consultadas

Te puede interesar...