prom-client: métricas customizadas em Node.js

prom-client é a biblioteca oficial do Prometheus para Node.js. Ela instrumenta sua API com três tipos fundamentais de métricas: counters (eventos que só crescem), gauges (valores que sobem e descem) e histogramas (distribuição de valores como latência). O endpoint /metrics expõe tudo no formato que o Prometheus sabe coletar.

Configurar prom-client no Express

Configuração base com métricas padrão do processo Node.js:

typescript
// npm install prom-client

import { Registry, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client'
import type { Express } from 'express'

// Registry separado (melhor que o default para testes e múltiplas instâncias):
export const registry = new Registry()

// Coletar métricas padrão do processo Node.js:
// (CPU, memória heap, event loop lag, GC, handles ativos)
collectDefaultMetrics({
  register: registry,
  prefix: 'nodejs_',   // prefixo para identificar a aplicação
  labels: {
    app: 'minha-api',
    env: process.env.NODE_ENV ?? 'production',
  },
})

// Registrar endpoint /metrics:
export function registrarMetricas(app: Express) {
  app.get('/metrics', async (req, res) => {
    res.set('Content-Type', registry.contentType)
    res.end(await registry.metrics())
  })

  // Proteger /metrics de acesso externo (apenas Prometheus interno):
  // Adicionar antes: verificar se o IP é o do Prometheus
  // ou usar middleware de autenticação simples
}

Counter, Gauge e Histogram na prática

Os três tipos de métricas mais importantes com exemplos reais:

typescript
// lib/metricas.ts — definir todas as métricas em um único arquivo:
import { Counter, Gauge, Histogram } from 'prom-client'
import { registry } from './registry'

// COUNTER: só cresce — total de requisições, erros, emails enviados:
export const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total de requisições HTTP recebidas',
  labelNames: ['method', 'route', 'status_code'],
  registers: [registry],
})

export const emailsEnviadosTotal = new Counter({
  name: 'emails_enviados_total',
  help: 'Total de emails enviados com sucesso',
  labelNames: ['tipo'],  // ex: 'boas_vindas', 'recuperacao_senha', 'notificacao'
  registers: [registry],
})

// GAUGE: sobe e desce — conexões ativas, jobs na fila, tamanho do cache:
export const conexoesAtivas = new Gauge({
  name: 'websocket_conexoes_ativas',
  help: 'Número de conexões WebSocket ativas',
  registers: [registry],
})

export const jobsNaFila = new Gauge({
  name: 'jobs_fila_tamanho',
  help: 'Quantidade de jobs pendentes na fila',
  labelNames: ['fila'],  // ex: 'emails', 'webhooks', 'exports'
  registers: [registry],
})

// HISTOGRAM: distribuição de valores — latência de requisições, tamanho de payload:
export const httpDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Latência das requisições HTTP em segundos',
  labelNames: ['method', 'route', 'status_code'],
  // Buckets: valores de limite para cada "balde" do histograma
  // Adaptar para sua API: se latência típica é 50-200ms, use buckets menores
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
  registers: [registry],
})

export const dbQueryDuration = new Histogram({
  name: 'db_query_duration_seconds',
  help: 'Latência de queries ao banco de dados',
  labelNames: ['query_name', 'tabela'],
  buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
  registers: [registry],
})

Middleware de instrumentação automática

Registrar latência e contagem de todas as rotas automaticamente:

typescript
// middleware/metricas.ts — instrumentar todas as rotas automaticamente:
import { httpRequestsTotal, httpDuration } from '../lib/metricas'

export function middlewareMetricas(req: Request, res: Response, next: NextFunction) {
  const start = process.hrtime.bigint()

  // Normalizar o path (evitar cardinality explosão com IDs dinâmicos):
  // /api/pedidos/123 → /api/pedidos/:id
  // /api/usuarios/abc-def → /api/usuarios/:id
  const route = req.route?.path ?? normalizarPath(req.path)

  res.on('finish', () => {
    const duracaoMs = Number(process.hrtime.bigint() - start) / 1e9

    const labels = {
      method: req.method,
      route,
      status_code: String(res.statusCode),
    }

    httpRequestsTotal.inc(labels)
    httpDuration.observe(labels, duracaoMs)
  })

  next()
}

function normalizarPath(path: string): string {
  return path
    .replace(//[0-9a-f]{8}-[0-9a-f-]{27}/g, '/:uuid')  // UUIDs
    .replace(//d+/g, '/:id')                            // IDs numéricos
    .replace(//[0-9a-f]{24}/g, '/:objectId')             // MongoDB ObjectIDs
}

// Registrar no Express ANTES das rotas:
app.use(middlewareMetricas)
app.use('/api', roteador)

Instrumentar banco de dados e serviços externos

Medir latência de queries e chamadas a APIs externas:

typescript
// lib/db.ts — wrapper do pg com métricas:
import { Pool } from 'pg'
import { dbQueryDuration } from './metricas'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })

export async function query<T>(
  queryName: string,
  tabela: string,
  sql: string,
  params?: unknown[]
): Promise<T[]> {
  const end = dbQueryDuration.startTimer({ query_name: queryName, tabela })

  try {
    const result = await pool.query(sql, params)
    end()  // registrar duração com sucesso
    return result.rows
  } catch (err) {
    end()  // registrar duração mesmo em caso de erro
    throw err
  }
}

// Usar na aplicação:
const pedidos = await query('listar_pedidos', 'pedidos',
  'SELECT * FROM pedidos WHERE usuario_id = $1',
  [userId]
)

// Métricas de serviços externos (ex: API de pagamento):
import { Histogram } from 'prom-client'

export const externalApiDuration = new Histogram({
  name: 'external_api_duration_seconds',
  help: 'Latência de chamadas a APIs externas',
  labelNames: ['servico', 'endpoint', 'status'],
  buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10],
  registers: [registry],
})

