StackPractices
advanced Por Mathias Paulenko

Prometheus y Grafana: métricas, dashboards y alertas

Instrumenta servicios con métricas de Prometheus, consúltalas en PromQL, construye dashboards en Grafana y publica reglas de alerta que no oscilan.

Introducción

Algo te está despertando a las 3 AM y tu mejor herramienta de depuración es kubectl logs | grep error. Ese es el momento en que un stack de métricas se paga solo: Prometheus te dice que la tasa de errores se duplicó a las 02:47 y qué endpoint la causó, antes de que abras un solo log.

Prometheus es una base de datos de series temporales que obtiene métricas de tus servicios por HTTP, las almacena localmente y evalúa reglas de alerta sobre ellas. Grafana es la capa de visualización: consulta Prometheus con PromQL y renderiza dashboards. Entre ambos está Alertmanager, que deduplica, agrupa y enruta las alertas disparadas hacia Slack, PagerDuty o email. El stack es el estándar de facto para métricas cloud-native — Prometheus se graduó en la CNCF en 2018 — y el resto de esta guía lo monta de punta a punta: tipos de métricas, instrumentación en Python, Node.js y Java, PromQL, reglas de alerta, aprovisionamiento de dashboards y un despliegue completo con Docker Compose.

Esta guía es para ingenieros de backend e infraestructura que ya corren servicios HTTP y quieren métricas y alertas delante. Necesitas Docker y un servicio que exponga un endpoint HTTP. Si antes quieres el panorama completo — logs y trazas junto a las métricas — la guía de observabilidad con el stack de Grafana cubre las tres señales; esta se queda a fondo en la mitad de métricas. Para estrategia de alertas y SLOs en lugar del stack en sí, consulta la guía de monitorización y alertado.

Cómo encajan las piezas

El modelo pull es la idea central a interiorizar: tus servicios exponen un endpoint /metrics en un formato de texto plano y Prometheus lo recolecta según una programación. Nada en tu aplicación sabe que Prometheus existe — solo responde HTTP. Esa inversión es lo que hace el modelo simple de operar: el sistema de monitorización decide la frecuencia de recolección, detecta targets caídos y nunca depende de que tu app sepa a dónde enviar datos.

flowchart diagram: Servicios app<br/>/metrics

Dos componentes merecen una línea cada uno porque el resto de la guía los usa libremente. Un exporter es un proceso que traduce el estado de un sistema de terceros al formato de Prometheus — node-exporter expone CPU, memoria y disco del host; hay exporters para PostgreSQL, Redis, NGINX y casi toda la infraestructura que desplegarías. Alertmanager es el binario compañero que recibe las alertas disparadas por Prometheus, las deduplica, las agrupa y las enruta hacia receptores como Slack o PagerDuty. Prometheus decide que algo va mal; Alertmanager decide quién se entera.

Tipos de métricas de Prometheus

Prometheus define cuatro tipos de métricas. Elegir el correcto importa porque determina qué funciones de PromQL puedes aplicar — rate() necesita un counter, y los percentiles entre instancias necesitan un histogram, no un summary.

TipoComportamientoPara qué sirveConsulta de ejemplo
CounterSolo sube (se reinicia al reiniciar)Totales: peticiones, errores, pedidosrate(orders_total[5m]) → pedidos/seg
GaugeSube y bajaEstado actual: profundidad de cola, memoria, conexionesactive_users → valor actual
HistogramMuestras en buckets configurablesLatencia, tamaños de respuesta, todo lo que tenga percentileshistogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
SummaryPercentiles del lado del cliente, no agregablesCasi nunca — prefiere histogramshistogram_quantile no aplica

La distinción histogram-versus-summary muerde en producción. Un Summary calcula percentiles dentro de tu app, así que un P99 de la instancia A no puede combinarse con un P99 de la instancia B — promediarlos da un resultado erróneo. Un Histogram exporta contadores acumulados por bucket (_bucket{le="0.5"}), y histogram_quantile calcula el P99 real sobre todas las instancias del lado del servidor. El coste es que eliges los límites de los buckets por adelantado; los valores por defecto o el conjunto (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0) usado más abajo cubre la mayoría de servicios HTTP.

El nombrado sigue una convención que conviene adoptar desde el día uno: nombre base más sufijo de unidad — _seconds, _bytes, _total — y labels para las dimensiones, nunca incrustadas en el nombre.

Instrumentación de servicios

