Aplicação Node.js em produção sem telemetria é operar no escuro

1 mês atrás

9 min de leitura

Sumário Executivo

Observabilidade é a capacidade de entender o que acontece dentro de um sistema a partir dos sinais que ele emite, substituindo “chute” por evidência. Em aplicações modernas, especialmente em Node com múltiplos serviços, filas e workers, a falta de telemetria torna o diagnóstico lento e reativo. Esse artigo propõe estruturar esse “painel de controle” consolidando os três pilares de telemetria (logs, métricas e traces) com OpenTelemetry, para reduzir tempo de investigação, aumentar previsibilidade e sustentar escala com segurança.

O conteúdo explica (1) o papel complementar de logs, métricas e traces, (2) como começar rápido com auto-instrumentação “zero-code” e evoluir com instrumentação manual para adicionar contexto de negócio, e (3) como montar uma stack reproduzível baseada em Grafana usando OTel Collector como hub (OTLP), Tempo para traces, Loki para logs, Prometheus para métricas e Alloy para coletar logs de containers.

Introdução

De maneira objetiva, observabilidade é a capacidade de entender o que está acontecendo dentro de um sistema através dos sinais que ele emite. Em vez de adivinhar por que uma requisição falhou, você pergunta ao sistema, ele responde.

O problema é que, por padrão, aplicações Node não emitem quase nada de útil para realizar um monitoramento de maneira profissional. É como voar sem painel de instrumentos: você sabe que está no ar, mas não sabe para onde vai. 

A proposta é estruturar esse “painel de controle”: consolidar os três pilares de telemetria (métricas, logs e traces) e aplicar instrumentação com OpenTelemetry, criando uma base de visibilidade operacional que reduz tempo de diagnóstico, melhora previsibilidade e sustenta escala com segurança.

Por que observabilidade importa?

Em arquiteturas modernas, com múltiplos serviços, comunicação assíncrona, filas e workers, a pergunta “o que está acontecendo?” deixa de ter uma resposta direta, óbvia, como acontecia em aplicações monolíticas mais simples. Sem sinais correlacionados entre os componentes, a operação vira um ciclo de reação: cada incidente expõe um novo sintoma, mas evidencia raramente a causa raiz. Realizar diagnóstico, entender um problema e até mesmo um debug pode se tornar um grande desafio.

Com observabilidade, o fluxo passa a ser visível ponta a ponta, do ingresso da requisição, atravessando dependências e mensageria, até o processamento em fila e a conclusão do trabalho. Isso permite que a equipe responda não apenas “o que falhou?”, mas principalmente “por que falhou?” e “onde falhou?”, mudando a postura de combate a incêndios para investigação orientada por evidências.

Os três pilares: logs, métricas e traces

Toda telemetria parte de três tipos de sinais, cada um cobrindo um ângulo diferente:

Logs são mensagens com *timestamp* emitidas por serviços. Registram eventos pontuais, como a criação de um usuário, uma falha de conexão com o banco ou uma mudança de status. Sozinhos, não rastreiam o fluxo de uma requisição, mas mostram o que aconteceu num momento específico.

Métricas são números que evoluem ao longo do tempo: taxa de erro, latência média, uso de CPU, requisições por segundo. Mostram quando algo saiu do padrão, mas não explicam o motivo.

Traces (rastreamento distribuído) apresenta a reconstrução ponta a ponta do caminho de uma requisição através de serviços, dependências, bancos de dados e mensageria. Evidenciam onde o tempo foi consumido, quais dependências foram acionadas e em que etapa a falha surgiu, permitindo diagnosticar a causa raiz com evidências, sem depender de reprodução local do problema.

Instrumentação prática com OpenTelemetry

OpenTelemetry (OTel) é o padrão aberto para instrumentação de aplicações. Ele surgiu para evitar lock-in com fornecedores de aplicações de monitoramento. Você, por exemplo, instrumenta uma vez e troca o backend de Datadog para Grafana sem reescrever o código da aplicação.

Existem duas estratégias complementares para instrumentar uma aplicação com OpenTelemetry. A diferença principal está em o que é capturado automaticamente versus o que precisa ser modelado de propósito.

1) Auto-instrumentação (zero-code)

É a forma mais rápida de começar. Sem alterar o código de negócio, o OpenTelemetry “encaixa” instrumentações prontas em bibliotecas e frameworks amplamente usados e passa a gerar traces e spans automaticamente.

