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:

typescript
# 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:

typescript
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:

yaml
# 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:

typescript
// 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/Tempo

Grafana Tempo como alternativa ao Jaeger

Backend de tracing mais integrado ao ecossistema Grafana:

yaml
# 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

Não quer configurar manualmente?

Implante o VPS para APIs em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Datadog/New Relic: setup em minutos, SaaS gerenciado, mas caro ($$) e dados no servidor deles. OpenTelemetry self-hosted: gratuito, dados na sua infra, padrão aberto (sem vendor lock-in), mas você gerencia. Para startups com orçamento apertado ou dados sensíveis: OpenTelemetry + Jaeger/Tempo. Para empresas que podem pagar pela conveniência: Datadog faz sentido.

Com sampling configurado corretamente: overhead < 1%. O sampling padrão captura 100% dos traces, para produção com alto tráfego: configure tail-based sampling (captura apenas traces com erro ou lentos) ou head-based sampling (X% aleatório). O custo real é o armazenamento de traces no Jaeger/Tempo, 1 GB/dia é típico para APIs médias.

Sim, @opentelemetry/instrumentation-pg captura cada query PostgreSQL como span filho, mostrando SQL executado, duração e parâmetros (com enhancedDatabaseReporting: true). Redis, HTTP outbound, gRPC e expressjs são igualmente instrumentados automaticamente. Você vê toda a cadeia sem adicionar uma linha de código às funções existentes.

Distributed tracing conecta a cadeia completa de chamadas de uma requisição que passa por múltiplos serviços, API gateway → serviço de auth → serviço de pedidos → banco → serviço de email. Para monolito em uma única VPS: traces ainda são úteis para ver latência por função. Para microsserviços ou APIs que chamam outros serviços externos: essencial para debugar onde o tempo está sendo gasto.

Jaeger badger (padrão): armazena localmente, configure SPAN_STORAGE_TYPE=badger com TTL de 7 dias (BADGER_SPAN_STORE_TTL=168h). Para retenção maior: use Jaeger com backend Cassandra ou Elasticsearch. Grafana Tempo com S3/MinIO como backend: armazenamento barato e ilimitado com compressão. Para auditorias: exporte traces críticos para parquet no S3.