DevOps

Rastreamento Distribuído com OpenTelemetry Já leu

13 min de leitura

Rastreamento Distribuído com OpenTelemetry
Considere o seguinte cenário: o dashboard de monitoramento mostra que a latência P95 da API aumentou de 120ms para 850ms às 15h43. Os logs de erro não mostram nada anormal — as requisições estão completando com sucesso,

Considere o seguinte cenário: o dashboard de monitoramento mostra que a latência P95 da API aumentou de 120ms para 850ms às 15h43. Os logs de erro não mostram nada anormal — as requisições estão completando com sucesso, apenas lentamente. A taxa de erros continua em zero. O time sabe que há um problema, mas não sabe onde.

A API chama um serviço de autenticação, que por sua vez consulta o banco de dados de usuários. Ela também chama um serviço de inventário, que consulta outro banco de dados e um cache Redis. Uma única requisição ao endpoint /checkout pode envolver seis serviços diferentes e doze chamadas de rede.

Com métricas e logs, é possível saber que o problema existe. Com rastreamento distribuído, é possível saber exatamente onde ele está — qual serviço, qual operação, qual linha de código contribuiu com mais tempo para aquela requisição de 850ms.

O rastreamento distribuído captura o caminho completo de uma requisição através de todos os serviços que ela toca, atribuindo a cada segmento um intervalo de tempo chamado span. O conjunto de spans de uma requisição forma um trace — uma árvore que visualiza a causalidade e a temporalidade de toda a operação.


OpenTelemetry: O Padrão da Indústria

Durante anos, cada fornecedor de observabilidade — Datadog, New Relic, Jaeger, Zipkin — tinha seu próprio SDK proprietário. Instrumentar uma aplicação significava depender de um fornecedor específico. Migrar para outro exigia reinstrumentar todo o código.

O OpenTelemetry resolve esse problema definindo um padrão aberto e agnóstico de fornecedor para coleta de telemetria — métricas, logs e traces. Uma aplicação instrumentada com OpenTelemetry pode exportar dados para Jaeger, Zipkin, Datadog, Grafana Tempo ou qualquer outro backend sem alterar o código da aplicação.

A arquitetura do OpenTelemetry tem três componentes principais:

SDK — a biblioteca que roda dentro da aplicação e coleta telemetria. Disponível para Node.js, Python, Java, Go, .NET e outras linguagens.

API — a interface de programação usada para criar spans e registrar atributos. Separada do SDK para que bibliotecas possam ser instrumentadas sem depender de uma implementação específica.

Collector — um processo independente que recebe telemetria de múltiplas aplicações, processa e exporta para um ou mais backends. Atua como um proxy de telemetria.


Instrumentando uma Aplicação Node.js

A instrumentação automática do OpenTelemetry para Node.js captura traces de HTTP, banco de dados, Redis e dezenas de outras bibliotecas sem modificar o código da aplicação. A instrumentação manual complementa com spans específicos do domínio do negócio.

Instalação das dependências:

npm install \
  @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions

Arquivo de instrumentação — deve ser carregado antes de qualquer outro módulo:

// src/instrumentation.js
'use strict';

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');

// Define os atributos do recurso — identificam a aplicação nos traces
const resource = new Resource({
  [SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'minha-api',
  [SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION || '0.0.0',
  [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
});

// Exportador de traces — envia para o OpenTelemetry Collector
const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
    || 'http://otel-collector:4318/v1/traces',
});

// Exportador de métricas
const metricExporter = new OTLPMetricExporter({
  url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
    || 'http://otel-collector:4318/v1/metrics',
});

const sdk = new NodeSDK({
  resource,
  traceExporter,
  metricReader: new PeriodicExportingMetricReader({
    exporter: metricExporter,
    exportIntervalMillis: 15000,
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      // Instrumenta automaticamente: HTTP, Express, PostgreSQL,
      // Redis, MongoDB, gRPC e dezenas de outras bibliotecas
      '@opentelemetry/instrumentation-http': {
        enabled: true,
        // Ignora o endpoint de métricas para não poluir os traces
        ignoreIncomingRequestHook: (req) => {
          return req.url === '/metrics' || req.url === '/health';
        },
      },
      '@opentelemetry/instrumentation-express': {
        enabled: true,
      },
      '@opentelemetry/instrumentation-pg': {
        enabled: true,
        // Inclui o texto das queries nos spans — cuidado com dados sensíveis
        addSqlCommenterCommentToQueries: true,
      },
      '@opentelemetry/instrumentation-redis-4': {
        enabled: true,
      },
      '@opentelemetry/instrumentation-fs': {
        // Desabilitado — gera volume excessivo de spans
        enabled: false,
      },
    }),
  ],
});

