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.
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 |
