DevOps desde Cero: Observabilidad en Kubernetes

2026-06-02 | Gabriel Garrido | 24 min de lectura
Share:

Apoya este blog

Si te resulta util este contenido, considera apoyar el blog.

Introduccion

Bienvenido al articulo quince de la serie DevOps desde Cero. En los articulos anteriores deployeamos nuestra API TypeScript a Kubernetes y la empaquetamos con Helm. Todo esta corriendo, los pods estan en verde, y la vida es buena. Pero despues alguien pregunta: “La API esta sana de verdad? Como sabemos si los tiempos de respuesta estan empeorando? Que paso a las 3am cuando los usuarios empezaron a quejarse?”


Sin observabilidad, estas volando a ciegas. Deployeaste tu app, pero no tenes idea de que esta pasando adentro. La observabilidad te da la capacidad de entender el estado interno de tu sistema examinando los datos que produce. Es la diferencia entre “algo esta roto” y “el endpoint /orders esta devolviendo errores 500 porque el pool de conexiones a la base de datos esta agotado.”


En este articulo vamos a cubrir los tres pilares de la observabilidad (logs, metricas y traces), instalar Prometheus y Grafana en EKS usando Helm, armar un dashboard basico, instrumentar nuestra API TypeScript con logging estructurado y un endpoint de metricas, configurar una alerta simple, y recorrer el flujo de observabilidad que vas a usar durante incidentes reales. Esta es una introduccion para principiantes. Si queres ir mas profundo en temas como alertas basadas en SLOs, Loki para agregacion de logs, o patrones avanzados de OpenTelemetry, mira el Deep Dive de Observabilidad SRE de la serie SRE.


Vamos a meternos de lleno.


Los tres pilares de la observabilidad

La observabilidad esta construida sobre tres tipos de datos de telemetria. Cada uno responde una pregunta diferente, y necesitas los tres para debuggear problemas de produccion efectivamente.


  • Logs: Eventos discretos que te dicen que paso. “El request abc123 fallo con un error 500 a las 14:32:05.” Los logs dan el contexto mas rico porque pueden incluir detalles arbitrarios como bodies de requests, stack traces e IDs de usuario.
  • Metricas: Mediciones numericas a lo largo del tiempo. “La API manejo 150 requests por segundo con una latencia p99 de 200ms.” Las metricas son baratas de almacenar, rapidas de consultar, y perfectas para dashboards y alertas.
  • Traces: El camino que un request toma a traves de tu sistema. “Este request paso por el API gateway, despues por el servicio de ordenes, despues por la base de datos, y la parte lenta fue la query a la base de datos.” Los traces son esenciales cuando tenes multiples servicios comunicandose entre si.

Pensalo asi: las metricas te dicen que algo esta mal, los traces te dicen donde en el sistema esta mal, y los logs te dicen por que esta mal. Aca esta el flujo:


# El flujo de observabilidad durante un incidente:
#
# 1. ALERTA (de metricas): "Tasa de errores > 5% en los ultimos 5 minutos"
#    -> Sabes que ALGO esta mal
#
# 2. DASHBOARD (metricas): Chequeando Grafana, ves que /orders tiene alta tasa de errores
#    -> Sabes QUE esta mal
#
# 3. TRACES: Encontras requests fallidos, ves que todos fallan en la llamada a la DB
#    -> Sabes DONDE esta mal
#
# 4. LOGS: Chequeando los logs del servicio de DB: "ERROR: demasiadas conexiones"
#    -> Sabes POR QUE esta mal

Vamos a cubrir cada pilar en detalle, empezando con los logs porque son los mas familiares.


Logs: logging estructurado

Si alguna vez usaste console.log("algo se rompio") en produccion, conoces el problema. Cuando tenes miles de lineas de log fluyendo por tu sistema, encontrar la relevante es como buscar una aguja en un pajar. Los logs no estructurados (strings de texto plano) son dificiles de buscar, dificiles de filtrar y dificiles de agregar.


El logging estructurado resuelve esto escribiendo logs como objetos JSON con campos consistentes. En vez de:


[2026-06-02 14:32:05] ERROR: Failed to process order 12345 for user [email protected]

Escribis:


{
  "timestamp": "2026-06-02T14:32:05.123Z",
  "level": "error",
  "message": "Failed to process order",
  "orderId": "12345",
  "userId": "[email protected]",
  "service": "orders-api",
  "traceId": "abc123def456",
  "duration_ms": 1523
}

