Stack Completo de Observabilidad

Guía completa para implementar un sistema de observabilidad moderno con Prometheus, Grafana, Loki, Tempo, Jaeger y OpenTelemetry en entornos Kubernetes y cloud

¿Alguna vez te ha pasado que una aplicación empieza a fallar en producción y tu única respuesta es entrar a un cluster de Kubernetes a ciegas, haciendo kubectl logs de forma desesperada y rezando para que el error aparezca? Si dependes de la suerte para diagnosticar incidentes, no tienes observabilidad, tienes un juego de azar.

Para escalar sistemas cloud-native, no basta con saber que «algo va mal». Necesitas saber qué va mal, dónde está el cuello de botella y cómo afecta eso al usuario final. Un sistema moderno de observabilidad te permite pasar del caos de la sospecha a la precisión del dato, integrando métricas, logs y traces en un solo flujo de trabajo. En esta guía, vamos a construir un stack profesional completo para que dejes de adivinar y empieces a observar.

📋 Resumen

Esta guía cubre la implementación completa de un stack de observabilidad moderno que incluye métricas con Prometheus, visualización con Grafana, logs centralizados con Loki, tracing distribuido con Jaeger/OpenTelemetry, y alerting avanzado con Alertmanager. Aprenderás desde la instalación básica hasta configuraciones de producción, dashboards personalizados, y estrategias de troubleshooting.

🎯 Audiencia

  • DevOps/SRE engineers
  • Administradores de sistemas cloud-native
  • Desarrolladores que necesitan monitoreo en producción
  • Equipos de operaciones que requieren alerting y troubleshooting avanzado

📚 Prerrequisitos

  • Conocimientos básicos de Kubernetes
  • Familiaridad con Docker y contenedores
  • Entendimiento básico de métricas, logs y tracing
  • Cluster Kubernetes funcionando (minikube, k3s, EKS, GKE, AKS, o self-hosted)

🏗️ Arquitectura Completa del Stack

Antes de tirar líneas de comando, echemos un vistazo al mapa de carreteras. La clave no es tener herramientas aisladas, sino un ecosistema donde los datos fluyan.

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Aplicaciones  │───▶│   OpenTelemetry │───▶│     Jaeger      │
│   (Instrumented)│    │   Collector     │    │  (Tracing)      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Prometheus    │    │      Loki       │    │    Grafana      │
│   (Métricas)    │    │    (Logs)       │    │ (Visualización) │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 ▼
                   ┌─────────────────┐
                   │  Alertmanager  │
                   │   (Alerting)   │
                   └─────────────────┘

Componentes del Stack

  • Prometheus: Recolección y almacenamiento de métricas time-series
  • Grafana: Visualización, dashboards y alerting
  • Loki: Logs centralizados y búsqueda eficiente
  • Jaeger: Tracing distribuido y análisis de latencia
  • OpenTelemetry: Estándar para instrumentación de aplicaciones
  • Alertmanager: Gestión y enrutamiento de alertas
  • Node Exporter: Métricas del sistema operativo
  • cAdvisor/Kubelet: Métricas de contenedores y Kubernetes

🚀 Instalación en Kubernetes con Helm

Si estás trabajando en Kubernetes, no reinventes la rueda. Helm es nuestro mejor aliado para desplegar estos gigantes de forma estandarizada.

Preparar el entorno

Empezamos añadiendo los repositorios necesarios y creando un espacio limpio para nuestro stack.

# Añadir repositorios de Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update

# Crear namespace para observabilidad
kubectl create namespace observability

Instalar Prometheus Stack completo

El kube-prometheus-stack es el estándar de oro porque ya trae Prometheus, Grafana y Alertmanager configurados para trabajar juntos.

# Instalar kube-prometheus-stack (Prometheus + Grafana + Alertmanager)
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace observability \
  --set grafana.enabled=true \
  --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \
  --set prometheus.prometheusSpec.ruleSelectorNilUsesHelmValues=false \
  --set prometheus.prometheusSpec.retention=30d \
  --set prometheus.prometheusSpec.retentionSize="50GB" \
  --wait

