Com 12 serviços NestJS conversando por HTTP e RabbitMQ, a pergunta "por que esse cadastro demorou?" deixa de ter resposta olhando um log só. Na PokerNetic eu construí a observabilidade da plataforma inteira, do SDK nos serviços até os alertas. Aqui conto como ela funciona e o que aprendi pelo caminho.
O que eu queria resolver
- Seguir uma requisição de ponta a ponta, passando por serviços diferentes e pela fila.
- Saber de um problema antes do usuário: erros 5xx, latência, CPU, memória, disco e reinícios.
- Ir de um trace para os logs e as métricas daquele momento com um clique.
- Fazer tudo isso com custo previsível.
flowchart LR
subgraph back["Backend"]
svc["12 serviços NestJS: SDK OpenTelemetry e logs Pino com trace_id"]
end
subgraph front["Navegador"]
browser["Front-ends Next.js: traces, Web Vitals e erros, amostra de 10%"]
end
edge["Relay de borda: valida a origem e injeta a credencial"]
nginx["NGINX: ingest com autenticação"]
collector["OpenTelemetry Collector: limite de memória, limpeza de atributos, amostra de 20%, batch"]
loki["Loki: logs, 7 dias"]
tempo["Tempo: traces, 7 dias"]
prom["Prometheus: métricas, 30 dias"]
grafana["Grafana: 10 dashboards e 12 alertas"]
chat["Canal de alertas"]
svc -->|"OTLP com gzip"| collector
browser -->|OTLP| edge
edge --> nginx
nginx --> collector
collector --> loki
collector --> tempo
collector --> prom
tempo -->|"service graph e span metrics"| prom
loki --> grafana
tempo --> grafana
prom --> grafana
grafana -->|alertas| chat
class svc tech-nestjs
class nginx tech-nginx
Instrumentação dos serviços
Cada serviço tem um arquivo de tracing importado na primeira linha do main, que só chama uma função da lib compartilhada com o nome do serviço. Como o projeto é CommonJS, isso garante que os hooks fiquem registrados antes de o código carregar pg, ioredis e amqplib.
A lib sobe o SDK do OpenTelemetry com exportadores OTLP para traces, métricas e logs, todos com gzip, e liga as instrumentações automáticas de HTTP, Express, NestJS, PostgreSQL, Redis, RabbitMQ, Pino e do runtime do Node, além de métricas de host, porque o PaaS fica fora do alcance do cAdvisor.
Os atributos de recurso identificam serviço, versão e ambiente. O ambiente vem de uma variável própria, porque o PaaS define NODE_ENV como produção também em staging. E o id da instância é estável, tirado da réplica: com um UUID por boot, cada reinício criava séries novas e uma métrica chegou a milhares de séries fantasmas.
Logs que apontam para o trace
Os logs saem pelo Pino com trace_id e span_id em toda linha, em dois destinos: o stdout legível e um registro OTLP estruturado que vai para o Loki. Os serializers de requisição e resposta são enxutos, porque antes os headers eram a maior parte dos bytes de log, e campos como authorization, cookie, password e tokens são mascarados antes de sair do processo.
A fila sem perder o fio
A propagação pelo RabbitMQ não tem código manual: a instrumentação do amqplib coloca o traceparent nos headers da mensagem e o consumidor extrai do mesmo lugar. Funciona porque os clientes de fila do Nest rodam sobre amqplib. Para validar, um cadastro precisa mostrar auth, comunicação e afiliados no mesmo trace.
O navegador, sem credencial no bundle
Os front-ends Next.js mandam spans de carregamento, fetch, cliques e tarefas longas, os Web Vitals e os erros não tratados, com amostra de 10%. A telemetria não vai direto para o ingest: passa por um relay de borda que aceita só os três caminhos OTLP e injeta a credencial a partir de um segredo do próprio relay. Assim nenhuma credencial aparece no JavaScript público.
O Collector controla custo e cardinalidade
- Proteção: um limitador de memória na frente de tudo.
- Limpeza: atributos de processo e de sistema operacional são removidos antes de virarem labels.
- Labels úteis: ambiente e versão viram dimensões das métricas.
- Amostragem: um sampler probabilístico de 20%, sem estado, pelo hash do trace_id.
- Envio: batch em todos os pipelines e, na exportação para o Tempo, fila e retry limitados.
O Tempo ainda gera o service graph e métricas de span, que vão para o Prometheus por remote write, e do trace o Grafana leva aos logs e às métricas.
Duas lições que custaram caro
A primeira foi a cardinalidade. Um label sem limite, somado à temporalidade cumulativa das métricas, multiplicou o tráfego de telemetria. A resposta foi atacar em vários pontos: poucos labels no Loki e o resto como metadados, id de instância estável, atributos removidos no Collector, cAdvisor podado, um alerta de streams no Loki e uma sonda que avisa quando o número de séries passa do limite. O gzip e os logs enxutos foram as maiores reduções de volume.
A segunda foi a amostragem. Comecei com tail sampling, para guardar sempre os traces com erro, mas ele segurava traces em memória até travar o pipeline. Troquei por um sampler probabilístico sem estado. Perdi a garantia de guardar 100% dos traces com erro, mas os logs de erro continuam inteiros no Loki, com o trace_id para quando o trace existir.
Dashboards e alertas
São 10 dashboards: uma visão geral do que está pegando fogo, o detalhe por serviço com p50, p95 e p99, recursos de contêiner, métricas de negócio, o mapa de serviços, a saúde do Loki, do Tempo e do Collector e duas visões do front-end com Web Vitals e erros do navegador. As 12 regras de alerta cobrem taxa de 5xx acima de 1%, p95 acima de 500 ms, CPU, memória e reinícios dos contêineres da stack, disco do host, alvos fora do ar e a saúde da própria stack, e chegam num canal de chat agrupadas para não virar ruído.
Infra e entrega
A stack roda self-hosted numa VPS com Docker Compose: Loki, Tempo, Prometheus, Collector, Grafana, cAdvisor e node-exporter, com o ingest público atrás do NGINX e autenticação. Logs e traces ficam 7 dias e métricas, 30. Um push na main dispara o deploy por SSH, que só reinicia o Collector quando a configuração dele mudou, e o CI valida as configurações, sobe a stack inteira e roda um smoke test que manda um trace, um log e uma métrica e confere a chegada de cada um.
O que ainda falta
- Armazenamento em objeto para Loki e Tempo.
- Ajuste fino dos alertas depois da troca de amostragem.
- SSO no Grafana.