Instrumentar significa tres cosas: definir las métricas una vez al arrancar, actualizarlas dentro de tu lógica de negocio y exponerlas en /metrics. Las librerías cliente oficiales — prometheus_client para Python, prom-client para Node.js, el registry de Prometheus de Micrometer para Java — se encargan del formato de exposición por ti. Los ejemplos de abajo instrumentan el mismo OrderService en cada lenguaje para que puedas comparar las formas. Para un recorrido más enfocado, la receta del exporter en Python y la receta de Micrometer profundizan por lenguaje.

El patrón a copiar es dónde viven los contadores: se incrementan dentro de las ramas de resultado (éxito, fallo de pago, error inesperado), no al inicio del método. Etiqueta con qué pasó, nunca con a quién le pasó — status y payment_method tienen valores acotados; user_id no, y ese único label crearía una serie temporal por usuario.

Python: prometheus_client

# metrics.py — instrumentación de Prometheus para Python
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
import time

# Definición de métricas
ORDERS_TOTAL = Counter(
    "orders_total",
    "Total number of orders created",
    ["status", "payment_method"],
)

ORDER_DURATION = Histogram(
    "order_creation_duration_seconds",
    "Time spent creating orders",
    ["endpoint"],
    buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)

ACTIVE_USERS = Gauge(
    "active_users",
    "Number of currently active users",
)

QUEUE_DEPTH = Gauge(
    "message_queue_depth",
    "Number of messages in the queue",
    ["queue_name"],
)

# Uso en el código del servicio
class OrderService:
    def create_order(self, user_id: int, items: list[dict]) -> dict:
        start = time.time()
        try:
            order = self._process_order(user_id, items)
            ORDERS_TOTAL.labels(status="success", payment_method=order["payment_method"]).inc()
            return order
        except PaymentError:
            ORDERS_TOTAL.labels(status="payment_failed", payment_method="unknown").inc()
            raise
        except Exception:
            ORDERS_TOTAL.labels(status="error", payment_method="unknown").inc()
            raise
        finally:
            ORDER_DURATION.labels(endpoint="/api/orders").observe(time.time() - start)

# Endpoint de métricas
from flask import Flask, Response

app = Flask(__name__)

@app.route("/metrics")
def metrics():
    return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)

Node.js: prom-client

// metrics.ts — instrumentación de Prometheus para Node.js
import { Counter, Histogram, Gauge, register } from "prom-client";

// Definición de métricas
const ordersTotal = new Counter({
  name: "orders_total",
  help: "Total number of orders created",
  labelNames: ["status", "paymentMethod"] as const,
});

const orderDuration = new Histogram({
  name: "order_creation_duration_seconds",
  help: "Time spent creating orders",
  labelNames: ["endpoint"] as const,
  buckets: [0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
});

const activeUsers = new Gauge({
  name: "active_users",
  help: "Number of currently active users",
});

const queueDepth = new Gauge({
  name: "message_queue_depth",
  help: "Number of messages in the queue",
  labelNames: ["queueName"] as const,
});

// Uso en el código del servicio
class OrderService {
  async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
    const start = Date.now();
    try {
      const order = await this.processOrder(userId, items);
      ordersTotal.inc({ status: "success", paymentMethod: order.paymentMethod });
      return order;
    } catch (error) {
      ordersTotal.inc({ status: "error", paymentMethod: "unknown" });
      throw error;
    } finally {
      orderDuration.observe({ endpoint: "/api/orders" }, (Date.now() - start) / 1000);
    }
  }
}

// Endpoint de métricas (Express)
import express from "express";
const app = express();

app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});

Java: Micrometer

// MetricsConfig.java — Micrometer con Prometheus
import io.micrometer.core.instrument.*;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MetricsConfig {

    @Bean
    public PrometheusMeterRegistry prometheusRegistry() {
        PrometheusMeterRegistry registry = new PrometheusMeterRegistry(key -> null);

        // Vincula métricas de JVM y de sistema
        new JvmMemoryMetrics().bindTo(registry);
        new ProcessorMetrics().bindTo(registry);

        return registry;
    }
}

// Código del servicio usando Micrometer
@Service
public class OrderService {

    private final Counter ordersCounter;
    private final Timer orderDurationTimer;
    private final Gauge activeUsersGauge;