Instalar Loki para logs centralizados

Para los logs, Loki es la opción inteligente: es como Prometheus pero para texto, lo que lo hace extremadamente eficiente y fácil de correlacionar con tus métricas.

# Instalar Loki Stack (Loki + Promtail + Grafana datasource)
helm install loki grafana/loki-stack \
  --namespace observability \
  --set grafana.enabled=true \
  --set prometheus.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.size=50Gi \
  --set loki.config.limits_config.retention_period=168h \
  --wait

Instalar Jaeger para tracing distribuido

Cuando una petición pasa por cinco microservicios y tarda 10 segundos, necesitas saber exactamente quién es el culpable. Jaeger te da esa respuesta.

# Instalar Jaeger completo con Cassandra
helm install jaeger jaegertracing/jaeger \
  --namespace observability \
  --set cassandra.config.max_heap_size=1024M \
  --set cassandra.config.heap_new_size=256M \
  --set storage.type=cassandra \
  --wait

Instalar OpenTelemetry Collector

El Collector actúa como el «hub» central de telemetría, recibiendo datos de cualquier fuente y enviándolos al destino correcto.

# otel-collector-values.yaml
mode: deployment
image:
  repository: otel/opentelemetry-collector-contrib

config:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318
    prometheus:
      config:
        scrape_configs:
          - job_name: 'otel-collector'
            static_configs:
              - targets: ['localhost:8888']

  processors:
    batch:
      timeout: 1s
      send_batch_size: 1024
    memory_limiter:
      limit_mib: 512
      spike_limit_mib: 128

  exporters:
    jaeger:
      endpoint: jaeger-collector:14268
      tls:
        insecure: true
    prometheus:
      endpoint: "prometheus-kube-prometheus-prometheus:9090"
    loki:
      endpoint: "http://loki:3100/loki/api/v1/push"

  service:
    pipelines:
      traces:
        receivers: [otlp]
        processors: [memory_limiter, batch]
        exporters: [jaeger]
      metrics:
        receivers: [prometheus]
        processors: [memory_limiter, batch]
        exporters: [prometheus]
      logs:
        receivers: [otlp]
        processors: [memory_limiter, batch]
        exporters: [loki]

# Instalar
helm install opentelemetry-collector open-telemetry/opentelemetry-collector \
  --namespace observability \
  -f otel-collector-values.yaml \
  --wait

📊 Exposición de Servicios

Una vez desplegado todo, necesitamos una forma de interactuar con las interfaces. Usaremos port-forward para acceder desde nuestro entorno local.

# Exponer Prometheus
kubectl port-forward -n observability svc/prometheus-kube-prometheus-prometheus 9090:9090

# Exponer Grafana
kubectl port-forward -n observability svc/prometheus-grafana 3000:3000

# Exponer Jaeger UI
kubectl port-forward -n observability svc/jaeger-query 16686:16686

# Exponer Loki (opcional)
kubectl port-forward -n observability svc/loki 3100:3100

Acceso inicial a Grafana

Para entrar en el centro de mando, primero necesitamos la contraseña generada automáticamente.

# Obtener contraseña de admin
kubectl get secret -n observability prometheus-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

# Usuario: admin
# URL: http://localhost:3000

🎨 Dashboards Personalizados en Grafana

Tener los datos es inútil si no puedes leerlos. Grafana es donde la magia ocurre. Aquí tienes tres plantillas esenciales para cualquier entorno Kubernetes.

Dashboard de Nodes de Kubernetes

Para vigilar la salud de tu infraestructura física o virtual.

