Voltar para Artigos
DevOps10 min de leitura

Observabilidade com Grafana e Prometheus no Node.js

Instrumentação da API com prom-client: métricas de latência por rota com Histogram, taxa de erro, pool do banco de dados e event loop lag. Prometheus scraping e dashboards Grafana com alertas.

12 de agosto de 2026

Logs contam o que aconteceu. Métricas contam como o sistema está se comportando agora. A diferença é crucial: com logs, você descobre que a API ficou lenta depois que o cliente reclama. Com métricas, você vê o p99 de latência subindo antes que qualquer usuário sinta o impacto — e pode agir de forma proativa.

O trio Prometheus + Grafana + prom-client é o padrão open-source de observabilidade. O Prometheus coleta e armazena séries temporais. O Grafana visualiza e alerta. O prom-client instrumenta a API Node.js para expor as métricas no formato que o Prometheus entende.

Os Quatro Tipos de Métricas

  • Counter — só sobe, nunca desce. Total de requisições processadas, total de erros, total de logins. Use inc() para incrementar.
  • Gauge — sobe e desce. Conexões ativas no momento, uso de memória atual, tamanho do pool de conexões. Use set(), inc() e dec().
  • Histogram — distribui observações em buckets. Latência de requisições em percentis (p50, p90, p99). Use observe().
  • Summary — similar ao Histogram, mas calcula percentis no cliente (mais caro em CPU). Use Histogram em vez de Summary na maioria dos casos.
bash
npm install prom-client

Instrumentação da API

O Histogram é a métrica mais importante para APIs: ele distribui as durações em buckets e permite calcular percentis como p50, p90 e p99. O p99 é a métrica que importa para SLAs: "99% das requisições respondem em menos de 250ms". A média é enganosa — 100ms de média pode esconder que 1% dos usuários espera 5 segundos. O event loop lag é a métrica de saúde do processo Node.js: acima de 100ms indica que o event loop está bloqueado por operações síncronas pesadas ou computação intensa.

metrics.ts
import client from 'prom-client';

// Coleta métricas padrão do Node.js automaticamente:
// - process_cpu_seconds_total
// - process_resident_memory_bytes
// - nodejs_heap_size_total_bytes
// - nodejs_eventloop_lag_seconds (crucial para detectar bloqueio do event loop)
// - nodejs_active_handles_total
client.collectDefaultMetrics({
  prefix: 'api_',          // Prefixo para diferenciar de outros serviços
  gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
});

// ── Métricas HTTP ─────────────────────────────────────────────────────────────

// Histogram de latência: distribui em buckets (em segundos)
export const httpRequestDuration = new client.Histogram({
  name: 'api_http_request_duration_seconds',
  help: 'Duração das requisições HTTP em segundos',
  labelNames: ['method', 'route', 'status_code'] as const,
  // Buckets em segundos: 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});

// Counter de requisições totais
export const httpRequestsTotal = new client.Counter({
  name: 'api_http_requests_total',
  help: 'Total de requisições HTTP recebidas',
  labelNames: ['method', 'route', 'status_code'] as const,
});

// Counter de erros HTTP 500
export const httpErrorsTotal = new client.Counter({
  name: 'api_http_errors_total',
  help: 'Total de erros HTTP 5xx',
  labelNames: ['method', 'route'] as const,
});

// ── Métricas de Banco de Dados ────────────────────────────────────────────────

// Gauge do pool de conexões
export const dbPoolSize = new client.Gauge({
  name: 'api_db_pool_connections',
  help: 'Conexões no pool do banco de dados',
  labelNames: ['state'] as const, // 'active' | 'idle' | 'waiting'
});

// Histogram de duração das queries
export const dbQueryDuration = new client.Histogram({
  name: 'api_db_query_duration_seconds',
  help: 'Duração das queries no banco de dados',
  labelNames: ['operation', 'entity'] as const,
  buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5],
});

export { client };

Middleware de Instrumentação HTTP

metricsMiddleware.ts
import type { Request, Response, NextFunction } from 'express';
import { httpRequestDuration, httpRequestsTotal, httpErrorsTotal } from './metrics';

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

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000; // Converte para segundos

    // Usa req.route?.path para agrupar rotas parametrizadas:
    // '/users/123' e '/users/456' viram '/users/:id' no label
    // Sem isso, cada userId único cria uma série no Prometheus (cardinality explosion)
    const route = req.route?.path ?? req.path;
    const method = req.method;
    const statusCode = res.statusCode.toString();

    httpRequestDuration.observe(
      { method, route, status_code: statusCode },
      duration
    );

    httpRequestsTotal.inc({ method, route, status_code: statusCode });

    if (res.statusCode >= 500) {
      httpErrorsTotal.inc({ method, route });
    }
  });

  next();
}