// Wrapper para fetch com métricas:
export async function fetchComMetricas(servico: string, url: string, opts?: RequestInit) {
  const end = externalApiDuration.startTimer({ servico, endpoint: new URL(url).pathname })
  const res = await fetch(url, opts)
  end({ status: String(res.status) })
  return res
}

Métricas de negócio customizadas

Monitorar KPIs da aplicação além de métricas técnicas:

typescript
// Métricas de negócio — o que importa para o produto:
import { Counter, Gauge } from 'prom-client'

// Eventos de negócio:
export const pedidosCriados = new Counter({
  name: 'negocios_pedidos_criados_total',
  help: 'Total de pedidos criados',
  labelNames: ['plano', 'canal'],  // ex: plano='premium', canal='web'
  registers: [registry],
})

export const receita = new Counter({
  name: 'negocios_receita_reais_total',
  help: 'Receita total em reais',
  labelNames: ['plano', 'metodo_pagamento'],
  registers: [registry],
})

export const usuariosAtivos = new Gauge({
  name: 'negocios_usuarios_ativos',
  help: 'Usuários com sessão ativa no momento',
  registers: [registry],
})

// Usar nos handlers da aplicação:
app.post('/api/pedidos', autenticar, async (req, res) => {
  const pedido = await criarPedido(req.body, req.usuario)

  // Métricas técnicas (via middleware automático)
  // + Métricas de negócio (explícitas):
  pedidosCriados.inc({
    plano: req.usuario.plano,
    canal: req.headers['x-source'] as string || 'web',
  })

  receita.inc({
    plano: req.usuario.plano,
    metodo_pagamento: pedido.metodoPagamento,
  }, pedido.valorTotal)

  res.status(201).json(pedido)
})

// Alerta de negócio: queda de pedidos (possível problema na checkout):
# - alert: QuedaNaPedidos
#   expr: rate(negocios_pedidos_criados_total[30m]) < 0.1
#   for: 15m
#   annotations:
#     summary: "Taxa de pedidos abaixo de 6/hora — verificar checkout"

Não quer configurar manualmente?

Implante o VPS para Node.js em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Counter: valor que só cresce, nunca decresce (reset ao reiniciar o processo). Use para: total de requisições, erros, emails enviados. Gauge: valor que sobe e desce livremente. Use para: conexões ativas, jobs na fila, uso de memória, usuários logados. Histogram: distribui observações em buckets (ex: "quantas requisições levaram 100-200ms?"). Use para: latência, tamanho de payload. Summary: similar ao histogram mas calcula percentis no lado do cliente, evite em aplicações com múltiplas instâncias (os percentis não podem ser agregados entre instâncias).

Cardinality é o número total de series únicas (combinações de métrica + labels). Alta cardinality pode sobrecarregar o Prometheus. Problema: usar user_id ou request_id como label cria uma série por usuário/requisição, com 100k usuários, você tem 100k series só para essa métrica. Solução: use labels de baixa cardinalidade (método HTTP = 5-10 valores, código de status = ~10 valores, rota normalizada = 20-50 valores). Nunca use IDs únicos como labels.

Opções: (1) bind apenas em localhost (recomendado): criar servidor HTTP separado em 127.0.0.1:9090/metrics, separado da API pública; (2) verificar IP de origem: aceitar apenas 127.0.0.1 ou o IP do container Prometheus; (3) basic auth com nginx: usar auth_basic no Nginx para a rota /metrics; (4) mover para porta separada não exposta: o Prometheus acessa internamente, a porta não está no UFW. O prom-client suporta criar um servidor HTTP separado (express/http na porta de métricas).

O overhead é mínimo para as operações de instrumentação (.inc(), .observe(), .set()): microssegundos. O único momento com overhead perceptível é o scrape do /metrics, quando o Prometheus coleta, prom-client serializa todas as métricas em texto. Para APIs com milhares de series: o scrape pode levar 10-50ms. Solução: criar o servidor de métricas em processo separado ou usar aggregator com pushgateway para isolar o overhead.

O processo é idêntico ao Express, prom-client não depende do framework. Alternativamente, use o plugin @fastify/metrics que integra automaticamente e registra métricas de todas as rotas sem middleware manual. Para instrumentação automática completa: opentelemetry-js-contrib tem auto-instrumentação para Express e Fastify que captura métricas de latência sem código adicional.