{
  "dashboard": {
    "title": "Kubernetes Nodes - Custom",
    "tags": ["kubernetes", "nodes", "custom"],
    "panels": [
      {
        "title": "CPU Usage by Node",
        "type": "bargauge",
        "targets": [
          {
            "expr": "100 - (avg by (instance) (irate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
            "legendFormat": "{{instance}}"
          }
        ]
      },
      {
        "title": "Memory Usage by Node",
        "type": "bargauge",
        "targets": [
          {
            "expr": "(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100",
            "legendFormat": "{{instance}}"
          }
        ]
      },
      {
        "title": "Network I/O",
        "type": "graph",
        "targets": [
          {
            "expr": "rate(node_network_receive_bytes_total[5m])",
            "legendFormat": "RX {{instance}}"
          },
          {
            "expr": "rate(node_network_transmit_bytes_total[5m])",
            "legendFormat": "TX {{instance}}"
          }
        ]
      },
      {
        "title": "Disk I/O",
        "type": "graph",
        "targets": [
          {
            "expr": "rate(node_disk_reads_completed_total[5m])",
            "legendFormat": "Reads {{instance}}"
          },
          {
            "expr": "rate(node_disk_writes_completed_total[5m])",
            "legendFormat": "Writes {{instance}}"
          }
        ]
      }
    ]
  }
}

Dashboard de Pods y Contenedores

Vital para identificar qué aplicación específica está consumiendo todos los recursos del cluster.

{
  "dashboard": {
    "title": "Kubernetes Pods - Custom",
    "tags": ["kubernetes", "pods", "containers"],
    "panels": [
      {
        "title": "Pod CPU Usage",
        "type": "table",
        "targets": [
          {
            "expr": "sum(rate(container_cpu_usage_seconds_total{container!=\"\"}[5m])) by (pod, namespace)",
            "legendFormat": "{{pod}}"
          }
        ],
        "fieldConfig": {
          "overrides": [
            {
              "matcher": { "id": "byName", "options": "Time" },
              "properties": [{ "id": "custom.hidden", "value": true }]
            }
          ]
        }
      },
      {
        "title": "Pod Memory Usage",
        "type": "table",
        "targets": [
          {
            "expr": "sum(container_memory_usage_bytes{container!=\"\"}) by (pod, namespace) / 1024 / 1024",
            "legendFormat": "{{pod}}"
          }
        ]
      },
      {
        "title": "Container Restarts",
        "type": "table",
        "targets": [
          {
            "expr": "kube_pod_container_status_restarts_total",
            "legendFormat": "{{pod}}"
          }
        ]
      }
    ]
  }
}

Dashboard de Ingress y Services

Para monitorizar el tráfico que entra a tu cluster y la latencia de respuesta percibida por el usuario.

{
  "dashboard": {
    "title": "Kubernetes Ingress & Services",
    "tags": ["kubernetes", "ingress", "services"],
    "panels": [
      {
        "title": "HTTP Requests by Ingress",
        "type": "graph",
        "targets": [
          {
            "expr": "sum(rate(nginx_ingress_controller_requests{ingress!=\"\"}[5m])) by (ingress, status)",
            "legendFormat": "{{ingress}} - {{status}}"
          }
        ]
      },
      {
        "title": "Response Time by Ingress",
        "type": "graph",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(nginx_ingress_controller_request_duration_seconds_bucket[5m]))",
            "legendFormat": "p95 {{ingress}}"
          }
        ]
      },
      {
        "title": "Active Connections",
        "type": "singlestat",
        "targets": [
          {
            "expr": "nginx_ingress_controller_nginx_process_connections_total{state=\"active\"}",
            "legendFormat": "Active"
          }
        ]
      }
    ]
  }
}

🕵️ OpenTelemetry y Tracing Distribuido

La instrumentación es el paso donde tus aplicaciones empiezan a «hablar» con el stack de observabilidad. Gracias a OpenTelemetry, no estás atado a un solo proveedor.

Instrumentación de Aplicaciones

Python con OpenTelemetry

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor

# Configurar tracing
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

# Configurar exportador OTLP
otlp_exporter = OTLPSpanExporter(
    endpoint="otel-collector.observability.svc.cluster.local:4317",
    insecure=True
)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

# Auto-instrumentación para frameworks populares
FlaskInstrumentor().instrument()
RequestsInstrumentor().instrument()

