cd ../blog
prometheus-y-grafana-la-dupla-perfecta-para-monitoreo-y-obse.md

$ cat prometheus-y-grafana-la-dupla-perfecta-para-monitoreo-y-obse.md

Prometheus y Grafana: La Dupla Perfecta para Monitoreo y Observabilidad

5 de abril de 2026
monitoreoobservabilidadalertas

¿Qué es Prometheus?

flowchart LR subgraph Apps["📊 Aplicaciones a Monitorear"] App1["App Node.js"] App2["App Go"] App3["MySQL"] App4["Nginx"] end subgraph Prom["🔍 Prometheus"] TSDB[(TSDB\nBase de datos\ntemporal)] Scrape["Scrape (Pull)"] Alert["AlertManager"] Rules["Reglas de\nAlerta"] end subgraph Viz["📈 Visualización"] Grafana["Grafana\nDashboards"] API["API HTTP"] end Apps -->|"/metrics"| Scrape Scrape --> TSDB Rules --> Alert Alert -->|Alertas| Grafana TSDB -->|Consulta| Grafana TSDB -->|Consulta| API note1["✅ Prometheus extrae métricas (pull)\n❌ No espera a que le envíen datos"]

Características Clave

| Característica | Descripción | |----------------|-------------| | Modelo Pull | Prometheus "extrae" métricas de los targets cada cierto tiempo | | TSDB | Base de datos temporal optimizada para series de tiempo | | PromQL | Lenguaje potente para consultar y agregar métricas | | Multi-dimensional | Métricas con etiquetas (labels) para filtrado avanzado | | AlertManager | Sistema de alertas integrado (Slack, Email, PagerDuty) | | Exporters | Adaptadores para servicios que no exponen métricas nativamente |

¿Qué es Grafana?

flowchart TD subgraph Sources["📡 Fuentes de Datos"] Prometheus Loki["Loki (Logs)"] Tempo["Tempo (Traces)"] MySQL PostgreSQL end subgraph Grafana["🎨 Grafana"] Dashboard["Dashboards\nInteractivos"] Explore["Explore\nConsulta ad-hoc"] Alert["Alertas\nVisuales"] Plugins["Plugins\n100+ fuentes"] end subgraph Users["👥 Usuarios"] Devs["Desarrolladores"] Ops["Operaciones"] Managers["Gerencia"] end Sources --> Grafana Grafana --> Users note1["🎯 Un único panel para\nmétricas, logs y traces"]

Instalación Rápida con Docker Compose

docker-compose.yml

version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_INSTALL_PLUGINS=grafana-piechart-panel
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    depends_on:
      - prometheus
    restart: unless-stopped

  # Ejemplo: Aplicación Node.js a monitorear
  node-app:
    build: ./node-app
    ports:
      - "3001:3000"
    labels:
      - "prometheus-job=node-app"
    restart: unless-stopped

  # Ejemplo: PostgreSQL con exporter
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    
  postgres-exporter:
    image: prometheuscommunity/postgres-exporter:latest
    environment:
      DATA_SOURCE_NAME: "postgresql://postgres:secret@postgres:5432/postgres?sslmode=disable"
    ports:
      - "9187:9187"
    depends_on:
      - postgres

volumes:
  prometheus_data:
  grafana_data:

Configuración de Prometheus (prometheus/prometheus.yml)

global:
  scrape_interval: 15s      # Frecuencia de extracción
  evaluation_interval: 15s  # Frecuencia de evaluación de alertas

# Configuración de AlertManager
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Archivos con reglas de alerta
rule_files:
  - "alerts/*.yml"

# Targets a monitorear
scrape_configs:
  # Auto-descubrimiento: Prometheus mismo
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # Aplicación Node.js personalizada
  - job_name: 'node-app'
    static_configs:
      - targets: ['node-app:3000']

  # PostgreSQL Exporter
  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-exporter:9187']

  # Node Exporter (métricas del sistema)
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  # cAdvisor (métricas de contenedores Docker)
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

Reglas de Alerta (prometheus/alerts/app_alerts.yml)

groups:
  - name: app_alerts
    rules:
      - alert: HighCPUUsage
        expr: rate(process_cpu_seconds_total[5m]) > 0.8
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Alto uso de CPU en {{ $labels.instance }}"
          description: "CPU está al {{ $value }}% por más de 5 minutos"

      - alert: ServiceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Servicio {{ $labels.job }} está caído"
          description: "{{ $labels.instance }} no responde desde hace 1 minuto"

      - alert: HighMemoryUsage
        expr: (node_memory_MemTotal_bytes - node_memory_MemFree_bytes - node_memory_Buffers_bytes - node_memory_Cached_bytes) / node_memory_MemTotal_bytes > 0.9
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Memoria alta en {{ $labels.instance }}"
          description: "Uso de memoria al {{ $value | humanizePercentage }}"

Instrumentando una Aplicación en Go

Ejemplo: Servidor HTTP con métricas Prometheus

// main.go
package main