    public OrderService(MeterRegistry registry) {
        this.ordersCounter = Counter.builder("orders_total")
            .description("Total number of orders created")
            .tags("status", "success")
            .register(registry);

        this.orderDurationTimer = Timer.builder("order_creation_duration")
            .description("Time spent creating orders")
            .publishPercentileHistogram()  // expone series _bucket, agregables en PromQL
            .register(registry);

        this.activeUsersGauge = Gauge.builder("active_users", this, OrderService::getActiveUserCount)
            .description("Number of currently active users")
            .register(registry);
    }

    public Order createOrder(Long userId, List<OrderItem> items) {
        return orderDurationTimer.record(() -> {
            Order order = processOrder(userId, items);
            ordersCounter.increment();
            return order;
        });
    }

    private int getActiveUserCount() {
        return userSessionManager.getActiveCount();
    }
}

// Endpoint de métricas
@RestController
public class MetricsController {

    private final PrometheusMeterRegistry registry;

    public MetricsController(PrometheusMeterRegistry registry) {
        this.registry = registry;
    }

    @GetMapping("/metrics")
    public String metrics() {
        return registry.scrape();
    }
}

Fíjate en la llamada a .publishPercentileHistogram() del ejemplo en Java: publica series de buckets en lugar de percentiles del lado del cliente, que es el equivalente en Micrometer de elegir un histogram sobre un summary.

PromQL esencial

PromQL — el Prometheus Query Language — es un lenguaje funcional para seleccionar, agregar y computar sobre series temporales. Cada panel de Grafana y cada regla de alerta ejecuta una expresión PromQL, así que el puñado de patrones de abajo cubre casi todo lo que escribirás en el día a día.

Consultas básicas

# Valor actual de un gauge
active_users

# Rate de un counter en 5 minutos
rate(orders_total[5m])

# Incremento total en 1 hora
increase(orders_total[1h])

# Media en 5 minutos
avg_over_time(memory_usage_bytes[5m])

# Máximo en 10 minutos
max_over_time(queue_depth[10m])

Dos reglas prácticas. Los counters no significan nada en crudo — envuélvelos siempre en rate() o increase(), que además corrigen los reinicios del counter cuando un pod se reinicia. Y para alertas prefiere rate(): irate() usa solo las dos últimas muestras y produce gráficas con saltos, mientras rate() promedia sobre la ventana.

Filtrado por labels

# Filtrar por label status
orders_total{status="error"}

# Filtrar por varios labels
http_requests_total{method="POST", status=~"5.."}

# Regex (errores 5xx)
http_requests_total{status=~"5.."}

# Regex negativa (excluir health checks)
http_requests_total{path!="/health"}

Agregaciones

# Suma por servicio
sum by (service) (rate(http_requests_total[5m]))

# Latencia media por endpoint
avg by (endpoint) (rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]))

# Top 5 endpoints por tasa de error
topk(5, sum by (endpoint) (rate(http_requests_total{status=~"5.."}[5m])))

# Percentil 99 de latencia sobre todas las instancias
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

Aritmética y vector matching

# Tasa de error en porcentaje
100 * sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))

# Uso de memoria en porcentaje del límite, emparejado por el label instance
100 * (memory_usage_bytes / on(instance) memory_limit_bytes)

# CPU por encima del 80%
(100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)) > 80

Cuando divides dos expresiones de vectores, Prometheus necesita saber cómo emparejar las muestras: on(instance) le dice que empareje series que comparten el mismo label instance. Dejar on() vacío — u omitirlo cuando los labels no coinciden — devuelve silenciosamente cero datos o emparejamientos erróneos.

Reglas de alertado

Una regla de alerta es una expresión PromQL que se evalúa en cada evaluation_interval. Cuando se mantiene verdadera durante el tiempo indicado en for, Prometheus dispara la alerta y se la entrega a Alertmanager. Mantén las reglas en un YAML dedicado cargado vía rule_files.