Ahora podes buscar todos los errores relacionados con un usuario especifico, una orden especifica, o un trace especifico. Podes contar cuantos errores pasaron por servicio. Podes correlacionar logs con traces usando el campo traceId. Este es el poder del logging estructurado.


Los niveles de log definen la severidad de una entrada de log. Usalos consistentemente:


  • error: Algo fallo y necesita atencion. Un request devolvio un 500, una query a la base de datos hizo timeout, una API externa no responde.
  • warn: Algo inesperado paso pero el sistema lo manejo. Un retry funciono, hubo un cache miss, se llamo a un endpoint deprecado.
  • info: Operaciones normales que vale la pena registrar. Un request se proceso exitosamente, un usuario se logueo, un job de background se completo.
  • debug: Informacion detallada util durante el desarrollo. Payloads de requests, queries SQL, estado interno. Desactivalo en produccion a menos que estes debuggeando activamente.

Agreguemos logging estructurado a nuestra API TypeScript usando pino, que es el logger JSON mas rapido para Node.js:


# Instalar pino y el pretty-printer para desarrollo local
npm install pino pino-http
npm install -D pino-pretty

// src/logger.ts
import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  // En produccion, output JSON crudo. Localmente, usar pino-pretty para legibilidad.
  transport:
    process.env.NODE_ENV !== "production"
      ? { target: "pino-pretty", options: { colorize: true } }
      : undefined,
  // Agregar campos por defecto a cada entrada de log
  base: {
    service: "task-api",
    version: process.env.APP_VERSION || "unknown",
  },
});

export default logger;

// src/app.ts
import express from "express";
import pinoHttp from "pino-http";
import logger from "./logger";

const app = express();

// Loguear automaticamente cada request HTTP con metodo, URL, status y duracion
app.use(pinoHttp({ logger }));

app.get("/tasks", async (req, res) => {
  try {
    const tasks = await db.query("SELECT * FROM tasks");
    // Log de nivel info con contexto estructurado
    logger.info({ taskCount: tasks.length }, "Tasks retrieved successfully");
    res.json(tasks);
  } catch (error) {
    // Log de nivel error con el objeto error y contexto del request
    logger.error(
      { err: error, path: req.path, method: req.method },
      "Failed to retrieve tasks"
    );
    res.status(500).json({ error: "Internal server error" });
  }
});

Con pino-http, cada request automaticamente obtiene una entrada de log como esta:


{
  "level": 30,
  "time": 1748870525123,
  "service": "task-api",
  "req": { "method": "GET", "url": "/tasks" },
  "res": { "statusCode": 200 },
  "responseTime": 45,
  "msg": "request completed"
}

Esto es exactamente el tipo de data que podes buscar y filtrar en un sistema de agregacion de logs como Loki, Elasticsearch o CloudWatch Logs. Podes consultar cosas como “mostrame todos los requests donde responseTime > 1000” o “mostrame todos los logs de nivel error del servicio task-api en la ultima hora.”


Metricas: contando lo que importa

Mientras los logs te cuentan sobre eventos individuales, las metricas te dicen sobre el comportamiento general de tu sistema a lo largo del tiempo. Las metricas son mediciones numericas recolectadas a intervalos regulares.


Hay tres tipos de metricas core que necesitas conocer:


  • Counter: Un valor que solo sube. Ejemplos: numero total de requests HTTP, numero total de errores, bytes totales transferidos. Generalmente te importa la tasa de cambio (requests por segundo) mas que el valor crudo.
  • Gauge: Un valor que puede subir y bajar. Ejemplos: uso actual de CPU, uso de memoria, numero de conexiones activas, profundidad de cola. Los gauges representan el estado actual de algo.
  • Histogram: Mide la distribucion de valores. Ejemplos: duracion de requests, tamano de respuestas. Los histogramas te permiten responder preguntas como “cual es la latencia del percentil 99?” que es mucho mas util que el promedio.

Prometheus es el sistema de metricas estandar en el ecosistema Kubernetes. Funciona con un modelo pull: en vez de que tu aplicacion pushee metricas a un servidor, Prometheus scrapea el endpoint de metricas de tu aplicacion a intervalos regulares (generalmente cada 15 o 30 segundos).


Asi es como funciona el flujo:


Tu App (endpoint /metrics)
  |
  v
Prometheus (scrapea cada 15s, almacena datos de series de tiempo)
  |
  v
