OpenTelemetry em Node.js: tracing distribuído
Quando uma requisição leva 2 segundos, logs e métricas dizem "demorou" — OpenTelemetry tracing mostra exatamente onde: 50ms na autenticação, 1,8s em uma query PostgreSQL lenta, 150ms na chamada à API de pagamento. Traces distribuídos conectam toda a cadeia de chamadas de uma requisição, do browser ao banco de dados.
Instrumentação automática do Node.js
Configurar OpenTelemetry com instrumentação zero-code:
# npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
# npm install @opentelemetry/exporter-otlp-http
// instrumentation.ts — carregar ANTES do código da aplicação
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { Resource } from '@opentelemetry/resources'
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'minha-api',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces', // Jaeger ou Grafana Tempo
}),
instrumentations: [
getNodeAutoInstrumentations({
// Instrumentação automática de:
// HTTP, Express, PostgreSQL, Redis, gRPC, fetch, etc.
'@opentelemetry/instrumentation-pg': { enhancedDatabaseReporting: true },
'@opentelemetry/instrumentation-http': { ignoreIncomingRequestHook: (req) => req.url === '/metrics' },
}),
],
})
sdk.start()
// package.json — carregar antes do código:
// "start": "node -r ./dist/instrumentation.js dist/index.js"Criar spans customizados
Rastrear operações específicas da sua aplicação:
import { trace, SpanStatusCode, context } from '@opentelemetry/api'
const tracer = trace.getTracer('minha-api')
async function processarPedido(id: string) {
// Criar span pai:
return tracer.startActiveSpan('processarPedido', async (span) => {
span.setAttribute('pedido.id', id)
span.setAttribute('pedido.tipo', 'checkout')
try {
// Spans filhos automáticos (pg, redis instrumentados automaticamente)
const usuario = await db.query('SELECT * FROM usuarios WHERE id = $1', [id])
// Span filho manual para lógica de negócio:
const desconto = await tracer.startActiveSpan('calcularDesconto', async (s) => {
s.setAttribute('usuario.plano', usuario.plano)
const d = await calcularDesconto(usuario)
s.setAttribute('desconto.valor', d)
s.end()
return d
})
span.setAttribute('pedido.desconto', desconto)
span.setStatus({ code: SpanStatusCode.OK })
return { usuario, desconto }
} catch (err) {
span.recordException(err as Error)
span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message })
throw err
} finally {
span.end()
}
})
}Instalar Jaeger para visualizar traces
Backend de tracing self-hosted com Jaeger:
# docker-compose.yml — adicionar Jaeger:
services:
jaeger:
image: jaegertracing/all-in-one:latest
restart: always
ports:
- "127.0.0.1:16686:16686" # UI web
- "127.0.0.1:4317:4317" # OTLP gRPC
- "127.0.0.1:4318:4318" # OTLP HTTP
environment:
COLLECTOR_OTLP_ENABLED: "true"
SPAN_STORAGE_TYPE: badger # storage em arquivo (simples)
volumes:
- jaeger_data:/badger
volumes:
jaeger_data:
# Expor Jaeger UI via Nginx (acesso restrito):
# /etc/nginx/conf.d/jaeger.conf:
server {
listen 443 ssl http2;
server_name tracing.seudominio.com.br;
allow 177.10.0.1;
deny all;
location / {
proxy_pass http://127.0.0.1:16686;
proxy_set_header Host $host;
}
}Correlacionar traces com logs
Adicionar trace ID e span ID nos logs para correlação:
// Adicionar trace ID nos logs do Pino para correlação com Jaeger:
import pino from 'pino'
import { trace, context } from '@opentelemetry/api'
const logger = pino()
// Middleware que injeta trace ID em cada requisição:
app.use((req, res, next) => {
const span = trace.getActiveSpan()
const spanContext = span?.spanContext()
// Criar child logger com trace context:
req.log = logger.child({
traceId: spanContext?.traceId,
spanId: spanContext?.spanId,
})
next()
})
// Usar req.log nas rotas:
app.post('/pedidos', async (req, res) => {
req.log.info('Iniciando criação de pedido')
try {
const pedido = await criarPedido(req.body)
req.log.info({ pedidoId: pedido.id }, 'Pedido criado com sucesso')
res.json(pedido)
} catch (err) {
req.log.error({ err }, 'Erro ao criar pedido')
throw err
}
})
// No Grafana: buscar pelo traceId nos logs do Loki
// e saltar direto para o trace no Jaeger/TempoGrafana Tempo como alternativa ao Jaeger
Backend de tracing mais integrado ao ecossistema Grafana:
# docker-compose.yml — Grafana Tempo:
services:
tempo:
image: grafana/tempo:latest
restart: always
ports:
- "127.0.0.1:4317:4317" # OTLP gRPC
- "127.0.0.1:4318:4318" # OTLP HTTP
- "127.0.0.1:3200:3200" # Tempo API (para Grafana)
command: -config.file=/etc/tempo/config.yaml
volumes:
- tempo_data:/var/tempo
- ./tempo-config.yaml:/etc/tempo/config.yaml
# tempo-config.yaml:
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
http:
grpc:
storage:
trace:
backend: local
local:
path: /var/tempo/traces
wal:
path: /var/tempo/wal
# No Grafana — adicionar Tempo como data source:
# Configuration → Data Sources → Tempo
# URL: http://tempo:3200
# Correlação automática Loki → Tempo no Grafana:
# No data source Loki: adicionar derived field "traceID"
# Regex: "traceId":"([a-f0-9]{32})"
# Internal link → Tempo data source$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.