# alerting_rules.yml — reglas de alerta de Prometheus
groups:
  - name: service-alerts
    rules:
      # Tasa de error alta
      - alert: HighErrorRate
        expr: |
          100 * sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
          / sum(rate(http_requests_total[5m])) by (service) > 5
        for: 5m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "High error rate on {{ $labels.service }}"
          description: "{{ $labels.service }} has {{ $value }}% error rate for the last 5 minutes."

      # Latencia alta (P99 > 2s)
      - alert: HighLatencyP99
        expr: |
          histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) > 2
        for: 10m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "High P99 latency on {{ $labels.service }}"
          description: "P99 latency is {{ $value }}s for the last 10 minutes."

      # Servicio caído
      - alert: ServiceDown
        expr: up == 0
        for: 2m
        labels:
          severity: critical
          team: oncall
        annotations:
          summary: "Service {{ $labels.instance }} is down"
          description: "{{ $labels.instance }} has been down for more than 2 minutes."

      # Uso alto de memoria
      - alert: HighMemoryUsage
        expr: |
          100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 85
        for: 10m
        labels:
          severity: warning
          team: infra
        annotations:
          summary: "High memory usage on {{ $labels.instance }}"
          description: "Memory usage is {{ $value }}% for the last 10 minutes."

      # Profundidad de cola creciendo
      - alert: QueueDepthGrowing
        expr: |
          avg_over_time(message_queue_depth[10m]) > avg_over_time(message_queue_depth[1h]) * 2
        for: 5m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "Queue {{ $labels.queue_name }} depth is growing"
          description: "Queue depth is 2x higher than the 1h average."

La anatomía de alerta que importa: expr es la condición, for es la histéresis que evita el flapping ante picos momentáneos, labels llevan los metadatos de enrutado (severity, team) sobre los que Alertmanager filtra, y annotations guardan el texto legible plantillado con {{ $labels.x }} y {{ $value }}.

Las expresiones caras que comparten muchos dashboards o alertas van en recording rules: una expresión pre-computada almacenada como una nueva serie temporal en el mismo archivo de reglas.

  - name: recording-rules
    rules:
      - record: job:http_requests:rate5m
        expr: sum by (job) (rate(http_requests_total[5m]))

      - record: service:http_errors:ratio5m
        expr: |
          sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
          / sum by (service) (rate(http_requests_total[5m]))

Los dashboards consultan entonces service:http_errors:ratio5m directamente en lugar de recomputar una expresión de cinco términos en cada refresco. Valida las reglas antes de desplegar con promtool check rules alerting_rules.yml, y hazles pruebas unitarias contra datos fijos con promtool test rules — detecta umbrales mal calibrados antes de que despierten a alguien.

Dashboards de Grafana

Grafana lee dashboards y data sources desde archivos YAML bajo /etc/grafana/provisioning, que es lo que hace reproducible todo el stack: los dashboards viven en Git, no en la sesión de navegador de alguien.

Se necesitan dos archivos, y el primero es el que la mayoría de ejemplos olvida: el data source. Sin él, los dashboards aprovisionados cargan pero no muestran datos.

# grafana/provisioning/datasources/datasource.yml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    uid: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
# grafana/provisioning/dashboards/dashboards.yml
apiVersion: 1
providers:
  - name: 'default'
    orgId: 1
    folder: 'Services'
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    options:
      path: /var/lib/grafana/dashboards

JSON del dashboard (paneles clave)

{
  "dashboard": {
    "title": "Order Service Overview",
    "panels": [
      {
        "title": "Request Rate",
        "type": "timeseries",
        "datasource": { "type": "prometheus", "uid": "prometheus" },
        "targets": [
          {
            "expr": "sum(rate(http_requests_total[5m])) by (endpoint)",
            "legendFormat": "{{endpoint}}"
          }
        ]
      },
      {
        "title": "Error Rate %",
        "type": "stat",
        "datasource": { "type": "prometheus", "uid": "prometheus" },
        "targets": [
          {
            "expr": "100 * sum(rate(http_requests_total{status=~\"5..\"}[5m])) / sum(rate(http_requests_total[5m]))"
          }
        ],
        "thresholds": [
          { "color": "green", "value": 0 },
          { "color": "yellow", "value": 1 },
          { "color": "red", "value": 5 }
        ]
      },
      {
        "title": "P99 Latency",
        "type": "timeseries",
        "datasource": { "type": "prometheus", "uid": "prometheus" },
        "targets": [
          {
            "expr": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))",
            "legendFormat": "P99"
          },
          {
            "expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))",
            "legendFormat": "P95"
          }
        ]
      }
    ]
  }
}

Los paneles usan timeseries, que reemplazó al tipo graph deprecado desde Grafana 8. Referenciar el data source por uid mantiene el JSON portable entre entornos. Para construir los paneles en sí — variables, filas repetidas, anotaciones de alertas — la receta de dashboards de Grafana lo recorre paso a paso.

Despliegue con Docker Compose

Este compose levanta el stack completo en local — Prometheus, Grafana, Alertmanager y node-exporter — cableado a los archivos de configuración de arriba. Para producción la topología es la misma, pero los secretos vienen de tu gestor de secretos y en Kubernetes desplegarías normalmente kube-prometheus-stack.