Grafana (consulta Prometheus, renderiza dashboards)
  |
  v
Alertmanager (recibe alertas de Prometheus, envia notificaciones)

Agreguemos un endpoint /metrics a nuestra API TypeScript usando la libreria prom-client:


npm install prom-client

// src/metrics.ts
import client from "prom-client";

// Crear un registry para contener todas las metricas
const register = new client.Registry();

// Agregar metricas por defecto de Node.js (CPU, memoria, event loop lag, etc.)
client.collectDefaultMetrics({ register });

// Counter custom: total de requests HTTP, etiquetado por metodo, path y status
export const httpRequestsTotal = new client.Counter({
  name: "http_requests_total",
  help: "Total number of HTTP requests",
  labelNames: ["method", "path", "status"] as const,
  registers: [register],
});

// Histogram custom: duracion de requests en segundos
export const httpRequestDuration = new client.Histogram({
  name: "http_request_duration_seconds",
  help: "Duration of HTTP requests in seconds",
  labelNames: ["method", "path", "status"] as const,
  buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
  registers: [register],
});

// Gauge custom: numero de conexiones activas a la base de datos
export const dbActiveConnections = new client.Gauge({
  name: "db_active_connections",
  help: "Number of active database connections",
  registers: [register],
});

export { register };

// src/middleware/metrics.ts
import { Request, Response, NextFunction } from "express";
import { httpRequestsTotal, httpRequestDuration } from "../metrics";

export function metricsMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const start = Date.now();

  res.on("finish", () => {
    const duration = (Date.now() - start) / 1000;
    const path = req.route?.path || req.path;
    const labels = {
      method: req.method,
      path: path,
      status: res.statusCode.toString(),
    };

    httpRequestsTotal.inc(labels);
    httpRequestDuration.observe(labels, duration);
  });

  next();
}

// src/app.ts - agregar el endpoint de metricas y el middleware
import { register } from "./metrics";
import { metricsMiddleware } from "./middleware/metrics";

// Aplicar el middleware de metricas a todas las rutas
app.use(metricsMiddleware);

// Exponer metricas para que Prometheus las scrapee
app.get("/metrics", async (_req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});

Cuando Prometheus scrapea /metrics, obtiene output como este:


# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/tasks",status="200"} 1523
http_requests_total{method="POST",path="/tasks",status="201"} 47
http_requests_total{method="GET",path="/tasks",status="500"} 3

# HELP http_request_duration_seconds Duration of HTTP requests in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{method="GET",path="/tasks",status="200",le="0.05"} 1200
http_request_duration_seconds_bucket{method="GET",path="/tasks",status="200",le="0.1"} 1450
http_request_duration_seconds_bucket{method="GET",path="/tasks",status="200",le="0.25"} 1510
http_request_duration_seconds_bucket{method="GET",path="/tasks",status="200",le="+Inf"} 1523

Para que Prometheus descubra este endpoint en Kubernetes, agregas anotaciones a tu pod o servicio:


# En el template de deployment de tu chart Helm o values
metadata:
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "3000"
    prometheus.io/path: "/metrics"

Instalando Prometheus y Grafana en EKS

La forma mas facil de tener Prometheus y Grafana corriendo en Kubernetes es el chart Helm kube-prometheus-stack. Este unico chart instala Prometheus, Grafana, Alertmanager, node-exporter (para metricas del host), kube-state-metrics (para metricas de objetos Kubernetes), y un monton de dashboards y reglas de alertas preconfiguradas.


# Agregar el repositorio Helm de la comunidad Prometheus
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Crear un namespace para monitoreo
kubectl create namespace monitoring

# Instalar el kube-prometheus-stack
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set grafana.adminPassword=tu-password-seguro \
  --set prometheus.prometheusSpec.retention=7d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=20Gi

Eso es todo. Un solo comando Helm y tenes un stack de monitoreo completo. Verifiquemos que todo este corriendo:


# Verificar todos los pods en el namespace monitoring
kubectl get pods -n monitoring

# Output esperado:
# NAME                                                     READY   STATUS    RESTARTS   AGE
# alertmanager-monitoring-kube-prometheus-alertmanager-0    2/2     Running   0          2m
# monitoring-grafana-6c4f8d5b7-x2k4f                      3/3     Running   0          2m
# monitoring-kube-prometheus-operator-7d9f5b8c9-abc12      1/1     Running   0          2m
# monitoring-kube-state-metrics-5f8d9b7c6-def34            1/1     Running   0          2m
# monitoring-prometheus-node-exporter-ghij5                1/1     Running   0          2m
# prometheus-monitoring-kube-prometheus-prometheus-0        2/2     Running   0          2m