O que normalmente cobre bem:

  • Entrada e saída HTTP (servidor e cliente)
  • Frameworks web (ex.: Express)
  • Banco de dados (ex.: pg)
  • Mensageria (ex.: amqplib, dependendo do suporte e da forma de uso)

O que ela entrega na prática:

  • Visibilidade imediata do “caminho técnico” da requisição (camadas e dependências)
  • Base suficiente para começar a medir latência, gargalos e taxa de erro por endpoint

Limitação típica:

  • Mostra muito bem o que aconteceu tecnicamente, mas nem sempre deixa claro o que aquilo significa para o negócio (ex.: “validar proposta”, “calcular preço”, “emitir apólice”).

2) Instrumentação manual (code-based)

É usada quando se quer capturar etapas específicas do domínio ou pontos que não são instrumentados automaticamente. Nessa abordagem, o time cria spans de forma explícita para representar trechos relevantes do processamento.

Quando faz diferença:

  • Processos internos importantes que acontecem “dentro” do serviço (regras, validações, orquestração)
  • Trechos assíncronos e workers em que é necessário garantir contexto e correlação
  • Integrações customizadas ou bibliotecas sem instrumentação pronta
  • Necessidade de enriquecer o trace com atributos úteis (ex.: tenant, orderId, paymentMethod, queueName, featureFlag)

O que ela entrega na prática:

  • Diagnóstico com contexto: não só “qual chamada ficou lenta”, mas “qual etapa do processo degradou”
  • Capacidade de responder com precisão “por que falhou?” e “onde atacar?”

Como isso costuma ser aplicado no mundo real

A prática mais comum é combinar as duas:

  • Auto-instrumentação para cobrir o “esqueleto” técnico (HTTP, DB, mensageria, chamadas externas) com baixo esforço.
  • Instrumentação manual para adicionar “músculo” de contexto (spans de negócio, etapas críticas, correlação entre filas e processamento, atributos relevantes).

Uma forma simples de resumir:

  • Auto-instrumentação = cobertura rápida e ampla
  • Manual = precisão e contexto de negócio

Nesse artigo, para ilustrar uma forma de começar, vamos usar a abordagem zero-code

Para ativar a auto-instrumentação, basta instalar o pacote `@opentelemetry/auto-instrumentations-node` e importá-lo na primeira linha do entrypoint da aplicação:

import './instrumentation';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TraceLogger } from './trace.logger';

async function bootstrap() {

  const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
  });

  app.useLogger(new TraceLogger('Bootstrap'));
  await app.listen(process.env.PORT ?? 3001);
}

bootstrap();

Com isso, toda requisição HTTP e chamada ao banco geram *spans* automaticamente. O destino dos dados é configurado via variáveis de ambiente, normalmente no docker-compose.yml:

OTel Collector e a stack de observabilidade

A aplicação instrumentada emite sinais, mas quem processa e distribui tudo é a stack de observabilidade. O OpenTelemetry Collector é o hub central: recebe os dados em OTLP (OpenTelemetry Protocol) e os distribui para cada backend.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}

exporters:
  otlp/traces:
    endpoint: tempo:4317
    tls:
      insecure: true
  otlphttp/logs:
    endpoint: http://loki:3100/otlp
    tls:
      insecure: true
  prometheus:
    endpoint: "0.0.0.0:8889"
    resource_to_telemetry_conversion:
      enabled: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/traces]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/logs]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

 

Aqui será utilizada a stack da Grafana por um motivo simples: roda localmente, é leve e reproduzível, ideal para demonstrar o fluxo completo de observabilidade.

Grafana Tempo (traces)
Backend de rastreamento distribuído. Armazena spans e viabiliza investigação por trace ID e por consultas estruturadas com TraceQL, permitindo reconstruir o caminho ponta a ponta de uma requisição.

Grafana Loki (logs)
Backend de logs orientado a labels. Em vez de indexar o conteúdo dos logs, indexa metadados (labels), o que reduz custo e melhora performance. As consultas são feitas via LogQL, com foco em filtrar, correlacionar e explorar eventos.

Prometheus (métricas)
Backend de séries temporais. Realiza scrape das métricas expostas (neste caso, do endpoint do Collector na porta 8889) e armazena indicadores para consultas e alertas com PromQL.