import (
    "fmt"
    "net/http"
    "time"
    
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// Definir métricas
var (
    // Contador: número de peticiones
    httpRequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "endpoint", "status"},
    )
    
    // Histograma: duración de peticiones
    httpRequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "Duration of HTTP requests",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "endpoint"},
    )
    
    // Gauge: usuarios activos (puede subir y bajar)
    activeUsers = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_users",
            Help: "Current active users",
        },
    )
    
    // Summary: tamaño de respuestas
    responseSize = promauto.NewSummaryVec(
        prometheus.SummaryOpts{
            Name:       "http_response_size_bytes",
            Help:       "Size of HTTP responses",
            Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
        },
        []string{"method", "endpoint"},
    )
)

func main() {
    // Middleware para registrar métricas
    http.HandleFunc("/", metricsMiddleware(handleHome))
    http.HandleFunc("/api/users", metricsMiddleware(handleUsers))
    
    // Endpoint de métricas para Prometheus
    http.Handle("/metrics", promhttp.Handler())
    
    // Simular cambio de usuarios activos
    go simulateActiveUsers()
    
    fmt.Println("Server running on :8080")
    http.ListenAndServe(":8080", nil)
}

func metricsMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Crear response writer personalizado para capturar status
        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        next(rw, r)
        
        duration := time.Since(start).Seconds()
        endpoint := r.URL.Path
        method := r.Method
        
        httpRequestsTotal.WithLabelValues(method, endpoint, fmt.Sprintf("%d", rw.statusCode)).Inc()
        httpRequestDuration.WithLabelValues(method, endpoint).Observe(duration)
        responseSize.WithLabelValues(method, endpoint).Observe(float64(rw.size))
    }
}

func handleHome(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello World"))
}

func handleUsers(w http.ResponseWriter, r *http.Request) {
    activeUsers.Set(42) // Actualizar gauge
    w.Write([]byte(`{"users": ["alice", "bob"]}`))
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
    size       int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
    size, err := rw.ResponseWriter.Write(b)
    rw.size += size
    return size, err
}

func simulateActiveUsers() {
    for {
        time.Sleep(10 * time.Second)
        // Simular fluctuación de usuarios
        activeUsers.Set(float64(40 + time.Now().Second()%30))
    }
}

Dockerfile para la app Go

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

Dashboards en Grafana

Configuración Automática (provisioning)

# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: true

Dashboard JSON básico

{
  "title": "Mi App - Dashboard Principal",
  "panels": [
    {
      "title": "Peticiones por Segundo",
      "type": "graph",
      "targets": [
        {
          "expr": "rate(http_requests_total[1m])",
          "legendFormat": "{{method}} {{endpoint}}"
        }
      ],
      "gridPos": {"x": 0, "y": 0, "w": 12, "h": 8}
    },
    {
      "title": "Latencia P99",
      "type": "graph",
      "targets": [
        {
          "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))",
          "legendFormat": "{{method}} {{endpoint}}"
        }
      ],
      "gridPos": {"x": 12, "y": 0, "w": 12, "h": 8}
    },
    {
      "title": "Usuarios Activos",
      "type": "stat",
      "targets": [
        {
          "expr": "active_users"
        }
      ],
      "gridPos": {"x": 0, "y": 8, "w": 6, "h": 4}
    }
  ]
}

Consultas PromQL Esenciales

| Qué quieres medir | Consulta PromQL | |------------------|-----------------| | Tasa de peticiones | rate(http_requests_total[5m]) | | Latencia promedio | rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]) | | Percentil 99 | histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) | | Tasa de error | rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) | | Uso de CPU | rate(process_cpu_seconds_total[1m]) | | Memoria usada | process_resident_memory_bytes | | Servicios caídos | up == 0 | | Predicción de espacio | predict_linear(node_filesystem_free_bytes[1h], 4 * 3600) < 0 |


Exporters Populares

flowchart TD subgraph Exporters["🔌 Exporters Comunes"] Node["Node Exporter\n(Sistema Operativo)"] MySQL["MySQL Exporter"] Redis["Redis Exporter"] Nginx["Nginx Exporter"] Blackbox["Blackbox Exporter\n(Probes HTTP/ICMP)"] Pushgateway["Pushgateway\n(Métricas efímeras)"] end Exporters --> Prometheus note1["🎯 Cada exporter expone métricas\nen el formato que Prometheus entiende"]

Configuración de Node Exporter

# docker-compose.yml (agregar)
services:
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'
    restart: unless-stopped

Alertas Avanzadas con AlertManager

Configuración (alertmanager/config.yml)

global:
  smtp_smarthost: 'smtp.gmail.com:587'
  smtp_from: 'alerts@tuapp.com'
  smtp_auth_username: 'alerts@tuapp.com'
  smtp_auth_password: 'tu-password'

route:
  group_by: ['alertname', 'severity']
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 1h
  
  routes:
    - match:
        severity: critical
      receiver: pagerduty
      continue: true
    - match:
        severity: warning
      receiver: slack