# Instrumentación manual
@app.route('/api/users')
def get_users():
    with tracer.start_as_current_span("get_users") as span:
        span.set_attribute("operation.name", "database_query")
        span.set_attribute("db.table", "users")

        # Tu lógica de negocio aquí
        users = db.query("SELECT * FROM users")

        span.set_attribute("db.rows_returned", len(users))
        return jsonify(users)

JavaScript/Node.js con OpenTelemetry

const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-otlp-grpc');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');

const provider = new NodeTracerProvider();
const exporter = new OTLPTraceExporter({
  url: 'http://otel-collector.observability.svc.cluster.local:4318/v1/traces'
});

provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();

// Auto-instrumentación
provider.addSpanProcessor(new ExpressInstrumentation());
provider.addSpanProcessor(new HttpInstrumentation());

// Instrumentación manual
app.get('/api/users', (req, res) => {
  const span = trace.getTracer('my-service').startSpan('get_users');
  span.setAttribute('operation.name', 'database_query');

  // Tu lógica aquí
  db.query('SELECT * FROM users', (err, results) => {
    span.setAttribute('db.rows_returned', results.length);
    span.end();
    res.json(results);
  });
});

Java con OpenTelemetry

import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;

public class MyService {
    private static final Tracer tracer = GlobalOpenTelemetry.getTracer("my-service");

    public void processRequest() {
        Span span = tracer.spanBuilder("process_request").startSpan();

        try (Scope scope = span.makeCurrent()) {
            span.setAttribute("operation.name", "business_logic");

            // Tu lógica de negocio
            doBusinessLogic();

            span.setAttribute("result", "success");
        } catch (Exception e) {
            span.setAttribute("error", true);
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

Configuración del Collector OpenTelemetry

El secreto de un buen Collector es el enriquecimiento de datos mediante el uso de procesadores.

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  # Receivers adicionales para métricas del sistema
  prometheus:
    config:
      scrape_configs:
        - job_name: 'otel-collector'
          static_configs:
            - targets: ['localhost:8888']

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  memory_limiter:
    limit_mib: 512
    spike_limit_mib: 128

  # Procesador para añadir atributos comunes
  resource:
    attributes:
      - key: service.namespace
        value: "production"
        action: insert
      - key: k8s.cluster.name
        value: "my-cluster"
        action: insert

exporters:
  jaeger:
    endpoint: jaeger-collector.observability.svc.cluster.local:14268
    tls:
      insecure: true
  prometheus:
    endpoint: "prometheus-kube-prometheus-prometheus.observability.svc.cluster.local:9090"
  loki:
    endpoint: "http://loki.observability.svc.cluster.local:3100/loki/api/v1/push"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [jaeger]
    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, resource, batch]
      exporters: [prometheus]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [loki]

🔔 Alerting Avanzado con Alertmanager

Tener dashboards es genial, pero el objetivo real es que el sistema te avise antes de que el usuario te llame para quejarse.

Reglas de Alerting para Kubernetes

{% raw %}

# alert-rules.yaml
groups:
- name: kubernetes-apps
  rules:
  - alert: HighPodRestartRate
    expr: rate(kube_pod_container_status_restarts_total[15m]) > 0.1
    for: 10m
    labels:
      severity: warning
      team: platform
    annotations:
      summary: "High pod restart rate"
      description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} has restarted {{ $value }} times in the last 15 minutes."
      runbook_url: "https://docs.frikiteam.es/doc/monitoring/troubleshooting#pod-restarts"

  - alert: PodNotReady
    expr: kube_pod_status_ready{condition="false"} == 1
    for: 5m
    labels:
      severity: critical
      team: platform
    annotations:
      summary: "Pod not ready"
      description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is not ready for 5+ minutes."

  - alert: HighMemoryUsage
    expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
    for: 5m
    labels:
      severity: warning
      team: infrastructure
    annotations:
      summary: "High memory usage on {{ $labels.instance }}"
      description: "Memory usage is {{ $value }}% on {{ $labels.instance }}."