Grafana (visualização e correlação)
Camada de exploração e diagnóstico. Conecta-se a Tempo, Loki e Prometheus como datasources e permite correlacionar sinais, por exemplo, partir de uma métrica anômala, abrir os traces daquele período e chegar nos logs relevantes em poucos cliques.

Grafana Alloy (coleta/agente)
Agente de coleta e encaminhamento. Faz discovery de containers Docker e envia logs de stdout/stderr diretamente para o Loki, evitando alterações no código da aplicação para captura de logs e padronizando a coleta em ambiente local.

O Collector é passivo: recebe o que a aplicação emite e envia via push OTLP. O Alloy é ativo: vai buscar logs diretamente nos containers. No mesmo projeto, o Collector recebe os sinais da aplicação e o Alloy captura os logs dos containers, cobrindo fontes diferentes de sinais.

O resumo do fluxo por tipo de sinal:

Essa separação significa que, quando você quiser trocar o provedor, de Grafana Cloud para Datadog, por exemplo, basta mudar a configuração do Collector. A aplicação não precisa saber para onde os dados vão. A tabela acima resume o fluxo por tipo de sinal.

Mão na massa: Primeiros sinais em 5 minutos.

  1. Suba a stack local
    • Clone o repositório (link nas referências) e inicie os serviços: docker compose up -d
  2. Gere tráfego para produzir telemetria
    • Crie um usuário para disparar logs/traces/métricas na aplicação:
    • curl --request POST \
        --url http://localhost:3001/users \
        --header 'content-type: application/json' \
        --data '{ "email": "fulano@eximia.co" }'
  3. Abra o Grafana e valide os sinais
    • Acesse http://localhost:3000 para abrir o Grafana.
    • Vá em Explore
    • Selecione o datasource Loki
    • Em Label filters, filtre por service_name = otel-app1

A partir daí, os logs do serviço ficam visíveis e já é possível correlacionar o que foi registrado com os traces e métricas gerados pelo mesmo fluxo.

O log ‘Usuário criado com sucesso’ aparece após a requisição de criação. Entre os campos, o mais importante é o trace_id: com ele é possível correlacionar o log com o trace.

Com logs no Loki e traces no Tempo, você tem dois backends separados. O que conecta os dois é o correlation ID: um identificador presente nos logs e nos spans, que permite ir do log direto para o trace no Grafana.

No OTel, esse identificador já existe: é o traceId, gerado e propagado automaticamente. O passo seguinte é garantir que ele apareça também nos logs.

Para isso, criamos um logger customizado que captura o span ativo e injeta o traceId em cada linha, estendendo o ConsoleLogger do NestJS:

import { trace } from '@opentelemetry/api';
import { ConsoleLogger } from '@nestjs/common';

function getTraceId(): string {
  const span = trace.getActiveSpan();
  return span?.spanContext().traceId ?? '';
}

export class TraceLogger extends ConsoleLogger {
  formatLine(level: string, message: string, context?: string): string {
    const traceId = getTraceId();
    const ctx = context ?? this.context ?? '';
    const tracePart = traceId ? ` [trace_id=${traceId}]` : '';
    return `${level.toUpperCase()}${tracePart} ${ctx} - ${message}`;
  }

  log(message: string, context?: string): void {
    process.stdout.write(this.formatLine('info', message, context) + '\n');
  }

  error(message: string, stack?: string, context?: string): void {
    process.stdout.write(this.formatLine('error', message, context) + '\n');
    if (stack) process.stdout.write(stack + '\n');
  }

  warn(message: string, context?: string): void {
    process.stdout.write(this.formatLine('warn', message, context) + '\n');
  }

  debug(message: string, context?: string): void {
    process.stdout.write(this.formatLine('debug', message, context) + '\n');
  }

  verbose(message: string, context?: string): void {
    process.stdout.write(this.formatLine('verbose', message, context) + '\n');
  }
}

Ao abrir um log no Grafana, aparece um link “View in Tempo” que leva direto para o trace completo daquela requisição, sem precisar copiar o ID manualmente.

Note as camadas de middleware do Express e como cada consulta ao banco aparece como um span separado.

Para explorar as métricas, acesse Metrics no Grafana. No card Select metric aparecem os gráficos gerados automaticamente. Separei algumas métricas úteis para acompanhar o comportamento de APIs e do runtime Node:

HTTP (APIs)

 

 

 

 

 

 

Runtime Node

Conclusão

Com o OpenTelemetry e alguns collectors, a aplicação passa a emitir logs, métricas e traces distribuídos sem alterar uma linha do código.