# docker-compose.yml — stack completo de Prometheus + Grafana
services:
  prometheus:
    image: prom/prometheus:v3.13.4
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./alerting_rules.yml:/etc/prometheus/alerting_rules.yml
      - prometheus-data:/prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.retention.time=30d"
      - "--web.enable-lifecycle"

  grafana:
    image: grafana/grafana:13.2.4
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?set it in .env or your secret store}
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
      - ./grafana/dashboards:/var/lib/grafana/dashboards
    depends_on:
      - prometheus

  alertmanager:
    image: prom/alertmanager:v0.34.1
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml

  node-exporter:
    image: prom/node-exporter:v1.12.1
    ports:
      - "9100:9100"
    pid: host

volumes:
  prometheus-data:
  grafana-data:
# prometheus.yml — configuración de Prometheus
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - alerting_rules.yml

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - alertmanager:9093

scrape_configs:
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

  - job_name: "node"
    static_configs:
      - targets: ["node-exporter:9100"]

  - job_name: "order-service"
    static_configs:
      - targets: ["order-service:8080"]
    metrics_path: /metrics
    scrape_interval: 10s

  - job_name: "api-gateway"
    static_configs:
      - targets: ["api-gateway:8080"]
    metrics_path: /metrics

Cada job_name se convierte en el label job de todas las series recolectadas, y static_configs es el mecanismo de descubrimiento más simple — una entrada por target. En plataformas de contenedores lo cambias por service discovery: kubernetes_sd_configs hace que Prometheus pregunte a la API de Kubernetes por pods y servicios y los recolecte a medida que aparecen — típicamente anotando los pods con prometheus.io/scrape: "true" — y consul_sd_configs hace lo mismo contra un catálogo de Consul. En Kubernetes concretamente, la mayoría de equipos se salta la configuración a mano y despliega kube-prometheus-stack, un chart de Helm que trae Prometheus, Alertmanager, Grafana, node-exporter y dashboards y reglas preconfigurados como una sola unidad.

