Pular para o conteúdo
devops

Monitoramento de Aplicação em Produção: Observabilidade com Logs, Métricas e Rastreamento

Como implementar observabilidade completa: structured logging com Pino, métricas com Prometheus, tracing distribuído com OpenTelemetry e alertas inteligentes.

Douglas M. Pereira4 min de leitura
monitoramentoobservabilidadeprometheusgrafanaopentelemetrylogssentry

Os três pilares da observabilidade

Logs dizem o que aconteceu. Métricas dizem como o sistema está. Traces dizem onde o tempo foi gasto em uma requisição.

Sistemas sem observabilidade são caixas pretas: quando algo quebra, você fica no escuro. Com os três pilares implementados, você diagnostica problemas em minutos.

Structured Logging com Pino

Logs em texto livre são inutilizáveis em produção. Use logs estruturados em JSON:

import pino from 'pino'

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  // Em produção: JSON. Em desenvolvimento: pretty print
  transport: process.env.NODE_ENV !== 'production'
    ? { target: 'pino-pretty' }
    : undefined,
  base: {
    service: 'api-backend',
    version: process.env.APP_VERSION || 'unknown',
    environment: process.env.NODE_ENV,
  },
  // Sanitizar campos sensíveis
  redact: ['req.headers.authorization', 'req.body.password', '*.password'],
})

export { logger }

// Uso em rotas
app.get('/users/:id', async (req, res) => {
  const log = logger.child({ userId: req.params.id, requestId: req.id })
  
  log.info('Buscando usuário')
  
  try {
    const user = await userService.findById(req.params.id)
    log.info({ userId: user.id }, 'Usuário encontrado')
    return res.json(user)
  } catch (error) {
    log.error({ err: error }, 'Erro ao buscar usuário')
    return res.status(500).json({ error: 'Erro interno' })
  }
})

Saída JSON estruturada — pode ser enviada para Loki, Elasticsearch, Datadog ou CloudWatch.

Métricas com Prometheus

import { register, Counter, Histogram, Gauge } from 'prom-client'

// Coletar métricas padrão do Node.js
collectDefaultMetrics({ register })

// Métricas de negócio
export const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total de requisições HTTP',
  labelNames: ['method', 'route', 'status_code'],
})

export const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duração das requisições HTTP em segundos',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
})

export const activeConnections = new Gauge({
  name: 'active_connections',
  help: 'Conexões ativas ao banco de dados',
})

// Middleware para capturar métricas automaticamente
app.use((req, res, next) => {
  const end = httpRequestDuration.startTimer()
  
  res.on('finish', () => {
    const labels = {
      method: req.method,
      route: req.route?.path || req.path,
      status_code: res.statusCode.toString(),
    }
    httpRequestsTotal.inc(labels)
    end(labels)
  })
  
  next()
})

// Endpoint de métricas para o Prometheus
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType)
  res.end(await register.metrics())
})

Tracing distribuído com OpenTelemetry

// instrumentation.ts — executar ANTES do app
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { Resource } from '@opentelemetry/resources'
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions'

const sdk = new NodeSDK({
  resource: new Resource({
    [SEMRESATTRS_SERVICE_NAME]: 'api-backend',
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://jaeger:4318/v1/traces',
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      // Instrumenta Express, HTTP, fetch, MySQL, Redis automaticamente
    }),
  ],
})

sdk.start()

Com isso, cada requisição gera um trace com spans mostrando tempo em cada serviço, query de banco, chamada Redis — sem alterar o código da aplicação.

Alertas inteligentes

Alerta apenas no que importa. Não alertar demais (alarme fadiga):

# alertmanager rules (Prometheus)
groups:
- name: api.critical
  rules:
  
  - alert: HighErrorRate
    # Mais de 1% de requests com erro 5xx nos últimos 5 minutos
    expr: |
      rate(http_requests_total{status_code=~"5.."}[5m]) 
      / rate(http_requests_total[5m]) > 0.01
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "Taxa de erros 5xx acima de 1%: {{ $value | humanizePercentage }}"

  - alert: HighP99Latency
    expr: |
      histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 2
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "P99 latência acima de 2s: {{ $value | humanizeDuration }}"

  - alert: ServiceDown
    expr: up{job="api-backend"} == 0
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Serviço api-backend está fora do ar"

Sentry para erros de frontend e backend

import * as Sentry from '@sentry/node'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: process.env.APP_VERSION,
  // Amostragem de traces: 10% em produção para não sobrecarregar
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
  beforeSend(event) {
    // Não reportar erros de validação de usuário (400s são esperados)
    if (event.extra?.statusCode === 400) return null
    return event
  },
})

// Capturar exceções não tratadas com contexto adicional
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  Sentry.withScope((scope) => {
    scope.setUser({ id: req.user?.id })
    scope.setExtra('requestId', req.id)
    Sentry.captureException(err)
  })
  
  res.status(500).json({ error: 'Erro interno do servidor' })
})

Dashboard Grafana recomendado

Panels essenciais no dashboard principal:

| Panel | Query | |---|---| | Requests/segundo | rate(http_requests_total[1m]) | | Taxa de erros (%) | rate(http_requests_total{status_code=~"5.."}[1m]) / rate(http_requests_total[1m]) | | Latência P50/P95/P99 | histogram_quantile(0.95, ...) | | CPU e memória | process_cpu_seconds_total, process_resident_memory_bytes | | Conexões de banco ativas | active_connections (gauge customizado) | | Lag de replicação MySQL | mysql_slave_status_seconds_behind_master |