receivers:
  - name: 'slack'
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/XXX'
        channel: '#alertas'
        title: '⚠️ Alerta: {{ .GroupLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'

  - name: 'pagerduty'
    pagerduty_configs:
      - service_key: 'tu-service-key'

  - name: 'email'
    email_configs:
      - to: 'equipo@tuapp.com'

Ejemplo: Alerta de SRE (Service Level Objective)

# prometheus/alerts/slo_alerts.yml
groups:
  - name: slo_alerts
    rules:
      - alert: HighErrorRate
        expr: |
          (
            sum(rate(http_requests_total{status=~"5.."}[5m]))
            /
            sum(rate(http_requests_total[5m]))
          ) > 0.01
        for: 5m
        labels:
          severity: critical
          slo: "99.9%"
        annotations:
          summary: "Tasa de error superior al 1%"
          description: "Error rate actual: {{ $value | humanizePercentage }}"
          runbook: "https://wiki.tuapp.com/runbooks/high-error-rate"

      - alert: LatencyBreach
        expr: |
          histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 0.5
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Latencia P99 superior a 500ms"
          description: "Latencia actual: {{ $value }}s"

Casos de Uso Reales

1. Monitoreo de Kubernetes (kube-prometheus-stack)

# Instalación con Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Instalar stack completo (Prometheus + Grafana + AlertManager + Exporters)
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace

Métricas que obtienes automáticamente:

  • Uso de CPU/Memoria por pod
  • Estado de los deployments
  • Latencia de API Server
  • Eventos del scheduler

2. Monitoreo de APIs con Blackbox Exporter

# prometheus/prometheus.yml (agregar)
scrape_configs:
  - job_name: 'blackbox-http'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
        - https://tuapi.com/health
        - https://tuapi.com/v1/users
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

3. Monitoreo de Batch Jobs (Pushgateway)

// Para jobs que no pueden esperar el pull de Prometheus
import "github.com/prometheus/client_golang/prometheus/push"

func main() {
    // Ejecutar job
    result := runBatchJob()
    
    // Pushear métrica al Pushgateway
    pusher := push.New("http://pushgateway:9091", "batch_job")
        .Collector(jobDuration)
        .Grouping("job_name", "daily_cleanup")
    
    if err := pusher.Push(); err != nil {
        log.Fatal("Error pushing metrics:", err)
    }
}

Mejores Prácticas

1. Nombrado de Métricas

// ✅ Bueno - Estructura clara
http_requests_total
http_request_duration_seconds
db_query_duration_seconds

// ❌ Malo - Confuso
counter1
time_taken
val

2. Uso de Labels

// ✅ Bueno - Labels útiles
http_requests_total{method="GET", endpoint="/users", status="200"}

// ❌ Malo - Demasiados valores únicos
http_requests_total{user_id="12345"}  // Cada usuario crea una serie nueva

3. Tipos de Métricas Correctos

| Tipo | Cuándo usar | |------|-------------| | Counter | Número de peticiones, errores, bytes transferidos (solo aumenta) | | Gauge | Temperatura, memoria usada, conexiones activas (sube y baja) | | Histogram | Latencia, tamaños (percentiles útiles) | | Summary | Similar a histogram pero con percentiles configurables |

4. Retención de Datos

# Configurar retención en Prometheus
command:
  - '--storage.tsdb.retention.time=30d'   # 30 días
  - '--storage.tsdb.retention.size=50GB'  # Máximo 50GB

¿Cuándo USAR Prometheus + Grafana?

✅ Casos Ideales

| Escenario | ¿Por qué? | |-----------|-----------| | Microservicios | Descubrimiento automático de servicios | | Kubernetes | Integración nativa (kube-state-metrics) | | Equipos de SRE/DevOps | Alertas potentes y SLI/SLO tracking | | Arquitecturas dinámicas | Auto-escalado, cambios frecuentes de instancias | | Necesidad de alertas complejas | PromQL permite lógica avanzada |

❌ Alternativas para otros casos

| Escenario | Alternativa | |-----------|-------------| | Monitoreo simple de 2 servidores | Netdata o Uptime Kuma | | Logs (no métricas) | ELK Stack (Elasticsearch, Logstash, Kibana) | | Traces distribuidos | Jaeger o Tempo | | Métricas empresariales (BI) | Tableau o PowerBI | | Infraestructura legacy | Zabbix o Nagios |


Conclusión

Prometheus y Grafana son la combinación estándar de la industria para observabilidad. Prometheus brilla en:

  • Recolección eficiente de métricas (pull model)
  • Consultas potentes con PromQL
  • Alertas confiables sin SPOF

Grafana aporta:

  • Visualización hermosa e interactiva
  • Múltiples fuentes (Prometheus, Loki, Tempo, CloudWatch)
  • Dashboards compartibles y alertas visuales

El consejo final: Empieza pequeño. Monitorea 3-4 métricas clave (peticiones, latencia, errores, CPU). Añade más conforme entiendas qué necesitas. No intentes monitorearlo todo desde el día 1.


Recursos Adicionales


¿Ya usas Prometheus y Grafana? ¿Qué métricas monitoreas primero en una aplicación nueva? ¡Déjamelo en los comentarios! 👇