Você já abriu os logs de produção tentando entender por que a API respondeu 500 e encontrou um console.log("erro aqui") sem timestamp, sem contexto e sem stack trace? Isso acontece em times de todos os tamanhos. Observabilidade não é sobre dashboards bonitos. É sobre conseguir responder “o que aconteceu?” sem precisar reproduzir o bug localmente.
Este tutorial cobre os três pilares que fazem diferença real no dia a dia: logs estruturados com pino, health checks honestos e métricas Prometheus para alertas. Nada de APM pago. Só o que uma aplicação Node.js comum precisa para você dormir tranquilo.
Resposta rápida
Para ter observabilidade em uma aplicação Node.js em produção, adicione três coisas: logs em formato JSON com pino (buscáveis na Guara Cloud), um endpoint /health que verifica banco e dependências (não só responde 200), e métricas Prometheus com prom-client para acompanhar latência, throughput e erros. A Guara Cloud coleta os logs de stdout automaticamente e pode raspar métricas via scraping endpoint.
Principais pontos
- Troque
console.logporpino. Logs JSON permitem filtrar por request ID, status code e duração direto na plataforma. - Seu health check precisa testar as dependências reais. Um
/healthque só retorna{"status":"ok"}não detecta banco fora do ar. - Métricas de latência em histograma são mais úteis que contadores simples. O percentil p99 mostra onde os usuários realmente sofrem.
- Adicione
requestIdem cada log de requisição. Sem correlação entre logs, debugar um erro específico vira trabalho de arqueologia.
Quando se aplica
Qualquer API Node.js em produção com usuários reais. Não importa se é Express, Fastify, NestJS ou Hono. Se a aplicação recebe tráfego e você precisa responder incidentes, esses três componentes (logs, health checks, métricas) são o mínimo necessário.
Quando não se aplica
Se o projeto é um script batch que roda e termina, a parte de métricas e health checks perde sentido. Para microsserviços com mais de 10 serviços e milhares de requisições por segundo, você provavelmente vai querer distributed tracing com OpenTelemetry, o que é um passo além do que este tutorial cobre.
Antes de começar
- Node.js 20 instalado localmente
- Uma API Node.js existente (Express, Fastify ou similar)
- Docker funcionando
- Conta na Guara Cloud para deploy
1. Logs estruturados com pino
console.log funciona em desenvolvimento. Em produção, ele gera texto sem formato que ninguém consegue filtrar. O pino resolve isso com JSON estruturado e performance alta (é um dos loggers mais rápidos do ecossistema Node).
Instale o pino e o middleware de HTTP:
npm install pino pino-http
Configure o logger em src/logger.js:
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
base: { service: 'minha-api' },
timestamp: pino.stdTimeFunctions.isoTime,
});
Agora integre com o Express em src/app.js:
import express from 'express';
import pinoHttp from 'pino-http';
import { logger } from './logger.js';
import { randomUUID } from 'crypto';
const app = express();
app.use(pinoHttp({
logger,
genReqId: () => randomUUID(),
customProps: () => ({ env: process.env.NODE_ENV }),
customSuccessMessage: (req, res) => `${req.method} ${req.url} ${res.statusCode}`,
customErrorMessage: (req, res, err) => `${req.method} ${req.url} ${res.statusCode} ${err.message}`,
}));
// Todos os logs agora incluem o requestId automaticamente
app.get('/pedidos', async (req, res) => {
req.log.info({ filtros: req.query }, 'Buscando pedidos');
// ...
});
Cada linha de log agora tem: timestamp ISO, nível, requestId, método, URL, status code e duração. Nos logs da Guara Cloud, você busca por exemplo requestId:"abc-123" e vê toda a jornada daquela requisição.
2. Health checks que testam algo de verdade
A maioria dos health checks que eu vejo em produção faz isso:
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
Isso diz que o processo está rodando. Não diz se o banco está acessível, se a API externa está respondendo, ou se a memória não estourou. Um health check honesto verifica as dependências:
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
app.get('/health', async (req, res) => {
const checks = {};
let healthy = true;
// Verifica banco de dados
try {
const start = Date.now();
await pool.query('SELECT 1');
checks.database = { status: 'up', latency: Date.now() - start };
} catch (err) {
checks.database = { status: 'down', error: err.message };
healthy = false;
}
// Verifica serviço externo (se aplicável)
try {
const resp = await fetch(process.env.PAYMENT_API_URL + '/ping', {
signal: AbortSignal.timeout(3000),
});
checks.paymentApi = { status: resp.ok ? 'up' : 'degraded' };
} catch {
checks.paymentApi = { status: 'down' };
healthy = false;
}
checks.uptime = process.uptime();
checks.memory = process.memoryUsage.rss();
res.status(healthy ? 200 : 503).json(checks);
});
A Guara Cloud usa esse endpoint para decidir se o container está saudável. Se retorna 503, a plataforma reinicia o serviço e manda tráfego só quando ele volta. Isso é muito melhor que ter usuários recebendo erro e você descobrindo horas depois.
Um detalhe importante: o health check roda a cada 30 segundos por padrão. Queries pesadas nele vão sobrecarregar o banco. Mantenha as verificações leves (SELECT 1, não SELECT com JOIN).
Variáveis de ambiente para observabilidade
| Nome | Valor |
|---|---|
LOG_LEVEL | info (use debug só em staging) |
NODE_ENV | production |
DATABASE_URL | postgres://user:pass@host/db |
METRICS_ENABLED | true |
3. Métricas Prometheus com prom-client
Métricas servem para duas coisas: entender padrões de uso (pico às 14h, latência piora depois do deploy) e configurar alertas automáticos. O prom-client é a biblioteca padrão para expor métricas no formato Prometheus.
npm install prom-client
Configure em src/metrics.js:
import client from 'prom-client';
// Coleta métricas padrão do Node (CPU, memória, event loop lag, GC)
client.collectDefaultMetrics({ prefix: 'nodejs_' });
export const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total de requisições HTTP por método, rota e status',
labelNames: ['method', 'route', 'status'],
});
export const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duração de requisições HTTP em segundos',
labelNames: ['method', 'route', 'status'],
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
});
export const dbQueryDuration = new client.Histogram({
name: 'db_query_duration_seconds',
help: 'Duração de queries no banco de dados',
labelNames: ['operation', 'table'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});
Registre as métricas em cada requisição (middleware Express):
import { httpRequestsTotal, httpRequestDuration } from './metrics.js';
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer({
method: req.method,
route: req.route?.path || req.path,
});
res.on('finish', () => {
const labels = {
method: req.method,
route: req.route?.path || req.path,
status: res.statusCode,
};
httpRequestsTotal.inc(labels);
end(labels);
});
next();
});
Exponha no endpoint /metrics:
import client from 'prom-client';
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
Com isso, você tem métricas de:
http_request_duration_seconds_bucket: percentis de latência por rotahttp_requests_total: throughput por status (200, 404, 500)nodejs_eventloop_lag_seconds: se o event loop está bloqueadonodejs_heap_size_used_bytes: uso de memória
Um alerta útil baseado nessas métricas: “disparar se o p99 de latência passar de 2 segundos por mais de 5 minutos.” Isso pega degradação antes dos usuários reclamarem.
4. Proteja os endpoints internos
Metrics e health checks expõem informação sobre o sistema. Em ambientes com dados sensíveis, o /metrics pode revelar nomes de tabelas, padrões de rotas e volume de tráfego. Restrinja o acesso:
// Só responde se o header secreto está presente
app.use(['/metrics', '/health'], (req, res, next) => {
const token = req.headers['x-internal-token'];
if (token !== process.env.INTERNAL_TOKEN && req.ip !== '127.0.0.1') {
return res.status(403).end();
}
next();
});
Na Guara Cloud, o scraping de métricas vem da rede interna da plataforma. O INTERNAL_TOKEN garante que só a infra autorizada acessa esses endpoints.
5. Dockerfile e deploy
O Dockerfile não precisa de nada especial para observabilidade. O importante é garantir que os logs vão para stdout (não para arquivo) e que os endpoints estão acessíveis:
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src/ ./src/
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
Publicando na Guara Cloud
- Push do Dockerfile e código para o repositório Git
- Crie um novo serviço na Guara Cloud apontando para o repositório
- Configure as variáveis de ambiente (LOG_LEVEL, DATABASE_URL, INTERNAL_TOKEN)
- Inicie o deploy e confira nos logs se os primeiros registros aparecem em JSON
- Acesse /health pela URL pública para confirmar que está retornando o status das dependências
guara deploy --name minha-api --env LOG_LEVEL=info --env METRICS_ENABLED=true Depois do deploy, os logs estruturados aparecem automaticamente no painel da Guara Cloud. Você pode filtrar por level:error, requestId, rota ou duração.
O que fazer com tudo isso
Ter logs, health checks e métricas funcionando é o início. O próximo passo é criar alertas. Alguns exemplos que eu uso e recomendo:
Taxa de erro alta: se mais de 5% das requisições retornarem 5xx nos últimos 5 minutos, notificar. Usa rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]).
Latência subindo: se o p99 passar de 1s por mais de 10 minutos. Usa histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[10m])).
Memory leak: se nodejs_heap_size_used_bytes cresce consistentemente por 2 horas sem liberar.
Na Guara Cloud, os logs já são coletados automaticamente. Você define filtros e alertas direto pela interface, sem precisar configurar um ELK separado.
Problemas comuns
- Problema Os logs aparecem como texto puro, não JSON
- Solução Verifique se está usando pino corretamente. Se usar console.log misturado com pino, os console.log não são formatados. Substitua todos os console por logger.
- Problema O health check retorna 503 mesmo com tudo funcionando
- Solução O timeout do health check (3s) pode ser curto se o banco está longe. Aumente para 5s ou use um endpoint de liveness que só checa o processo, não as dependências.
- Problema O /metrics retorna erro 500
- Solução Geralmente é um label duplicado no registro de métricas. Confira se cada combinação de nomes de métrica e labels é única.
- Problema Alta cardinalidade nas métricas (muitos labels diferentes)
- Solução Nunca use valores de usuário como label (userId, email). Use apenas valores finitos: method, route, status. Rotas com parâmetro (/:id) devem ser normalizadas.
- Problema Logs muito verbosos e caros de armazenar
- Solução Use LOG_LEVEL=info em produção e configure sampling para rotas de alto volume (health checks, endpoints de status não precisam ser logados).
Preciso de OpenTelemetry para observabilidade em Node.js?
Não necessariamente. OpenTelemetry é útil para distributed tracing, quando uma requisição passa por vários serviços. Se você tem um monolíto ou poucos serviços, pino + prom-client resolve 90% dos casos com muito menos complexidade.
Qual a diferença entre liveness e readiness probe?
Liveness verifica se o processo está vivo (responde rápido, sem dependências). Readiness verifica se está pronto para receber tráfego (banco conectado, cache carregado). Use /health/readiness para readiness e /health/liveness para liveness.
Devo logar o body da requisição?
Em produção, não. Logs com body aumentam volume de armazenamento e podem conter dados sensíveis (PII, tokens). Log só método, URL, status e duração. Se precisar do body para debug, use staging com LOG_LEVEL=debug.
Quanto de overhead o pino adiciona?
Praticamente zero. Pino serializa JSON em um buffer separado da thread principal e é considerado um dos loggers mais rápidos para Node.js. O impacto em latência é menor que 1ms por requisição.
Monitore suas aplicações na Guara Cloud
Logs estruturados coletados automaticamente, health checks gerenciados e métricas acessíveis. Tudo em infraestrutura brasileira com cobrança em Real.