// Inicia o SDK antes de qualquer código da aplicação
sdk.start();

// Garante encerramento gracioso ao desligar
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('OpenTelemetry encerrado'))
    .catch((err) => console.error('Erro ao encerrar OpenTelemetry', err))
    .finally(() => process.exit(0));
});

Carregando a instrumentação antes da aplicação:

// package.json
{
  "scripts": {
    "start": "node --require ./src/instrumentation.js src/server.js",
    "dev": "nodemon --require ./src/instrumentation.js src/server.js"
  }
}

Instrumentação Manual com Spans Customizados

A instrumentação automática captura as camadas de infraestrutura — HTTP, banco de dados, cache. A instrumentação manual adiciona contexto do domínio do negócio — operações específicas, regras de negócio, fluxos complexos:

// src/services/pedido.service.js
const { trace, SpanStatusCode, context } = require('@opentelemetry/api');

const tracer = trace.getTracer('minha-api.pedido-service');

class PedidoService {
  async processarPedido(pedidoId, usuarioId) {
    // Cria um span para toda a operação de processamento
    return tracer.startActiveSpan('pedido.processar', async (span) => {
      try {
        // Adiciona atributos que ficam visíveis no trace
        span.setAttributes({
          'pedido.id': pedidoId,
          'usuario.id': usuarioId,
          'pedido.operacao': 'processamento-completo',
        });

        // Validação do pedido — span filho
        const pedido = await this.validarPedido(pedidoId);
        span.setAttributes({
          'pedido.valor_total': pedido.valorTotal,
          'pedido.qtd_itens': pedido.itens.length,
        });

        // Verificação de estoque — span filho
        await this.verificarEstoque(pedido.itens);

        // Processamento de pagamento — span filho
        const pagamento = await this.processarPagamento(pedido);
        span.setAttributes({
          'pagamento.id': pagamento.id,
          'pagamento.metodo': pagamento.metodo,
        });

        // Atualização do banco de dados — capturada automaticamente pelo pg
        await this.salvarPedidoProcessado(pedido, pagamento);

        // Notificação — span filho
        await this.notificarCliente(usuarioId, pedido);

        span.setStatus({ code: SpanStatusCode.OK });
        return { pedidoId, status: 'processado', pagamentoId: pagamento.id };

      } catch (error) {
        // Registra o erro no span — aparece destacado no Jaeger/Tempo
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: error.message,
        });
        span.recordException(error);
        throw error;

      } finally {
        span.end();
      }
    });
  }

  async validarPedido(pedidoId) {
    return tracer.startActiveSpan('pedido.validar', async (span) => {
      try {
        span.setAttribute('pedido.id', pedidoId);

        const pedido = await this.db.query(
          'SELECT * FROM pedidos WHERE id = $1',
          [pedidoId]
        );

        if (!pedido) {
          throw new Error(`Pedido ${pedidoId} não encontrado`);
        }

        if (pedido.status !== 'pendente') {
          throw new Error(`Pedido ${pedidoId} não está pendente`);
        }

        span.setAttribute('pedido.status_atual', pedido.status);
        return pedido;

      } finally {
        span.end();
      }
    });
  }

  async verificarEstoque(itens) {
    return tracer.startActiveSpan('estoque.verificar', async (span) => {
      try {
        span.setAttribute('estoque.qtd_itens', itens.length);

        const resultados = await Promise.all(
          itens.map(item => this.redis.get(`estoque:${item.produtoId}`))
        );

        const itensInsuficientes = itens.filter((item, idx) => {
          const estoqueAtual = parseInt(resultados[idx] || '0');
          return estoqueAtual < item.quantidade;
        });

        if (itensInsuficientes.length > 0) {
          span.setAttribute(
            'estoque.itens_insuficientes',
            itensInsuficientes.map(i => i.produtoId).join(',')
          );
          throw new Error(`Estoque insuficiente para ${itensInsuficientes.length} item(s)`);
        }

        span.setAttribute('estoque.resultado', 'disponivel');
        return true;

      } finally {
        span.end();
      }
    });
  }
}

module.exports = PedidoService;

Propagação de Contexto entre Serviços

