Observability

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

O structured logging (log estruturado, o log emitido como objeto JSON em vez de frase solta) do JavaScript continua igual aqui. O que o TypeScript acrescenta é a garantia de que o log sai completo. Uma interface tipada para o logger obriga quem chama a passar um objeto, e um contexto tipado faz o compilador acusar o correlationId que alguém esqueceu de incluir. O campo que falta deixa de ser algo que você descobre no dia do incidente, quando o log não serve para nada.

Conceitos agnósticos: 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
Logger (interface tipada de log)Contrato que o logger cumpre; obriga quem chama a passar um objeto estruturado
log level (nível de log)Severidade tipada por união literal: `"trace"
correlation ID (ID de correlação)Identificador único por requisição que aparece em todos os logs do mesmo fluxo
LogContext (contexto tipado)Tipo do payload do log; campos obrigatórios não podem ser omitidos
redaction (redação)Remoção ou mascaramento de campos sensíveis 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

A interface do logger obriga o log a ser estruturado

Um logger sem tipo aceita logger.info("pedido criado " + orderId), e o resultado é uma frase. Buscar por ela no painel de logs significa procurar por texto, e filtrar por cliente ou por status deixa de ser possível, porque não existe campo para filtrar.

A interface tipada resolve na origem: o segundo parâmetro é um objeto, e quem chama não tem como passar uma string concatenada. Ela também deixa a implementação trocável, porque o código depende do contrato e não do Pino ou do Winston.

❌ Ruim: o logger sem tipo aceita a frase concatenada
// qualquer assinatura passa: strings, objetos, mistura
logger.info(`Order ${orderId} created`);
logger.error(error);
✅ Bom: a interface obriga quem chama a passar campos pesquisáveis
interface Logger {
  info(context: Record<string, unknown>, message: string): void;
  warn(context: Record<string, unknown>, message: string): void;
  error(context: Record<string, unknown>, message: string): void;
}

// uso
const orderContext = { orderId, customerId };
logger.info(orderContext, "order created");

O contexto de correlação declara os campos obrigatórios

O correlation ID (ID de correlação) é o que permite juntar todas as linhas de log de uma mesma requisição, mesmo quando ela passa por três serviços. Ele só serve se estiver em todas elas, e um contexto sem tipo não garante isso: basta alguém montar o objeto sem o campo, e aquele fluxo fica sem rastro.

AsyncLocalStorage com o tipo declarado transforma o esquecimento em erro de compilação. O correlationId faz parte do contrato do contexto, e um objeto sem ele não entra.

❌ Ruim: o contexto não tem tipo, e o campo pode faltar sem ninguém notar
const requestStore = new AsyncLocalStorage<Record<string, unknown>>();

export function correlationMiddleware(req: Request, res: Response, next: NextFunction): void {
  const correlationId = req.headers["x-correlation-id"] ?? crypto.randomUUID();
  requestStore.run({ correlationId }, next); // qualquer shape passa
}

// mixin: campo pode estar ausente sem erro de compilação
const context = requestStore.getStore();
logger.info({ ...context }, "processing"); // context pode ser undefined
✅ Bom: o contexto é tipado, e o campo que falta vira erro de compilação
interface RequestContext {
  correlationId: string;
}

const requestStore = new AsyncLocalStorage<RequestContext>();

export function correlationMiddleware(request: Request, response: Response, next: NextFunction): void {
  const correlationId =
    (request.headers["x-correlation-id"] as string | undefined) ?? crypto.randomUUID();

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

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

export function getRequestContext(): RequestContext | undefined {
  return requestStore.getStore();
}
// logger com mixin tipado
import pino from "pino";

export const logger = pino({
  mixin(): Record<string, unknown> {
    const context = getRequestContext();
    if (!context) return {};

    const mixin: Record<string, unknown> = { correlationId: context.correlationId };
    return mixin;
  },
});

O nível de log é uma união literal, não uma string

Com o nível declarado como string, logger.log("infoo", ...) compila, e a linha some do painel porque nenhum filtro conhece esse nível. A união literal ("trace" | "debug" | "info" | "warn" | "error") limita os valores aceitos aos que existem, e o erro de digitação vira erro de compilação.

❌ Ruim: o nível é uma string, e qualquer valor passa
function createLogger(level: string) {
  return pino({ level }); // "debugg", "infoo": sem erro de compilação
}
✅ Bom: union type nos níveis
type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace";

function createLogger(level: LogLevel = "info") {
  return pino({ level });
}

// "debugg" → erro de compilação

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