Para acceder a Grafana localmente, usa port-forwarding:


# Forwardear Grafana a localhost:3001
kubectl port-forward svc/monitoring-grafana 3001:80 -n monitoring

# Abrir http://localhost:3001 en tu navegador
# Login: admin / tu-password-seguro

Para produccion, expondrias Grafana a traves de un Ingress con TLS. Aca hay un archivo de values rapido para un setup tipo produccion:


# monitoring-values.yaml
grafana:
  adminPassword: "${GRAFANA_ADMIN_PASSWORD}"
  ingress:
    enabled: true
    ingressClassName: alb
    hosts:
      - grafana.tudominio.com
    tls:
      - secretName: grafana-tls
        hosts:
          - grafana.tudominio.com

prometheus:
  prometheusSpec:
    retention: 15d
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          resources:
            requests:
              storage: 50Gi
    # Decirle a Prometheus que scrapee pods con las anotaciones estandar
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false

alertmanager:
  alertmanagerSpec:
    storage:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          resources:
            requests:
              storage: 5Gi

# Instalar con los values de produccion
helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  -f monitoring-values.yaml

Basicos de PromQL: consultando tus metricas

PromQL es el lenguaje de consultas para Prometheus. Se ve raro al principio, pero solo necesitas aprender un punado de patrones para cubrir la mayoria de los casos de uso.


Vector instantaneo - seleccionar el valor actual de una metrica:


# Todos los requests HTTP del task-api
http_requests_total{service="task-api"}

# Solo errores 500
http_requests_total{service="task-api", status="500"}

Rate - la funcion mas importante. Calcula la tasa por segundo de incremento para counters en una ventana de tiempo:


# Requests por segundo en los ultimos 5 minutos
rate(http_requests_total[5m])

# Tasa de errores (solo 500s) por segundo
rate(http_requests_total{status="500"}[5m])

Agregacion - combinar multiples series de tiempo:


# Total de requests por segundo a traves de todas las instancias
sum(rate(http_requests_total[5m]))

# Requests por segundo agrupados por codigo de status
sum by (status) (rate(http_requests_total[5m]))

# Porcentaje de errores
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
* 100

Cuantiles de histograma - calcular percentiles:


# Latencia p99 (percentil 99)
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

# Latencia p50 (mediana)
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))

# Latencia p99 por endpoint
histogram_quantile(0.99, sum by (path, le) (rate(http_request_duration_seconds_bucket[5m])))

Aca hay algunas consultas que vas a usar todo el tiempo:


# Uso de CPU por pod (porcentaje)
sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="task-api"}[5m])) * 100

# Uso de memoria por pod (megabytes)
sum by (pod) (container_memory_working_set_bytes{namespace="task-api"}) / 1024 / 1024

# Reinicios de pods (un reinicio generalmente significa que algo crasheo)
increase(kube_pod_container_status_restarts_total{namespace="task-api"}[1h])

# Replicas disponibles vs replicas deseadas (estan todos los pods sanos?)
kube_deployment_status_replicas_available{namespace="task-api"}
/
kube_deployment_spec_replicas{namespace="task-api"}

Armando un dashboard en Grafana

Grafana viene con cientos de dashboards pre-armados que podes importar. Para Kubernetes, el kube-prometheus-stack ya incluye dashboards para metricas de nodos, metricas de pods y resumen del cluster. Pero tambien vas a querer un dashboard custom para tu aplicacion.


Importando un dashboard de la comunidad:


  1. Abri Grafana y anda a Dashboards > Import.
  2. Ingresa un ID de dashboard de grafana.com/dashboards. Por ejemplo, el dashboard 315 es uno popular para monitoreo de cluster Kubernetes.
  3. Selecciona tu data source de Prometheus y hace click en Import.

Eso te da un dashboard listo en segundos. Ahora armemos uno custom para nuestra API.


Creando un dashboard custom:


  1. Anda a Dashboards > New Dashboard > Add visualization.
  2. Selecciona tu data source de Prometheus.
  3. Para el primer panel, ingresa esta consulta PromQL:

sum by (status) (rate(http_requests_total{service="task-api"}[5m]))

  1. Ponele como titulo “Request Rate by Status Code”.
  2. Elegi el tipo de visualizacion “Time series”.
  3. En Legend, configuralo como {{status}} para que cada linea se etiquete con su codigo de status.

Agrega mas paneles para las metricas que mas importan:


  • Tasa de requests: sum(rate(http_requests_total{service="task-api"}[5m])) como panel stat mostrando RPS total.
  • Porcentaje de tasa de errores: La consulta de porcentaje de errores de antes, mostrada como gauge con umbrales (verde < 1%, amarillo < 5%, rojo >= 5%).
  • Latencia p99: histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{service="task-api"}[5m]))) como chart de series de tiempo.
  • Conexiones activas a la DB: db_active_connections{service="task-api"} como gauge.
  • CPU y memoria de pods: Las consultas de containers de la seccion anterior.

Un buen dashboard sigue el metodo USE (Utilization, Saturation, Errors) o el metodo RED (Rate, Errors, Duration). Para una API, el metodo RED es el mas practico:


Layout de Dashboard RED:
+---------------------+-------------------+--------------------+
| Tasa de Requests    | Tasa de Errores   | Latencia p99       |
| [panel stat]        | [panel gauge]     | [panel stat]       |
+---------------------+-------------------+--------------------+
| Tasa de Requests por Codigo de Status (series de tiempo)     |
+--------------------------------------------------------------+
| Distribucion de Latencia: p50, p90, p99 (series de tiempo)   |
+--------------------------------------------------------------+
| Stream de Logs de Error (si usas Loki)                       |
+--------------------------------------------------------------+

Una vez que estes conforme con el dashboard, guardalo y anota el modelo JSON. Podes exportarlo y guardarlo en tu repositorio Git para que se pueda provisionar automaticamente. El kube-prometheus-stack soporta provisionamiento de dashboards a traves de ConfigMaps:


# grafana-dashboard-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: task-api-dashboard
  namespace: monitoring
  labels:
    grafana_dashboard: "1"
data:
  task-api.json: |
    {
      "dashboard": {
        "title": "Task API",
        "panels": [ ... ]
      }
    }

Traces: siguiendo un request a traves de servicios

Los logs te dicen que paso en un solo servicio. Los traces te dicen que paso a traves de multiples servicios para un solo request. Cada trace esta compuesto de spans, y cada span representa una unidad de trabajo: un handler HTTP, una query a la base de datos, una llamada a una API externa.


Asi se ve un trace:


Trace ID: abc123def456
|
|-- Span: API Gateway (15ms)
|   |-- Span: Middleware de autenticacion (2ms)
|   |-- Span: Llamada HTTP a Orders Service (180ms)
|       |-- Span: Query a la DB: SELECT * FROM orders (150ms)  <-- el cuello de botella!
|       |-- Span: Escritura en cache (3ms)
|
Duracion total: 200ms

Sin tracing, verias que el API Gateway tardo 200ms pero no tendrias idea de que el cuello de botella era una query lenta a la base de datos dentro del Orders Service. Con tracing, podes ver el desglose exacto.


OpenTelemetry (OTel) es el estandar para instrumentar aplicaciones con traces (y metricas y logs). Provee SDKs para todos los lenguajes principales y una forma vendor-neutral de exportar datos de telemetria. Agreguemos tracing basico a nuestra API TypeScript:


# Instalar paquetes de OpenTelemetry
npm install @opentelemetry/api \
  @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http

// src/tracing.ts - debe ser importado antes que todo lo demas
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: "task-api",
    [ATTR_SERVICE_VERSION]: process.env.APP_VERSION || "0.1.0",
  }),
  traceExporter: new OTLPTraceExporter({
    // Enviar traces a un OTel Collector o Jaeger
    url:
      process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
      "http://otel-collector:4318/v1/traces",
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      // Auto-instrumentar Express, HTTP y clientes de base de datos
      "@opentelemetry/instrumentation-express": { enabled: true },
      "@opentelemetry/instrumentation-http": { enabled: true },
      "@opentelemetry/instrumentation-pg": { enabled: true },
    }),
  ],
});

sdk.start();
console.log("OpenTelemetry tracing initialized");

// Shutdown graceful
process.on("SIGTERM", () => {
  sdk.shutdown().then(() => process.exit(0));
});