Para que o rastreamento seja realmente distribuído — conectando spans de diferentes serviços em um único trace — o contexto do trace precisa ser propagado nas chamadas entre serviços. O OpenTelemetry faz isso automaticamente via headers HTTP quando a instrumentação automática está ativa, usando o formato W3C TraceContext:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
              ^^ versão  ^^ trace-id (128 bits)        ^^ span-id (64 bits) ^^ flags

Para verificar que a propagação está funcionando, pode-se inspecionar os headers de uma requisição de saída:

// Exemplo de chamada entre serviços — a instrumentação automática
// injeta os headers de trace automaticamente
const axios = require('axios');

async function chamarServicoDeInventario(produtoId) {
  // A instrumentação HTTP injeta automaticamente:
  // traceparent: 00-abc123...-def456...-01
  // tracestate: (vazio ou com dados do vendor)
  const response = await axios.get(
    `http://servico-inventario/api/produtos/${produtoId}`
  );
  return response.data;
}

Para chamadas que não usam HTTP — mensagens em filas, por exemplo — a propagação precisa ser manual:

// Propagando contexto em mensagens para uma fila
const { context, propagation } = require('@opentelemetry/api');

async function publicarEventoPedidoCriado(pedido) {
  return tracer.startActiveSpan('fila.publicar', async (span) => {
    try {
      // Extrai os headers de propagação do contexto atual
      const headers = {};
      propagation.inject(context.active(), headers);

      // Inclui os headers na mensagem da fila
      await filaService.publicar('pedidos.criados', {
        payload: pedido,
        tracingHeaders: headers,  // <-- propagação do trace
      });

    } finally {
      span.end();
    }
  });
}

// No consumidor da fila — extrai o contexto do trace
async function consumirEventoPedidoCriado(mensagem) {
  // Restaura o contexto do trace a partir dos headers da mensagem
  const contextoParent = propagation.extract(
    context.active(),
    mensagem.tracingHeaders
  );

  return context.with(contextoParent, () => {
    return tracer.startActiveSpan('fila.consumir', async (span) => {
      try {
        span.setAttribute('fila.nome', 'pedidos.criados');
        await processarPedidoCriado(mensagem.payload);
      } finally {
        span.end();
      }
    });
  });
}

Configurando o OpenTelemetry Collector

O Collector recebe telemetria das aplicações, processa e exporta para múltiplos backends simultaneamente — permitindo enviar traces para o Grafana Tempo para visualização e para o Datadog para alertas, sem alterar as aplicações:

# observabilidade/otel-collector/otel-collector.yml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  # Recebe métricas no formato Prometheus (pull)
  prometheus:
    config:
      scrape_configs:
        - job_name: 'otel-collector'
          scrape_interval: 10s
          static_configs:
            - targets: ['localhost:8888']

processors:
  # Remove spans de baixo valor para reduzir volume
  filter/drop_health_checks:
    traces:
      span:
        - 'attributes["http.route"] == "/health"'
        - 'attributes["http.route"] == "/metrics"'

  # Adiciona atributos do recurso a todos os spans
  resource:
    attributes:
      - action: insert
        key: collector.version
        value: "0.91.0"

  # Agrupa spans em lotes antes de exportar — melhora a performance
  batch:
    send_batch_size: 1024
    timeout: 5s
    send_batch_max_size: 2048

  # Limita o uso de memória do collector
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128

exporters:
  # Exporta traces para o Grafana Tempo
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

  # Exporta métricas para o Prometheus
  prometheus:
    endpoint: "0.0.0.0:8889"
    namespace: otelcol

  # Log dos traces no stdout — útil para debugging
  logging:
    verbosity: detailed
    sampling_initial: 5
    sampling_thereafter: 200

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, filter/drop_health_checks, resource, batch]
      exporters: [otlp/tempo, logging]

    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, batch]
      exporters: [prometheus]

  telemetry:
    logs:
      level: warn

Adicionando o Grafana Tempo ao Stack

O Grafana Tempo é o backend de traces do ecossistema Grafana — gratuito, open source e integrado nativamente com o Grafana:

# Adicionando ao docker-compose.observabilidade.yml

  tempo:
    image: grafana/tempo:2.4.0
    container_name: tempo
    command: ["-config.file=/etc/tempo.yml"]
    volumes:
      - ./observabilidade/tempo/tempo.yml:/etc/tempo.yml
      - tempo_data:/tmp/tempo
    ports:
      - "3200:3200"   # HTTP
      - "4317:4317"   # OTLP gRPC
    restart: unless-stopped

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.91.0
    container_name: otel-collector
    volumes:
      - ./observabilidade/otel-collector/otel-collector.yml:/etc/otelcol-contrib/config.yaml
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "8889:8889"   # Prometheus metrics
    depends_on:
      - tempo
    restart: unless-stopped

