OpenTelemetry no Node.js: instrumentação automática e manual na prática
1. Fundamentos do OpenTelemetry no Ecossistema Node.js
OpenTelemetry é um framework de observabilidade open-source que se tornou o padrão da indústria para coleta de telemetria em aplicações distribuídas. No ecossistema Node.js, ele oferece uma abordagem unificada para capturar traces, métricas e logs, permitindo que desenvolvedores entendam o comportamento de suas aplicações em produção.
Os componentes principais do OpenTelemetry incluem:
- API: Define interfaces para criar spans, métricas e logs sem depender de implementações específicas
- SDK: Implementação concreta que gerencia o ciclo de vida dos spans e a exportação dos dados
- Propagadores: Mecanismos para transmitir contexto entre serviços (ex.: W3C Trace Context)
- Exporters: Componentes que enviam os dados coletados para backends como Jaeger, Zipkin ou Grafana Tempo
Neste artigo, focaremos principalmente em traces, que representam o caminho de uma requisição através de múltiplos serviços e operações.
2. Configuração Inicial e Instrumentação Automática
A instrumentação automática é a forma mais rápida de começar. Com poucas linhas de código, você pode rastrear automaticamente requisições HTTP, chamadas a bancos de dados e operações em filas.
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start();
Com essa configuração, todas as requisições HTTP recebidas e enviadas pela sua aplicação serão automaticamente rastreadas. Por exemplo, um endpoint Express simples como:
const express = require('express');
const app = express();
app.get('/api/users', async (req, res) => {
const users = await fetchUsersFromDatabase();
res.json(users);
});
Gerará spans automaticamente para a requisição HTTP, a chamada ao banco de dados (se estiver usando um ORM instrumentado) e a resposta.
3. Instrumentação Manual: Criando Spans e Contextos Personalizados
Para operações específicas do seu domínio, a instrumentação manual oferece controle granular sobre o que é rastreado.
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('meu-servico');
async function processarPedido(pedidoId) {
const span = tracer.startSpan('processarPedido', {
attributes: {
'pedido.id': pedidoId,
'pedido.tipo': 'ecommerce'
}
});
try {
// Simulando operações assíncronas
await validarEstoque(pedidoId);
await processarPagamento(pedidoId);
span.addEvent('pedido.processado', {
'pedido.status': 'sucesso',
'timestamp': Date.now()
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
throw error;
} finally {
span.end();
}
}
Para propagar contexto entre funções assíncronas, use context.with():
const { context, trace } = require('@opentelemetry/api');
async function operacaoParalela() {
const parentSpan = tracer.startSpan('operacaoPrincipal');
await context.with(trace.setSpan(context.active(), parentSpan), async () => {
const [resultado1, resultado2] = await Promise.all([
tracer.startActiveSpan('subtarefa1', async (span) => {
// Lógica da subtarefa
span.end();
return 'resultado1';
}),
tracer.startActiveSpan('subtarefa2', async (span) => {
// Lógica da subtarefa
span.end();
return 'resultado2';
})
]);
});
parentSpan.end();
}
4. Integração com Frameworks e Bibliotecas Populares
A instrumentação automática cobre muitos casos, mas operações específicas exigem spans manuais. Veja exemplos com Redis e filas:
const redis = require('redis');
const client = redis.createClient();
async function buscarCache(chave) {
const span = tracer.startSpan('cache.consulta', {
attributes: {
'cache.chave': chave,
'cache.tipo': 'redis'
}
});
try {
const valor = await client.get(chave);
span.setAttribute('cache.hit', valor !== null);
return valor;
} finally {
span.end();
}
}
Para filas Bull:
const Queue = require('bull');
const emailQueue = new Queue('email');
emailQueue.process(async (job) => {
const span = tracer.startSpan('email.enviar', {
attributes: {
'email.destinatario': job.data.email,
'job.id': job.id
}
});
try {
await enviarEmail(job.data);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
5. Exportação de Dados e Configuração de Backends
Para produção, você precisará de um backend de tracing. O exemplo abaixo configura exportação via OTLP para um collector OpenTelemetry:
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const exporter = new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
});
const sdk = new NodeSDK({
spanProcessor: new BatchSpanProcessor(exporter, {
maxExportBatchSize: 512,
scheduledDelayMillis: 5000
}),
instrumentations: [getNodeAutoInstrumentations()]
});
O BatchSpanProcessor agrupa spans em lotes antes de enviá-los, reduzindo o número de requisições HTTP e melhorando a performance.
6. Boas Práticas e Padrões em Aplicações Reais
Estruture seus spans em camadas seguindo a arquitetura da aplicação:
// controller → service → repository
async function criarUsuario(req, res) {
return tracer.startActiveSpan('controller.criarUsuario', async (span) => {
try {
const usuario = await usuarioService.criar(req.body);
span.setAttribute('usuario.id', usuario.id);
res.status(201).json(usuario);
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(error);
res.status(500).json({ error: error.message });
}
});
}
Para tratamento de erros, sempre registre exceções e defina status ERROR:
try {
// operação
} catch (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
}
7. Monitoramento de Performance e Troubleshooting
Com traces bem estruturados, você pode derivar métricas importantes:
- Latência por endpoint: Calcule a duração média dos spans de controller
- Taxa de erro: Conte spans com status ERROR dividido pelo total
- Gargalos: Identifique spans com duração anormalmente alta
Problemas comuns e soluções:
| Problema | Causa | Solução |
|---|---|---|
| Spans perdidos | Contexto não propagado em callbacks | Usar context.bind() ou async_hooks |
| Exportação lenta | Muitos spans individuais | Aumentar maxExportBatchSize |
| Memória alta | Spans não finalizados | Verificar span.end() em todos os caminhos |
8. Próximos Passos e Considerações para Produção
Para ambientes de produção, considere:
Sampling head-based: Amostra decisões no início do trace:
const { ParentBasedSampler, TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-node');
const sdk = new NodeSDK({
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1) // 10% dos traces
})
});
Correlação com logs: Inclua traceId em todas as suas mensagens de log:
const { trace } = require('@opentelemetry/api');
const spanContext = trace.getSpan(context.active())?.spanContext();
console.log(`[traceId: ${spanContext?.traceId}] Usuário criado com sucesso`);
O futuro do OpenTelemetry inclui a Logs Bridge API, que permitirá correlação nativa entre logs, traces e métricas, unificando completamente a observabilidade.
Referências
- OpenTelemetry Node.js Documentation — Documentação oficial para instrumentação em Node.js, incluindo guias de instalação e configuração
- OpenTelemetry Automatic Instrumentations for Node.js — Pacote de instrumentações automáticas para Node.js, com suporte a Express, HTTP, MongoDB e mais
- OpenTelemetry Sampling Documentation — Conceitos e estratégias de sampling para controle de custos em produção
- Grafana Tempo Documentation — Guia completo para configurar o Grafana Tempo como backend para traces OpenTelemetry
- W3C Trace Context Specification — Especificação oficial do padrão de propagação de contexto entre serviços distribuídos
- OpenTelemetry Collector Documentation — Documentação do OpenTelemetry Collector, componente essencial para roteamento e processamento de telemetria
- Node.js Async Hooks Documentation — Documentação oficial do Node.js sobre async_hooks, utilizado pelo OpenTelemetry para propagação de contexto