  - alert: HighCPUUsage
    expr: 100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
    for: 5m
    labels:
      severity: warning
      team: infrastructure
    annotations:
      summary: "High CPU usage on {{ $labels.instance }}"
      description: "CPU usage is {{ $value }}% on {{ $labels.instance }}."

- name: application-metrics
  rules:
  - alert: HighErrorRate
    expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100 > 5
    for: 5m
    labels:
      severity: critical
      team: backend
    annotations:
      summary: "High error rate on {{ $labels.service }}"
      description: "Error rate is {{ $value }}% for service {{ $labels.service }}."

  - alert: SlowResponseTime
    expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2
    for: 5m
    labels:
      severity: warning
      team: backend
    annotations:
      summary: "Slow response time on {{ $labels.service }}"
      description: "95th percentile response time is {{ $value }}s for service {{ $labels.service }}."

Configuración Avanzada de Alertmanager

No queremos que un error de red sature nuestro Slack. Alertmanager nos permite enrutar, agrupar e inhibir alertas para evitar la fatiga de alertas.

{% raw %}

# alertmanager-config.yaml
global:
  smtp_smarthost: 'smtp.gmail.com:587'
  smtp_from: '[email protected]'
  smtp_auth_username: '[email protected]'
  smtp_auth_password: 'your-app-password'

# Plantillas de notificaciones
templates:
  - '/etc/alertmanager/templates/*.tmpl'

route:
  group_by: ['alertname', 'team']
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 1h
  receiver: 'default'
  routes:
  - match:
      severity: critical
    receiver: 'critical-alerts'
    continue: true
  - match:
      team: platform
    receiver: 'platform-team'
  - match:
      team: infrastructure
    receiver: 'infra-team'
  - match:
      team: backend
    receiver: 'backend-team'

receivers:
- name: 'default'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
    channel: '#alerts'
    title: '{{ .GroupLabels.alertname }}'
    text: '{{ .CommonAnnotations.description }}'
    color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'

- name: 'critical-alerts'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/YOUR/CRITICAL/WEBHOOK'
    channel: '#critical-alerts'
    title: '🚨 CRITICAL: {{ .GroupLabels.alertname }}'
    text: '{{ .CommonAnnotations.description }}'
    color: 'danger'
  pagerduty_configs:
  - service_key: 'your-pagerduty-integration-key'

- name: 'platform-team'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/PLATFORM/WEBHOOK'
    channel: '#platform-alerts'

- name: 'infra-team'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/INFRA/WEBHOOK'
    channel: '#infra-alerts'
  email_configs:
  - to: '[email protected]'
    subject: 'Infrastructure Alert: {{ .GroupLabels.alertname }}'
    body: '{{ .CommonAnnotations.description }}'

- name: 'backend-team'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/BACKEND/WEBHOOK'
    channel: '#backend-alerts'
  email_configs:
  - to: '[email protected]'

# Inhibiciones para evitar alertas duplicadas
inhibit_rules:
  - source_match:
      alertname: 'NodeDown'
    target_match:
      alertname: 'PodNotReady|HighMemoryUsage|HighCPUUsage'
    equal: ['instance']

💾 Monitoreo de Storage (Ceph, Pure Storage, NetApp)

Un cluster de Kubernetes es tan fuerte como el storage que lo soporta. No ignores las métricas de tus volúmenes.

Métricas de Ceph

# ceph-exporter-service-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: ceph-mgr
  namespace: observability
spec:
  selector:
    matchLabels:
      app: ceph-mgr
  endpoints:
  - port: metrics
    path: /metrics
    interval: 30s
    scrapeTimeout: 10s

Dashboard de Ceph en Grafana

{
  "dashboard": {
    "title": "Ceph Cluster Overview",
    "tags": ["ceph", "storage"],
    "panels": [
      {
        "title": "Cluster Health",
        "type": "stat",
        "targets": [
          {
            "expr": "ceph_health_status",
            "legendFormat": "Health"
          }
        ]
      },
      {
        "title": "OSD Usage",
        "type": "bargauge",
        "targets": [
          {
            "expr": "ceph_osd_stat_bytes_used / ceph_osd_stat_bytes",
            "legendFormat": "{{osd}}"
          }
        ]
      },
      {
        "title": "Pool Usage",
        "type": "table",
        "targets": [
          {
            "expr": "ceph_pool_bytes_used / ceph_pool_max_avail * 100",
            "legendFormat": "{{pool}}"
          }
        ]
      },
      {
        "title": "IOPS by Pool",
        "type": "graph",
        "targets": [
          {
            "expr": "rate(ceph_pool_rd[5m]) + rate(ceph_pool_wr[5m])",
            "legendFormat": "{{pool}}"
          }
        ]
      }
    ]
  }
}

Métricas de Pure Storage

# pure-storage-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: pure-storage-exporter
  namespace: observability
spec:
  selector:
    matchLabels:
      app: pure-storage-exporter
  endpoints:
  - port: metrics
    interval: 60s

Métricas de NetApp

# Consultas PromQL para NetApp
# Usage por volumen
netapp_volume_size_used / netapp_volume_size_total * 100

# IOPS por LUN
rate(netapp_lun_read_ops[5m]) + rate(netapp_lun_write_ops[5m])

# Latencia de storage
netapp_lun_avg_read_latency + netapp_lun_avg_write_latency

🌐 Monitoreo de Networking

Métricas de Red Básicas

# Bandwidth por interface
rate(node_network_receive_bytes_total[5m]) * 8 / 1000000

# Latencia de red (si tienes blackbox exporter)
probe_duration_seconds

# Errores de red
rate(node_network_receive_errs_total[5m])
rate(node_network_transmit_errs_total[5m])

# Conexiones TCP
node_netstat_Tcp_CurrEstab
node_netstat_Tcp_ActiveOpens

Dashboard de Networking

{
  "dashboard": {
    "title": "Network Monitoring",
    "tags": ["networking", "infrastructure"],
    "panels": [
      {
        "title": "Network Traffic by Interface",
        "type": "graph",
        "targets": [
          {
            "expr": "rate(node_network_receive_bytes_total[5m]) * 8 / 1000000",
            "legendFormat": "RX {{device}}"
          },
          {
            "expr": "rate(node_network_transmit_bytes_total[5m]) * 8 / 1000000",
            "legendFormat": "TX {{device}}"
          }
        ]
      },
      {
        "title": "Network Errors",
        "type": "table",
        "targets": [
          {
            "expr": "rate(node_network_receive_errs_total[5m])",
            "legendFormat": "RX Errors {{device}}"
          },
          {
            "expr": "rate(node_network_transmit_errs_total[5m])",
            "legendFormat": "TX Errors {{device}}"
          }
        ]
      },
      {
        "title": "TCP Connections",
        "type": "singlestat",
        "targets": [
          {
            "expr": "node_netstat_Tcp_CurrEstab",
            "legendFormat": "Established"
          }
        ]
      }
    ]
  }
}

🔧 Troubleshooting con Jaeger

Cuando la métrica te dice que hay latencia, pero el log no muestra errores, Jaeger es tu mejor amigo.

Configuración de Jaeger para Producción

En producción, no puedes usar el modo in-memory si quieres que los datos sobrevivan a un reinicio.

# jaeger-production-values.yaml
collector:
  enabled: true
  service:
    annotations:
      prometheus.io/scrape: "true"
      prometheus.io/port: "14268"

storage:
  type: cassandra
  cassandra:
    host: jaeger-cassandra
    keyspace: jaeger_v1_dc1
    # Para producción, considera Elasticsearch:
    # type: elasticsearch
    # elasticsearch:
    #   host: elasticsearch-master
    #   indexPrefix: jaeger

query:
  enabled: true
  service:
    type: ClusterIP

# Configuración de sampling
collector:
  otlp:
    enabled: true
  sampling:
    strategies:
      probabilistic:
        samplingRate: 0.1  # 10% de traces
      rateLimiting:
        maxTracesPerSecond: 100

Análisis de Traces en Jaeger

Puedes usar la CLI para buscar datos rápidamente sin siquiera abrir la UI.

# Buscar traces por servicio
curl -G "http://jaeger-query:16686/api/traces" \
  --data-urlencode "service=my-service" \
  --data-urlencode "limit=20"

# Buscar traces con errores
curl -G "http://jaeger-query:16686/api/traces" \
  --data-urlencode "service=my-service" \
  --data-urlencode "tags={\"error\":\"true\"}"

# Buscar traces por duración
curl -G "http://jaeger-query:16686/api/traces" \
  --data-urlencode "service=my-service" \
  --data-urlencode "maxDuration=5s"

Consultas TraceQL en Grafana

Si usas el plugin de Jaeger en Grafana, puedes usar TraceQL para ser quirúrgico.

# Traces lentas
{ duration > 5s }

# Traces con errores
{ status = error }

# Traces por servicio y operación
{ service.name = "my-service" && name = "http_request" }

# Traces con tags específicos
{ resource.service.name = "api-gateway" && span.http.status_code = "500" }

🚀 Configuración de Producción

Pasar de un laboratorio a producción requiere un cambio de mentalidad: redundancia y persistencia son obligatorias.

Alta Disponibilidad

# prometheus-ha-values.yaml
prometheus:
  prometheusSpec:
    replicas: 2
    retention: 90d
    retentionSize: "200GB"
    ruleSelector:
      matchLabels:
        prometheus: kube-prometheus
    serviceMonitorSelector:
      matchLabels:
        prometheus: kube-prometheus
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: fast-ssd
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 100Gi
    securityContext:
      runAsUser: 65534
      runAsNonRoot: true
      runAsGroup: 65534
      fsGroup: 65534

alertmanager:
  alertmanagerSpec:
    replicas: 2
    storage:
      volumeClaimTemplate:
        spec:
          storageClassName: fast-ssd
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 10Gi

Backup y Recuperación

No confíes en que el storage de Kubernetes sea eterno. Haz backups.

# Backup de Prometheus
kubectl exec -n observability prometheus-prometheus-0 -- tar czf /tmp/prometheus-backup.tar.gz -C /prometheus .

# Backup de Loki
kubectl exec -n observability loki-0 -- tar czf /tmp/loki-backup.tar.gz -C /loki .

# Backup de Grafana
kubectl exec -n observability prometheus-grafana-0 -- tar czf /tmp/grafana-backup.tar.gz -C /var/lib/grafana .

# Copiar backups a storage seguro
kubectl cp observability/prometheus-prometheus-0:tmp/prometheus-backup.tar.gz ./backups/
kubectl cp observability/loki-0:tmp/loki-backup.tar.gz ./backups/
kubectl cp observability/prometheus-grafana-0:tmp/grafana-backup.tar.gz ./backups/

Monitoreo de Costos

La observabilidad también sirve para que el equipo de finanzas no te llame por la factura de la nube.

# Costo estimado por namespace (AWS ejemplo)
sum(rate(container_cpu_usage_seconds_total[1h])) by (namespace) * 0.000024 * 730 +
sum(container_memory_usage_bytes / 1024 / 1024 / 1024) by (namespace) * 0.000018 * 730

# Costo de storage
sum(kube_persistentvolumeclaim_resource_requests_storage_bytes) by (namespace) / 1024 / 1024 / 1024 * 0.12

📊 SLOs y SLIs

No te limites a medir si el pod está vivo. Mide si estás cumpliendo tus promesas con el cliente.

Definición de SLOs

# slos.yaml
- name: api-availability
  objective: 99.9
  sli:
    events:
      good:
        metric: http_requests_total{status!~"5.."}
      total:
        metric: http_requests_total

- name: api-latency
  objective: 99
  sli:
    events:
      good:
        metric: http_request_duration_seconds{quantile="0.95"} < 0.1
      total:
        metric: http_request_duration_seconds_count

- name: storage-availability
  objective: 99.99
  sli:
    events:
      good:
        metric: ceph_health_status == 0
      total:
        metric: up{job="ceph-mgr"}

Dashboard de SLOs

{
  "dashboard": {
    "title": "Service Level Objectives",
    "tags": ["slo", "reliability"],
    "panels": [
      {
        "title": "API Availability",
        "type": "stat",
        "targets": [
          {
            "expr": "1 - (rate(http_requests_total{status=~\"5..\"}[30d]) / rate(http_requests_total[30d]))",
            "legendFormat": "Availability"
          }
        ]
      },
      {
        "title": "API Latency p95",
        "type": "stat",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[30d]))",
            "legendFormat": "p95 Latency"
          }
        ]
      }
    ]
  }
}