// src/index.ts - importar tracing PRIMERO
import "./tracing";
import app from "./app";

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Con auto-instrumentacion, cada request HTTP entrante, llamada HTTP saliente y query a la base de datos automaticamente obtiene un span. El SDK propaga el contexto del trace a traves de headers HTTP (traceparent), asi que cuando el servicio A llama al servicio B, los spans de ambos servicios se enlazan bajo el mismo trace ID.


Para spans custom cuando necesitas mas detalle:


// src/services/orders.ts
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("task-api");

export async function processOrder(orderId: string) {
  // Crear un span custom para esta operacion
  return tracer.startActiveSpan("processOrder", async (span) => {
    try {
      span.setAttribute("order.id", orderId);

      // Cada sub-operacion puede tener su propio span
      const order = await tracer.startActiveSpan(
        "fetchOrder",
        async (fetchSpan) => {
          const result = await db.query("SELECT * FROM orders WHERE id = $1", [
            orderId,
          ]);
          fetchSpan.end();
          return result;
        }
      );

      await tracer.startActiveSpan(
        "validatePayment",
        async (paymentSpan) => {
          await paymentService.validate(order.paymentId);
          paymentSpan.end();
        }
      );

      span.setAttribute("order.status", "processed");
      span.end();
      return order;
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: 2, message: (error as Error).message });
      span.end();
      throw error;
    }
  });
}

Para ver traces, necesitas un backend de traces. Para desarrollo, Jaeger es el mas facil de instalar:


# Correr Jaeger localmente con Docker
docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4318:4318 \
  jaegertracing/all-in-one:latest

# Abrir http://localhost:16686 para ver traces

En un cluster Kubernetes, podes deployear Jaeger junto con el OpenTelemetry Collector usando el Jaeger Operator o un chart Helm. El kube-prometheus-stack no incluye tracing out of the box, pero Grafana puede conectarse a Jaeger como data source y mostrar traces junto a tus dashboards de metricas.


El flujo de observabilidad en la practica

Recorramos un escenario realista para ver como los tres pilares trabajan juntos.


Escenario: Los usuarios reportan que crear tareas esta lento.


Paso 1: Chequear el dashboard. Abri tu dashboard RED en Grafana. Notas que la latencia p99 para POST /tasks salto de 100ms a 3 segundos en los ultimos 30 minutos. La tasa de errores sigue baja, asi que los requests estan funcionando pero son lentos.


Paso 2: Acotar con metricas. Agrega una consulta PromQL para chequear si el problema es especifico a un pod o a todos los pods:


histogram_quantile(0.99,
  sum by (pod, le) (
    rate(http_request_duration_seconds_bucket{path="/tasks", method="POST"}[5m])
  )
)

Todos los pods muestran la misma latencia lenta, asi que el problema no es un pod no saludable.


Paso 3: Encontrar un trace lento. Anda a Jaeger (o Grafana Tempo) y busca traces donde la operacion sea POST /tasks y la duracion sea mayor a 2 segundos. Encontras varios traces y abris uno. El trace muestra:


POST /tasks (3.1s)
  |-- Express middleware (2ms)
  |-- insertTask (3.05s)
      |-- pg.query: INSERT INTO tasks... (3.04s)  <-- el problema

El INSERT a la base de datos esta tardando 3 segundos. Eso es anormal.


Paso 4: Chequear los logs. Busca en tus logs errores relacionados con la base de datos en los ultimos 30 minutos:


{
  "level": "warn",
  "message": "Slow query detected",
  "query": "INSERT INTO tasks...",
  "duration_ms": 3041,
  "service": "task-api",
  "connection_pool_active": 19,
  "connection_pool_max": 20
}

El pool de conexiones esta casi lleno. Seguis investigando y encontras que un job de background que corre cada 30 minutos esta manteniendo conexiones abiertas mas de lo esperado. Arreglas el job de background, y la latencia vuelve a la normalidad.


Este es el flujo de observabilidad: alerta o sintoma, dashboard, trace, logs, causa raiz. Cada pilar acoto el problema hasta que encontraste la respuesta.


Basicos de alertas

Los dashboards son utiles para investigacion, pero necesitas alertas para saber cuando algo esta mal antes de que tus usuarios te digan. Prometheus soporta reglas de alerta que evaluan expresiones PromQL y disparan alertas cuando se cumplen las condiciones.


Aca hay un recurso PrometheusRule para una alerta simple:


# alert-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: task-api-alerts
  namespace: monitoring
  labels:
    release: monitoring  # Debe coincidir con el nombre del release kube-prometheus-stack
spec:
  groups:
    - name: task-api
      rules:
        # Alertar cuando la tasa de errores supere 5% por 5 minutos
        - alert: HighErrorRate
          expr: |
            sum(rate(http_requests_total{service="task-api", status=~"5.."}[5m]))
            /
            sum(rate(http_requests_total{service="task-api"}[5m]))
            > 0.05
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Alta tasa de errores en task-api"
            description: >
              La tasa de errores de task-api es {{ $value | humanizePercentage }}
              en los ultimos 5 minutos.

        # Alertar cuando la latencia p99 supere 1 segundo por 10 minutos
        - alert: HighLatency
          expr: |
            histogram_quantile(0.99,
              sum by (le) (rate(http_request_duration_seconds_bucket{service="task-api"}[5m]))
            ) > 1
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Alta latencia p99 en task-api"
            description: >
              La latencia p99 de task-api es {{ $value | humanizeDuration }}
              en los ultimos 5 minutos.

        # Alertar cuando un pod se reinicio mas de 3 veces en una hora
        - alert: PodCrashLooping
          expr: |
            increase(kube_pod_container_status_restarts_total{
              namespace="task-api"
            }[1h]) > 3
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Pod en crash-loop en el namespace task-api"
            description: >
              El pod {{ $labels.pod }} se reinicio {{ $value }} veces
              en la ultima hora.

Aplica la regla y Prometheus la levanta automaticamente:


kubectl apply -f alert-rules.yaml

Alertmanager recibe alertas de Prometheus y las routea al destino correcto: Slack, PagerDuty, email o un webhook. El kube-prometheus-stack incluye Alertmanager. Aca hay una configuracion basica que envia alertas a un canal de Slack:


# En tu monitoring-values.yaml, agregar configuracion de Alertmanager
alertmanager:
  config:
    global:
      slack_api_url: "https://hooks.slack.com/services/TU/SLACK/WEBHOOK"
    route:
      receiver: "slack-notifications"
      group_by: ["alertname", "namespace"]
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h
    receivers:
      - name: "slack-notifications"
        slack_configs:
          - channel: "#alertas"
            send_resolved: true
            title: '{{ .GroupLabels.alertname }}'
            text: >-
              {{ range .Alerts }}
              *{{ .Annotations.summary }}*
              {{ .Annotations.description }}
              {{ end }}

Las configuraciones clave para entender:


  • group_by: Agrupa alertas relacionadas para que recibas una notificacion en vez de cincuenta cuando algo sale mal.
  • group_wait: Cuanto esperar antes de enviar la primera notificacion despues de que se crea un grupo. Da tiempo para que alertas relacionadas lleguen y se agrupen.
  • repeat_interval: Cada cuanto re-enviar una alerta no resuelta. No queres que te pageen cada 30 segundos por el mismo problema.
  • send_resolved: Envia una notificacion cuando la alerta se resuelve. Copado para saber cuando el problema se arreglo sin tener que chequear manualmente.

Conectando los puntos: logs, metricas y traces juntos

El verdadero poder de la observabilidad viene cuando conectas los tres pilares. La clave es el trace ID. Cuando un request entra a tu sistema, recibe un trace ID unico. Si incluis ese trace ID en tus logs y en las labels de tus metricas, podes saltar de una entrada de log al trace correspondiente, o de una alerta a los logs exactos que explican que paso.


Asi se agrega el trace ID a tus logs estructurados:


// src/middleware/traceContext.ts
import { trace, context } from "@opentelemetry/api";
import { Request, Response, NextFunction } from "express";
import logger from "../logger";

export function traceContextMiddleware(
  req: Request,
  _res: Response,
  next: NextFunction
) {
  const span = trace.getSpan(context.active());
  if (span) {
    const spanContext = span.spanContext();
    // Adjuntar trace ID al logger del request para que todos los logs
    // en este request incluyan el trace ID automaticamente
    req.log = logger.child({
      traceId: spanContext.traceId,
      spanId: spanContext.spanId,
    });
  }
  next();
}

Ahora cada entrada de log de un request incluye el trace ID:


{
  "level": "error",
  "message": "Failed to process order",
  "traceId": "abc123def456789",
  "spanId": "def456789abc123",
  "orderId": "12345",
  "service": "task-api"
}