# alertmanager.yml — enrutado de alertas
route:
  receiver: "default"
  group_by: ["alertname", "service"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers:
        - 'severity="critical"'
      receiver: "oncall"

receivers:
  - name: "default"
    slack_configs:
      - api_url: "https://hooks.slack.com/services/..."
        channel: "#alerts"
        send_resolved: true
        title: '{{ .CommonLabels.alertname }}'
        text: '{{ .CommonAnnotations.summary }}'

  - name: "oncall"
    pagerduty_configs:
      - service_key: "your-pagerduty-key"

Las routes anidadas envían las alertas severity="critical" a PagerDuty mientras el resto aterriza en Slack — ese es el enrutado para el que existen los labels severity y team de tus reglas. group_wait agrupa las primeras alertas de un grupo para que un despliegue que dispara cinco alertas produzca una notificación, no cinco.

Verificar el stack

docker compose up -d y luego comprueba en orden: http://localhost:9090/targets debería listar todos los jobs como UP — un target atascado en DOWN suele significar un metrics_path equivocado o que la app aún no expone /metrics. Consulta up en http://localhost:9090/graph para confirmar. Después abre http://localhost:3000 — el data source de Prometheus aprovisionado y los dashboards bajo grafana/dashboards ya deberían estar ahí. Alertmanager queda en http://localhost:9093 mostrando las alertas enrutadas.

Cuándo Prometheus no es la herramienta adecuada

La monitorización pull cubre bien a los servicios de larga duración, pero tres situaciones piden otra cosa. Los batch jobs de vida corta terminan antes del siguiente scrape — usa el Pushgateway para que el job empuje sus métricas finales antes de salir, y trátalo como una caché de métricas de jobs, no como un reemplazo general del push. El almacenamiento a largo plazo y la alta disponibilidad exceden lo que una sola TSDB de Prometheus hace bien — haz remote-write a Thanos, Cortex o Mimir para retención de más de un mes y deduplicación entre réplicas. No quieres operar una TSDB — las opciones hospedadas (Grafana Cloud, Prometheus gestionado de AWS/GCP) cambian el control del self-hosting por no correr la capa de almacenamiento.

Mejores prácticas

  • Usa Histograms antes que Summaries: los histograms agregan del lado del servidor entre instancias; los percentiles de un summary no.
  • Sigue la convención de sufijo de unidad (_seconds, _bytes, _total): hace las métricas autodocumentadas en el autocompletado de Grafana.
  • Mantén la cardinalidad de labels acotada: status, endpoint, method son seguros; user_id, session_id, request_id crean una serie por usuario y pueden tumbar una instancia de Prometheus.
  • Dimensiona la retención a propósito: un scrape de 15s retenido 30 días cuesta aproximadamente 1GB por cada 100k series activas.
  • Alerta sobre síntomas (error rate > 5%), no sobre causas (CPU > 80%): las alertas por causa disparan mientras los usuarios no ven nada raro, y se pierden los fallos que importan.
  • Toda alerta necesita una cláusula for. Sin ella, un pico de dos segundos te despierta y se auto-resuelve antes de que abras el portátil.
  • Recolecta a 10-15s para servicios. Intervalos más finos multiplican el coste de almacenamiento por una resolución que rara vez mirarás.
  • Pre-computa las consultas caras como recording rules en lugar de pagarlas en cada refresco de dashboard.
  • Monitoriza el monitor: Prometheus expone sus propias métricas (prometheus_tsdb_*, prometheus_rule_evaluation_failures_total) — alerta también sobre fallos de scrape y presión de almacenamiento.

Errores comunes

  • Labels de alta cardinalidad: un label user_id convierte 5 métricas en millones de series. Los problemas de cardinalidad son la razón más común por la que las instalaciones de Prometheus mueren. Mantén la identidad en logs y trazas.
  • Usar Summaries para percentiles: no se pueden agregar entre instancias. Usa Histograms con buckets elegidos.
  • Sin cláusula for: las alertas disparan con picos momentáneos y se resuelven al instante. Añade for: 5m como mínimo en alertas basadas en rate.
  • Scrape demasiado frecuente: un intervalo de 1s crea 10 veces las series de uno de 10s casi sin beneficio. Usa 10-15s.
  • Sin aprovisionamiento del data source: los dashboards cargan pero se renderizan vacíos porque no se aprovisionó ningún data source. Distribuye datasource.yml junto a dashboards.yml.
  • Receptores muertos: declarar un receptor oncall sin una ruta que lo referencie hace que las alertas críticas caigan al receptor por defecto. Todo receptor necesita una ruta que lo alcance.

Ver también

Lecturas recomendadas

Preguntas frecuentes

¿Qué es Prometheus?

Una base de datos de series temporales y sistema de monitorización que recolecta métricas de servicios instrumentados por HTTP. Almacena las métricas en su propia TSDB, evalúa reglas de alerta periódicamente y entrega las alertas disparadas a Alertmanager.

¿Qué es PromQL?

Prometheus Query Language — el lenguaje de consulta funcional usado para seleccionar, agregar y computar sobre series temporales. Cada panel de Grafana y cada regla de alerta ejecuta una expresión PromQL.

¿Cuál es la diferencia entre monitorización pull y push?

Prometheus obtiene métricas recolectando el endpoint /metrics de cada servicio periódicamente. Los sistemas push (StatsD, Graphite) requieren que cada servicio sepa a dónde enviar datos. El pull permite al sistema de monitorización controlar la frecuencia de recolección y detectar targets caídos vía la métrica up; el push encaja con batch jobs de vida corta a través del Pushgateway.

¿Cuál es la diferencia entre Prometheus y Grafana?

Prometheus recolecta, almacena y consulta métricas — es la base de datos más el motor de alertas. Grafana no almacena nada: consulta Prometheus (y otros data sources) para renderizar dashboards. Un despliegue típico corre ambos, más Alertmanager para la entrega de alertas.

¿Cómo escribo una regla de alerta de Prometheus para la tasa de error?

Usa el ratio entre el rate de 5xx y el rate total con una cláusula for: 100 * sum(rate(http_requests_total{status=~"5.."}[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service) > 5 con for: 5m. La regla HighErrorRate completa con labels y annotations está en la sección de Reglas de alertado.

¿Cuánto tiempo debería retener las métricas?

15-30 días cubre la mayoría de necesidades operativas y cuesta aproximadamente 1GB por cada 100k series con un intervalo de 15s. Para retención mayor o alta disponibilidad, haz remote-write a Thanos, Cortex o Mimir en lugar de estirar el almacenamiento local.

¿Qué es una recording rule?

Una expresión PromQL que Prometheus evalúa periódicamente y almacena como una nueva serie temporal. Las recording rules existen para que dashboards y alertas consulten una métrica barata pre-computada en lugar de recomputar una agregación cara en cada refresco.