volumes:
  tempo_data:
# observabilidade/tempo/tempo.yml
stream_over_http_enabled: true

server:
  http_listen_port: 3200

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

ingester:
  max_block_duration: 5m

compactor:
  compaction:
    block_retention: 168h  # 7 dias

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo/blocks
    wal:
      path: /tmp/tempo/wal

# Gera métricas a partir dos traces — span metrics
metrics_generator:
  registry:
    external_labels:
      source: tempo
  storage:
    path: /tmp/tempo/generator/wal
    remote_write:
      - url: http://prometheus:9090/api/v1/write
        send_exemplars: true
  processors:
    - service-graphs
    - span-metrics

Correlacionando Logs, Métricas e Traces no Grafana

A observabilidade completa se realiza quando os três pilares estão conectados — é possível ir de um alerta de latência alta para os traces afetados, e de um trace com erro para os logs específicos daquela requisição.

No Grafana, essa correlação é configurada via derived fields no datasource do Loki e exemplars no Prometheus:

# Datasource do Loki com link para traces
- name: Loki
  type: loki
  url: http://loki:3100
  jsonData:
    derivedFields:
      - matcherRegex: '"traceId":"([a-f0-9]{32})"'
        name: TraceID
        url: '$${__value.raw}'
        datasourceUid: tempo
        urlDisplayLabel: 'Ver trace no Tempo'

Para que os logs contenham o trace ID, a aplicação deve injetá-lo automaticamente:

// src/middleware/logging.js — versão com trace correlation
const { trace, context } = require('@opentelemetry/api');
const logger = require('../logger');

function loggingMiddleware(req, res, next) {
  const span = trace.getActiveSpan();
  const spanContext = span?.spanContext();

  // Cria um logger filho com os IDs do trace injetados
  req.log = logger.child({
    traceId: spanContext?.traceId,
    spanId: spanContext?.spanId,
    requestId: req.headers['x-request-id'],
  });

  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    req.log.info({
      msg: 'Requisição concluída',
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration,
    });
  });

  next();
}

Com essa configuração, cada entrada de log contém o traceId da requisição. No Grafana, ao visualizar um log de erro, um clique no link "Ver trace no Tempo" abre o trace completo daquela requisição — com todos os spans, timings e atributos — conectando os três pilares em um único fluxo de investigação.


O Que Vem a Seguir

O próximo artigo fecha o tema abordando alertas inteligentes e cultura de resposta a incidentes — como estruturar runbooks, como conduzir postmortems sem culpa e como usar os dados de observabilidade para melhorar o sistema de forma contínua.


Referências para Aprofundamento

Documentação oficial - OpenTelemetry Documentation — opentelemetry.io — Documentação completa do OpenTelemetry, incluindo guias de instrumentação para todas as linguagens suportadas e referência da especificação. - OpenTelemetry JS — GitHub — Repositório oficial do SDK JavaScript, com exemplos de instrumentação automática e manual para Node.js. - Grafana Tempo Documentation — grafana.com — Documentação do Grafana Tempo, cobrindo configuração, ingestão de traces e integração com Prometheus e Loki.

Conceitos e arquitetura - OpenTelemetry Collector — opentelemetry.io — Documentação do Collector, incluindo configuração de receivers, processors e exporters para múltiplos backends. - W3C TraceContext Specification — w3.org — Especificação oficial do formato de propagação de contexto usado pelo OpenTelemetry entre serviços HTTP.

Observabilidade na prática - Charity Majors — Observability vs Monitoring — Artigo influente de Charity Majors, cofundadora do Honeycomb, sobre a diferença entre monitoramento tradicional e observabilidade moderna.

Comentários

Mais em DevOps

Introdução ao Terraform: Infraestrutura que Você Pode Versionar
Introdução ao Terraform: Infraestrutura que Você Pode Versionar

Imagine a seguinte situação, comum em empresas que não adotaram Infraestrutur...

Geração de Infraestrutura com IA
Geração de Infraestrutura com IA

Gerar Terraform, manifestos Kubernetes e pipelines CI/CD a partir de descriçõ...

AWS na Prática: EC2, VPC e IAM em Profundidade
AWS na Prática: EC2, VPC e IAM em Profundidade

Os artigos anteriores introduziram EC2, VPC e IAM como parte da prática com T...