Endpoint /metrics e Setup no Express

server.ts
import express from 'express';
import { client, metricsMiddleware } from './infra/metrics';

const app = express();

// Instrumenta todas as rotas antes de registrá-las
app.use(metricsMiddleware);

app.use('/api', router);

// Endpoint de métricas — protegido por rede interna ou basic auth
// O Prometheus raspa este endpoint a cada 15s (configurável)
app.get('/metrics', async (req, res) => {
  // Em produção, restrinja este endpoint à rede interna do cluster
  // ou adicione basic auth para evitar exposição pública
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});

Stack Completa com Docker Compose

docker-compose.monitoring.yml
name: monitoring

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - prometheus
    networks:
      - monitoring

volumes:
  prometheus_data:
  grafana_data:

networks:
  monitoring:
    driver: bridge
prometheus.yml
global:
  scrape_interval: 15s       # Coleta métricas a cada 15 segundos
  evaluation_interval: 15s   # Avalia regras de alerta a cada 15s

scrape_configs:
  - job_name: 'api-node'
    static_configs:
      - targets: ['host.docker.internal:3333'] # API rodando no host
    metrics_path: '/metrics'
    scrape_interval: 10s

Queries PromQL Essenciais

As queries PromQL no Grafana transformam as séries temporais em paineis acionáveis. Configure alertas para: p99 acima do SLA, taxa de erro 5xx acima de 1%, e event loop lag acima de 100ms. Com alertas configurados no Alertmanager do Prometheus (ou diretamente no Grafana), a equipe é notificada no Slack ou PagerDuty antes que os usuários reportem o problema.

PromQL no Grafana
# Taxa de requisições por segundo (média dos últimos 5 minutos)
rate(api_http_requests_total[5m])

# Latência p99 por rota
histogram_quantile(0.99, rate(api_http_request_duration_seconds_bucket[5m]))

# Taxa de erros 5xx (% do total de requisições)
rate(api_http_errors_total[5m]) / rate(api_http_requests_total[5m]) * 100

# Event Loop Lag (acima de 100ms = API bloqueada)
api_nodejs_eventloop_lag_seconds * 1000

# Uso de heap memory
api_process_resident_memory_bytes / 1024 / 1024

Cardinality explosion: não use valores de alta cardinalidade como labels (IDs de usuário, UUIDs, IPs). Cada combinação única de labels cria uma série temporal separada no Prometheus. Com 100.000 usuários únicos no label user_id, você cria 100.000 séries temporais — isso consome memória e degrada o Prometheus. Use apenas valores de baixa cardinalidade: method, route, status_code, environment.

Conclusão

Prometheus + Grafana transforma sua API em um sistema observável: você vê a latência p99 subindo antes que qualquer usuário reclame, identifica quais rotas estão lentas, e configura alertas para ser notificado proativamente. O prom-client com métricas padrão do Node.js (event loop lag, heap size, GC duration) + métricas HTTP customizadas dá uma visão completa da saúde da aplicação com poucos minutos de configuração.