Observability

Escopo: JavaScript. Visão transversal: shared/standards/observability.md.

Um sistema observável responde a uma pergunta: o que ele estava fazendo quando algo deu errado. Observability (observabilidade) é essa capacidade de entender o estado interno pelo que o sistema emite para fora, sem abrir o código. Em JavaScript e Node, quatro hábitos entregam isso: logar objetos estruturados em vez de strings concatenadas, escolher o nível de severidade certo, nunca deixar dado sensível vazar no log e carregar um correlation ID (identificador que liga todos os logs de uma mesma requisição). Os princípios que valem para qualquer linguagem estão em shared/standards/observability.md.

Conceitos fundamentais

ConceitoO que é
structured logging (log estruturado)Log emitido como objeto JSON com campos pesquisáveis, não string concatenada
log level (nível de log)Severidade: trace, debug, info, warn, error, fatal
correlation ID (ID de correlação)Identificador único por requisição que aparece em todos os logs do mesmo fluxo
redaction (mascaramento de dados sensíveis)Remoção ou mascaramento de campos sensíveis (senha, token, CPF) antes de emitir
PII (Personally Identifiable Information · Informação Pessoal Identificável)Dado que identifica um indivíduo; nunca sai cru no log
trace (rastro)Caminho de uma requisição atravessando múltiplos serviços; cada salto é um span
span (trecho)Unidade de trabalho dentro de um trace; tem início, fim e atributos
metric (métrica)Valor numérico agregado (contador, gauge, histograma) emitido periodicamente

Logging estruturado

Log é dado, não texto para humano ler. Uma string concatenada com console.log não dá para filtrar nem agregar: a ferramenta recebe uma frase pronta e não sabe onde termina o orderId. Emita um objeto com Pino (ou Winston) e cada campo vira uma propriedade que você pesquisa e agrupa.

❌ Ruim: string concatenada, ilegível para ferramentas
logger.info(`Order ${order.id} processed by user ${user.id}, total: $${order.total}`);
logger.error(`Payment failed: ${error.message} for order ${order.id}`);
✅ Bom: objeto estruturado com campos semânticos
const orderContext = { orderId: order.id, userId: user.id, total: order.total };
logger.info(orderContext, "order processed");

const paymentErrorContext = { orderId: order.id, error: error.message };
logger.error(paymentErrorContext, "payment failed");

Níveis de log

O nível de um log diz o quão urgente ele é. info narra o fluxo normal, warn marca algo estranho que ainda não quebrou nada, error marca uma falha que precisa de atenção. Jogar tudo em console.log apaga essa diferença e afoga o erro real no meio do ruído.

❌ Ruim: console.log para tudo, sem distinção de severidade
console.log("Checkout started");
console.log(`Database query took ${durationMs}ms`);

console.log(`User ${userId} not found`);
✅ Bom: nível correto por situação
const checkoutContext = { cartId };
logger.info(checkoutContext, "checkout started");

const slowQueryContext = { cartId, durationMs };
logger.warn(slowQueryContext, "slow database query");

const userNotFoundContext = { cartId, userId };
logger.error(userNotFoundContext, "user not found during checkout");

O que nunca logar

Log vaza. Ele vai para arquivo, para serviço de terceiros, para a tela de quem dá suporte. Senha, token, número de cartão e qualquer PII (Personally Identifiable Information · Informação Pessoal Identificável) nunca entram no log. Registre o identificador do recurso, uma referência que você resolve depois, e pare por aí.

❌ Ruim: PII e credenciais em log
logger.info({ email: user.email, password: user.password }, "login attempt");
logger.info({ cardNumber: payment.card, cvv: payment.cvv }, "payment initiated");

logger.info({ token }, "user authenticated");
✅ Bom: IDs e referências, nunca dados sensíveis
const loginContext = { userId: user.id };
logger.info(loginContext, "login attempt");

const paymentContext = { paymentId: payment.id, last4: payment.lastFour };
logger.info(paymentContext, "payment initiated");

const authContext = { userId: user.id };
logger.info(authContext, "user authenticated");

ID de correlação

Sem um identificador comum, os logs de uma mesma requisição viram ilhas soltas e você não consegue reconstruir o que aconteceu. O correlation ID é o fio que costura essas ilhas: o mesmo valor aparece em todos os logs do fluxo. O AsyncLocalStorage do Node guarda esse valor por baixo e injeta em cada log, sem você passar o ID de função em função.

❌ Ruim: logs sem contexto de requisição
async function processOrder(orderId) {
  logger.info("processing order");

  const invoice = await buildInvoice(orderId);

  logger.info("order processed");

  return invoice;
}
// {"msg":"processing order"}: impossível saber qual request originou
✅ Bom: correlationId propagado via AsyncLocalStorage
// middleware/correlation.js
const requestStore = new AsyncLocalStorage();

export function correlationMiddleware(request, response, next) {
  const correlationId = request.headers["x-correlation-id"] ?? crypto.randomUUID();

  response.setHeader("x-correlation-id", correlationId);

  const store = { correlationId };
  requestStore.run(store, next);
}

export const logger = pino({
  mixin() {
    const context = requestStore.getStore();
    return context ?? {};
  },
});

// handler: correlationId incluído automaticamente em todos os logs
async function processOrder(orderId) {
  const startContext = { orderId };
  logger.info(startContext, "processing order");

  const invoice = await buildInvoice(orderId);

  const completedContext = { orderId, invoiceId: invoice.id };
  logger.info(completedContext, "order processed");

  return invoice;
}
// {"correlationId":"abc-123","orderId":"...","msg":"processing order"}

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.