En Grafana, podes configurar un data link desde tu panel de logs (Loki) a tu panel de traces (Jaeger o Tempo). Hacele click a una entrada de log y saltas directamente al trace. Esta es la funcionalidad mas util para debuggear problemas de produccion.


Que observar: una checklist para empezar

Cuando recien empezas, es facil sentirse abrumado por la cantidad de cosas que podrias medir. Aca hay un punto de partida practico:


  • Para cada endpoint de API: Tasa de requests, tasa de errores y latencia (el metodo RED). Estas tres metricas cubren la mayoria de los problemas.
  • Para tu infraestructura: Uso de CPU, uso de memoria, uso de disco y I/O de red por pod. El kube-prometheus-stack te da esto gratis.
  • Para tu base de datos: Conexiones activas, duracion de queries y utilizacion del pool de conexiones. Estas son la fuente mas comun de problemas de rendimiento de aplicaciones.
  • Para la salud de tu aplicacion: Reinicios de pods, estado de replicas del deployment y readiness de containers. Esto te dice si Kubernetes esta luchando para mantener tu app corriendo.

Empeza con esto y agrega mas metricas a medida que encuentres problemas especificos. No trates de medir todo el primer dia.


Temas avanzados

Cubrimos lo esencial en este articulo, pero la observabilidad va mucho mas profundo. Aca hay temas que vale la pena explorar una vez que estes comodo con lo basico:


  • Alertas basadas en SLOs: En vez de alertar por umbrales crudos (“latencia > 1s”), defini Service Level Objectives y alerta por tasa de quema de error budget. Esto evita alertas ruidosas y se enfoca en lo que le importa a los usuarios.
  • Agregacion de logs con Loki: Loki es el equivalente de logging de Prometheus. Indexa metadata de logs (labels) y almacena el contenido comprimido, haciendolo mucho mas barato que Elasticsearch para logging en Kubernetes.
  • Tracing distribuido a escala con Tempo: Grafana Tempo es un backend de traces disenado para funcionar sin fricciones con Grafana, Loki y Prometheus. Soporta correlacion trace-to-log y trace-to-metric out of the box.
  • Testing basado en traces: Usa traces para verificar que tus servicios se comunican correctamente en tests de integracion. Herramientas como Tracetest te permiten escribir assertions sobre datos de traces.
  • Metricas custom para logica de negocio: Trackea cosas como ordenes procesadas, revenue por minuto o signups de usuarios. Estas metricas de negocio son frecuentemente mas valiosas que las metricas tecnicas.

Para un deep dive completo en todos estos temas, mira el Deep Dive de Observabilidad SRE. Cubre patrones de instrumentacion con OpenTelemetry, setup de Loki, Grafana Tempo, alertas basadas en SLOs con Pyrra, y arquitecturas de observabilidad de nivel produccion.


Notas finales

La observabilidad no es opcional. Una vez que tu aplicacion esta corriendo en produccion, necesitas saber que esta haciendo, como esta rindiendo, y cuando algo sale mal. Los tres pilares (logs, metricas y traces) te dan vistas complementarias del comportamiento de tu sistema.


En este articulo cubrimos que es la observabilidad y por que importa, los tres pilares y cuando usar cada uno, logging estructurado con pino, metricas de Prometheus con prom-client, instalacion de Prometheus y Grafana con el kube-prometheus-stack, consultas PromQL basicas para escenarios comunes, armado de dashboards en Grafana, tracing distribuido con OpenTelemetry, alertas con PrometheusRule y Alertmanager, y el flujo de observabilidad para debuggear problemas de produccion.


El takeaway clave es que la observabilidad es un flujo de trabajo, no una herramienta. No solo instalas Prometheus y lo das por terminado. Instrumentas tu aplicacion, armas dashboards que responden preguntas reales, configuras alertas que te notifican antes que tus usuarios, y practicas el flujo alerta-dashboard-trace-log hasta que se vuelva segunda naturaleza.


En el proximo articulo vamos a cubrir pipelines de CI/CD para Kubernetes, juntando todo lo que construimos hasta ahora en un flujo de deployment automatizado.


Espero que te haya resultado util y lo hayas disfrutado! Hasta la proxima!


Errata

Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.

Tambien podes revisar el codigo fuente y los cambios en las fuentes aca


$ Comentarios

Online: 0

Por favor inicie sesión para poder escribir comentarios.

2026-06-02 | Gabriel Garrido