A próxima vez que uma requisição travar, ninguém vai ficar chutando. Você abre sua ferramenta de observabilidade, vai do log para o trace com um clique e corrige a causa, não o sintoma. Observabilidade não resolve *bugs*, mas elimina o tempo gasto na investigação.

Essa estrutura é a base para alertas proativos, SLOs (Service Level Objectives) baseados em dados e dashboards que o time de produto também consegue ler. A base já está no lugar: o sistema agora fala. Basta aprender a ouvir.

 

Baixe agora um exemplo completo dessa implementação

Você também pode gostar

Explicando a Arquitetura do OpenClaw, na prática

Sumário Executivo

A transição da interface conversacional para a interface agêntica muda o jogo: em vez de apenas responder, o sistema passa a agir, lembrar, orquestrar e executar, tornando-se infraestrutura operacional e não só uma “IA para conversa”.

Este texto descreve (1) uma arquitetura de referência para AI Agents baseada em três blocos com fluxos claros (Interaction, Core e Resources), (2) como esses blocos ganham forma concreta demonstrando a utilização em uma assistente virtual construída sobre o OpenClaw e (3) como maximizar o resultado preservando salvaguardas práticas importante, cobrindo riscos como prompt injection, data exfiltration e excessive agency, além de e práticas como least privilege, isolamento e human-in-the-loop.

Microfrontends como estratégia arquitetural de modernização

Modernização de frontend legado sem reescrita total utilizando microfrontends como estratégia de arquitetura incremental para migração gradual e convivência com legado.

FinOps e governança de custos para inteligência artificial

Guia prático de FinOps para IA/GenAI: pare de olhar custo de GPU e meça custo por resposta. Entenda como aplicar guardrails para controlar consumo sem perder qualidade.

DDD (Domain-Driven Design) faz sentido no frontend?

Organização do frontend com DDD (Domain Driven Design) ao desenvolver uma aplicação faz sentido? Como estruturar o frontend?

A Importância do Refinamento de Dados para Modelos de IA: Por que algoritmos bilionários continuam falhando com dados de centavos

Recomendações práticas de técnicas de refinamento de dados para garantir resultados precisos em modelos de Inteligência Artificial.

Por que usar mensageria se posso chamar o outro serviço via HTTP?

Quando HTTP síncrono vira o caminho crítico, falhas e latência se propagam em cascata. Veja quando a mensageria deixa de ser opcional, como ela desacopla serviços e absorve picos, e quais disciplinas (idempotência, DLQ e rastreabilidade) evitam colapsos em sistemas distribuídos.

O Teatro da Engenharia: Números Corretos, Decisões Erradas

Como não cair na armadilha das métricas de engenharia de software e fazer a gestão de times de desenvolvimento da maneira certa.

Exemplo completo de implementação de Open Telemetry aplicação Node.js

Exemplo pronto de OpenTelemetry em Node.js para reduzir tempo de investigação e aumentar previsibilidade. Inclui instrumentação, OTel Collector e visualização no Grafana (Tempo/Loki/Prometheus). Ideal para usar como referência e acelerar a adoção no seu time.

Modelo de Governança para tratar itens urgentes

Aprenda a governar a urgência e evitar o colapso da engenharia. Descubra como repriorizações sem critérios destroem a produtividade e o fluxo técnico.

Guia técnico: Comunicação Síncrona ou Assíncrona

HTTP ou Mensageria? Entenda os impactos do acoplamento temporal e saiba quando o modelo síncrono se torna um gargalo para sistemas distribuídos.

O mito do rewrite na modernização de legado

Descubra por que a modernização incremental é mais segura que o rewrite total. Evite armadilhas técnicas e preserve o conhecimento do seu negócio.

Você Não Quer Desenvolvedores Cuidadosos. Você Quer Fly-by-Wire

Substitua a dependência do erro humano pela Engenharia Fly-by-Wire: crie envelopes operacionais para garantir entregas rápidas, seguras e escaláveis.

7 Controles de FinOps que Cortam Gastos na Nuvem: Estratégias AWS e Multicloud

Recomendações das estratégias FinOps mais eficientes para reduzir até 40% dos custos de nuvem em 90 dias, em cenários reais multicloud, com governança e otimização sem sacrificar performance.

Assine nossa newsletter.

Assine nossa newsletter para ficar por dentro de todas as novidades de tecnologia.