Métricas de negócio no Prometheus: KPIs com prom-client

Métricas técnicas dizem que o servidor está vivo; métricas de negócio dizem se o produto está saudável. Taxa de conversão caindo? Pedidos parando? Churn aumentando? Essas perguntas têm resposta quando você instrumenta o código de negócio com os mesmos counters e gauges que usa para monitorar a API.

Definir os KPIs do produto como métricas

Mapear os eventos importantes de negócio para tipos de métrica:

typescript
// lib/metricas-negocio.ts — KPIs instrumentados com prom-client:
import { Counter, Gauge, Histogram } from 'prom-client'
import { registry } from './registry'

// ── Aquisição ──────────────────────────────────────────────────────
export const cadastrosTotal = new Counter({
  name: 'negocio_cadastros_total',
  help: 'Total de novos usuários cadastrados',
  labelNames: ['canal', 'plano'],  // canal: 'google_oauth', 'email', 'convite'
  registers: [registry],
})

export const loginTotal = new Counter({
  name: 'negocio_logins_total',
  help: 'Total de logins realizados',
  labelNames: ['metodo', 'sucesso'],  // metodo: 'senha', 'google', 'github'
  registers: [registry],
})

// ── Ativação / Engajamento ─────────────────────────────────────────
export const usuariosAtivos = new Gauge({
  name: 'negocio_usuarios_ativos_gauge',
  help: 'Usuários com sessão ativa agora',
  registers: [registry],
})

export const acoesRealizadas = new Counter({
  name: 'negocio_acoes_total',
  help: 'Ações realizadas pelos usuários',
  labelNames: ['tipo_acao', 'plano_usuario'],
  registers: [registry],
})

// ── Receita ────────────────────────────────────────────────────────
export const receitaTotal = new Counter({
  name: 'negocio_receita_brl_total',
  help: 'Receita acumulada em BRL',
  labelNames: ['plano', 'periodo', 'metodo_pagamento'],
  registers: [registry],
})

export const assinaturas = new Gauge({
  name: 'negocio_assinaturas_ativas',
  help: 'Total de assinaturas ativas por plano',
  labelNames: ['plano'],
  registers: [registry],
})

// ── Retenção ───────────────────────────────────────────────────────
export const cancelamentos = new Counter({
  name: 'negocio_cancelamentos_total',
  help: 'Total de assinaturas canceladas',
  labelNames: ['plano', 'motivo'],
  registers: [registry],
})

// ── Conversão ─────────────────────────────────────────────────────
export const funil = new Counter({
  name: 'negocio_funil_etapa_total',
  help: 'Eventos de funil de conversão',
  labelNames: ['etapa'],  // 'landing', 'cadastro', 'trial', 'pagamento', 'ativo'
  registers: [registry],
})

Instrumentar os fluxos de negócio

Integrar as métricas nos handlers críticos da aplicação:

typescript
// handlers/auth.ts — instrumentar fluxo de cadastro e login:
app.post('/api/auth/register', async (req, res) => {
  const usuario = await criarUsuario(req.body)

  // Funil de conversão:
  funil.inc({ etapa: 'cadastro' })
  cadastrosTotal.inc({
    canal: req.body.provider ?? 'email',
    plano: usuario.plano ?? 'free',
  })

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

// handlers/pagamento.ts — instrumentar conversão de pagamento:
app.post('/api/assinaturas/checkout', autenticar, async (req, res) => {
  const assinatura = await processarPagamento(req.body, req.usuario)

  if (assinatura.status === 'ativo') {
    // Receita:
    receitaTotal.inc({
      plano: assinatura.plano,
      periodo: assinatura.periodo,  // 'mensal', 'anual'
      metodo_pagamento: assinatura.metodoPagamento,
    }, assinatura.valor)

    // Funil de conversão:
    funil.inc({ etapa: 'pagamento' })

    // Atualizar gauge de assinaturas ativas:
    assinaturas.inc({ plano: assinatura.plano })
  }

  res.json(assinatura)
})

// handlers/cancelamento.ts:
app.post('/api/assinaturas/cancelar', autenticar, async (req, res) => {
  const motivo = req.body.motivo ?? 'sem_motivo'
  const assin = await cancelarAssinatura(req.usuario.userId)

  cancelamentos.inc({ plano: assin.plano, motivo })
  assinaturas.dec({ plano: assin.plano })

  res.json({ status: 'cancelado' })
})

Sincronizar métricas de gauge com o banco

Manter gauges atualizados consultando o banco periodicamente:

typescript
// Problema: gauges podem ficar desatualizados se o processo reiniciar.
// Solução: sincronizar com o banco a cada N minutos.

import { db } from './db'

async function sincronizarMetricasNegocio() {
  // Buscar contagens atuais do banco:
  const [assinaturasAtivas] = await db.query<{ plano: string; total: number }[]>(
    `SELECT plano, COUNT(*) as total
     FROM assinaturas
     WHERE status = 'ativo'
     GROUP BY plano`
  )

  // Resetar e recarregar os gauges:
  for (const { plano, total } of assinaturasAtivas) {
    assinaturas.set({ plano }, Number(total))
  }

  // Usuários ativos na última hora:
  const [{ count }] = await db.query(
    "SELECT COUNT(DISTINCT usuario_id) as count FROM sessoes WHERE updated_at > NOW() - INTERVAL '1 hour'"
  )
  usuariosAtivos.set(Number(count))
}

// Sincronizar na inicialização e a cada 5 minutos:
sincronizarMetricasNegocio()
setInterval(sincronizarMetricasNegocio, 5 * 60 * 1000)

// Coletor assíncrono (alternativa mais elegante):
// O prom-client suporta coletores que executam lógica assíncrona:
import { Gauge } from 'prom-client'

new Gauge({
  name: 'negocio_pedidos_pendentes',
  help: 'Pedidos aguardando processamento',
  async collect() {
    const [{ count }] = await db.query(
      "SELECT COUNT(*) FROM pedidos WHERE status = 'pendente'"
    )
    this.set(Number(count))
  },
  registers: [registry],
})

Dashboards de negócio no Grafana

Criar painéis de KPIs para o time de produto e gestão:

bash
# Queries PromQL para dashboard de negócio:

# Taxa de cadastros (por canal) nos últimos 30 minutos:
sum by (canal) (
  rate(negocio_cadastros_total[30m])
) * 60 * 60  # converter para por hora

# Receita acumulada hoje:
increase(negocio_receita_brl_total[24h])

# Receita por plano (donut chart):
sum by (plano) (
  increase(negocio_receita_brl_total[24h])
)

# Taxa de conversão: cadastros → pagamento (últimas 24h):
increase(negocio_funil_etapa_total{etapa="pagamento"}[24h])
/
increase(negocio_funil_etapa_total{etapa="cadastro"}[24h]) * 100

# Churn rate (cancelamentos / assinaturas ativas):
sum(rate(negocio_cancelamentos_total[7d])) * 24 * 7
/
sum(negocio_assinaturas_ativas) * 100

# Assinaturas ativas por plano (stat panel):
sum by (plano) (negocio_assinaturas_ativas)

# Dicas de organização:
# - Usar variáveis de template para filtrar por período
# - Separar dashboards: Técnico (DevOps) vs Produto (CEO/PM)
# - Dashboard "Executive Summary" com os 4-5 KPIs principais em stat panels
# - Threshold coloridos: verde > meta, amarelo = atenção, vermelho = alerta

Alertas de negócio críticos

Detectar anomalias no produto antes que o cliente reclame:

yaml
# alerts/negocio.yml:
groups:
  - name: negocio
    rules:

      # Zero pedidos nos últimos 15 minutos (checkout pode estar quebrado):
      - alert: ZeroPedidos
        expr: |
          rate(negocio_pedidos_criados_total[15m]) == 0
        for: 15m
        labels:
          severity: critical
        annotations:
          summary: "Nenhum pedido nos últimos 15 minutos"
          description: "Possível problema no fluxo de checkout. Verificar logs."

      # Taxa de erro de pagamento acima de 10%:
      - alert: AltaTaxaErroPagamento
        expr: |
          rate(negocio_pagamentos_total{status="falhou"}[30m])
          / rate(negocio_pagamentos_total[30m]) * 100 > 10
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Taxa de falha de pagamento acima de 10%"
          description: "Taxa atual: {{ $value | printf "%.1f" }}%. Verificar integração com gateway."

      # Pico de cancelamentos (possível problema de qualidade/cobrança):
      - alert: PicoCancelamentos
        expr: |
          rate(negocio_cancelamentos_total[1h]) > 3
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "Taxa de cancelamento acima do normal"

      # Queda brusca de usuários ativos (> 30% em 1h):
      - alert: QuedaUsuariosAtivos
        expr: |
          negocio_usuarios_ativos_gauge
          < negocio_usuarios_ativos_gauge offset 1h * 0.7
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "Queda de >30% em usuários ativos na última hora"

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

São complementares, não concorrentes. Prometheus: tempo real, alertas, correlação com métricas técnicas (ex: queda de pedidos junto com latência alta → problema de infraestrutura). Analytics (Mixpanel, Amplitude, PostHog): análise histórica, funil de conversão, cohorts, retencão por segmento, A/B testing. Boa estratégia: emita eventos de negócio tanto para Prometheus (alertas em tempo real) quanto para analytics (análise histórica e produto).

Nunca use tenant_id como label no Prometheus, alta cardinalidade explode o armazenamento. Para multi-tenant: use labels de baixa cardinalidade (plano, tipo_conta, região). Para métricas por tenant específico: use a abordagem de sync com banco, uma gauge por tenant importante via coletor assíncrono, ou envie para uma ferramenta de analytics que suporta dimensões de alta cardinalidade.

Para MRR em tempo real (últimas 24h, última semana): sim, increase(negocio_receita_brl_total[30d]) funciona. Para MRR histórico e cálculos complexos (cohort analysis, LTV, churn calculado corretamente): Prometheus não é a ferramenta certa, use queries SQL no banco ou ferramentas de BI (Metabase, Superset). Prometheus brilha em monitoramento de variações anômalas em tempo real, não em análise histórica complexa.

Crie um counter para cada etapa do funil com a mesma label scheme. Use rate() para ver a taxa de cada etapa por hora. Calcule a conversão entre etapas: rate(etapa_B[1h]) / rate(etapa_A[1h]) * 100. Configure alertas quando a taxa de uma etapa cai abruptamente (> 30% abaixo da média das últimas 24h). Problema típico: taxa de cadastro normal, taxa de pagamento cai → problema no checkout. Isso é detectável em minutos com Prometheus.

Geralmente sim, simplifica a correlação (queda de pedidos junto com CPU alta). Para separar preocupações: use namespaces ou prefixos diferentes (negocio_, infra_, app_). Para grandes organizações com times diferentes: instâncias separadas do Prometheus com Grafana unificado usando múltiplos datasources. O custo de manter dois Prometheus > benefício da separação para maioria dos times.