🛠️ Comandos Útiles

Guarda esta sección para cuando el incidente esté ocurriendo y no quieras buscar en Google.

# Ver estado de componentes
kubectl get pods -n observability
kubectl get svc -n observability

# Logs de troubleshooting
kubectl logs -n observability -l app=prometheus --tail=100
kubectl logs -n observability -l app.kubernetes.io/name=grafana --tail=100

# Reiniciar componentes
kubectl rollout restart deployment/prometheus-grafana -n observability
kubectl rollout restart statefulset/prometheus-prometheus -n observability

# Ver alertas activas
kubectl port-forward -n observability svc/prometheus-kube-prometheus-alertmanager 9093:9093
# Luego: http://localhost:9093

# Backup de configuraciones
kubectl get configmap -n observability -o yaml > observability-configs.yaml

# Ver métricas de Prometheus
kubectl port-forward -n observability svc/prometheus-kube-prometheus-prometheus 9090:9090
# Luego: http://localhost:9090

# Consultas directas a Loki
kubectl port-forward -n observability svc/loki 3100:3100
curl "http://localhost:3100/loki/api/v1/query_range?query={namespace=\"default\"}&start=1640995200&end=1640998800&step=60"

# Ver traces en Jaeger
kubectl port-forward -n observability svc/jaeger-query 16686:16686
# Luego: http://localhost:16686

# Monitoreo de recursos
kubectl top nodes
kubectl top pods -n observability

# Ver eventos de Kubernetes
kubectl get events -n observability --sort-by=.metadata.creationTimestamp

# Debug de ServiceMonitors
kubectl get servicemonitor -n observability
kubectl describe servicemonitor prometheus-kube-prometheus -n observability

📚 Recursos Adicionales

Documentación Oficial

Dashboards y Configuraciones

Comunidad y Soporte

Libros y Cursos

✅ Checklist Completo de Implementación

Instalación Base

  • [x] Namespace observability creado
  • [x] Prometheus instalado con Helm
  • [x] Grafana instalado con Helm
  • [x] Loki instalado con Helm
  • [x] Jaeger instalado con Helm
  • [x] OpenTelemetry Collector configurado

Configuración

  • [x] Servicios expuestos correctamente
  • [x] Data sources configurados en Grafana
  • [x] Reglas de alerting definidas
  • [x] Dashboards básicos importados

Monitoreo Avanzado

  • [x] Dashboards personalizados para Kubernetes
  • [x] Instrumentación OpenTelemetry en aplicaciones
  • [x] Alerting con enrutamiento por equipos
  • [x] Monitoreo de storage (Ceph/Pure/NetApp)
  • [x] Monitoreo de networking
  • [x] SLOs y SLIs definidos

Producción

  • [x] Alta disponibilidad configurada
  • [x] Persistencia de datos habilitada
  • [x] Backups automáticos configurados
  • [x] Seguridad (TLS, RBAC) implementada
  • [x] Retención de datos optimizada
  • [x] Monitoreo de costos habilitado

Troubleshooting

  • [x] Runbooks documentados
  • [x] Alertas con contexto suficiente
  • [x] Tracing distribuido operativo
  • [x] Logs centralizados funcionando
  • [x] Métricas de rendimiento recopiladas

Implementar este stack no es una tarea de una sola tarde, pero es la inversión más rentable que puedes hacer por tu infraestructura. Un sistema de observabilidad bien configurado es la diferencia entre dormir tranquilo y pasar la noche entera pegado a una pantalla. ¡Manos a la obra!

Suscríbete al blog por correo electrónico

Introduce tu correo electrónico para suscribirte a este blog y recibir avisos de nuevas entradas.

Deja un comentario