DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Observabilidade com OpenTelemetry

logotech

## Desvendando o Caos: Como Rastrear Requisições em Arquiteturas Distribuídas

Em um mundo onde microsserviços reinam e a complexidade aumenta exponencialmente, entender o fluxo de uma requisição através de múltiplos serviços tornou-se um desafio monumental. Imagine uma transação bancária, uma busca em um e-commerce ou o processamento de um pedido – cada uma delas pode acionar uma cascata de chamadas entre dezenas de serviços. Quando algo dá errado, ou mesmo para otimizar a performance, identificar a origem do problema pode parecer como procurar uma agulha em um palheiro. É aqui que entra o rastreamento distribuído.

Por Que Rastrear Requisições?

Arquiteturas distribuídas, apesar de seus benefícios em escalabilidade e resiliência, introduzem uma nova camada de complexidade: a observabilidade. Sem um mecanismo eficaz de rastreamento, depurar problemas em produção se torna um pesadelo. O rastreamento distribuído nos permite:

  • Diagnosticar Falhas: Identificar rapidamente qual serviço falhou e em que ponto da cadeia de requisições.
  • Otimizar Performance: Mapear gargalos e latências em cada etapa do processamento.
  • Entender o Fluxo: Visualizar a jornada completa de uma requisição através de diferentes serviços.
  • Auditoria e Conformidade: Registrar o histórico de operações para fins de segurança e conformidade.

A Magia do Rastreamento Distribuído: Conceitos Fundamentais

A essência do rastreamento distribuído reside em propagar um identificador único – o Trace ID – através de todas as chamadas de rede entre os serviços. Cada operação dentro de um serviço é representada por um Span, que possui um ID único, o ID do Trace ao qual pertence, o ID do Span pai (se aplicável), o nome da operação e metadados (tags e logs).

  1. Trace ID: Um identificador global único para uma requisição completa, desde o ponto de entrada até o final.
  2. Span ID: Um identificador único para uma unidade de trabalho dentro de um serviço (ex: uma chamada a um banco de dados, uma requisição HTTP interna).
  3. Parent Span ID: O ID do Span que originou o Span atual. Essencial para construir a árvore de dependências.
  4. Context Propagation: A chave para conectar os Spans. O Trace ID, Span ID e outros metadados são injetados nos cabeçalhos das requisições (HTTP, gRPC, filas de mensagens) e extraídos pelos serviços consumidores.

Mãos à Obra: Implementando com TypeScript/Node.js

Vamos ilustrar com um exemplo prático usando Node.js e uma biblioteca popular como o OpenTelemetry, que se tornou um padrão de fato para observabilidade.

Imagine dois serviços: servico-a e servico-b. servico-a chama servico-b.

Pré-requisitos:

  • Node.js instalado
  • npm ou yarn

Instalação das dependências:

npm install @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http
# ou
yarn add @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http
Enter fullscreen mode Exit fullscreen mode

Configuração do OpenTelemetry (em ambos os serviços):

// otel.config.ts (em ambos os serviços)
import { NodeSDK } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  // Configuração do exportador para enviar traces para um backend (ex: Jaeger, Tempo)
  // Certifique-se que seu backend está rodando e acessível em 'http://localhost:4318/v1/traces'
  // ou ajuste a URL conforme necessário.
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTLP_ENDPOINT || 'http://localhost:4318/v1/traces', // Endpoint do seu coletor OTLP
  }),
  instrumentations: [
    // Instrumentações automáticas para capturar spans de bibliotecas comuns
    getNodeAutoInstrumentations(),
    // Instrumentações específicas para Express e HTTP, caso não cobertas pelo auto
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ],
});

// Inicializa o SDK do OpenTelemetry
sdk.start();

// Adiciona um handler para garantir que os spans sejam exportados ao encerrar a aplicação
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((error) => console.error('Error terminating tracing', error))
    .finally(() => process.exit(0));
});

export default sdk;
Enter fullscreen mode Exit fullscreen mode

Serviço A (servico-a.ts):

// servico-a.ts
import express, { Request, Response } from 'express';
import axios from 'axios';
import { trace, context, propagation, SpanKind, SpanStatusCode } from '@opentelemetry/api';
import './otel.config'; // Importa a configuração do OpenTelemetry

const app = express();
const port = 3000;
const servicoBUrl = process.env.SERVICO_B_URL || 'http://localhost:3001';

// Pega o tracer do OpenTelemetry
const tracer = trace.getTracer('servico-a-tracer');

app.get('/processar', async (req: Request, res: Response) => {
  // Cria um span para a requisição recebida em servico-a
  const currentSpan = tracer.startSpan('processar-request-servico-a', {
    kind: SpanKind.SERVER, // Indica que este span representa um trabalho iniciado por uma requisição externa
    attributes: { // Adiciona atributos úteis para o span
      'http.method': req.method,
      'http.url': req.url,
      'http.target': req.originalUrl,
      'http.host': req.headers.host,
      'net.peer.ip': req.socket.remoteAddress,
    }
  });

  // Ativa o contexto atual para que os spans filhos sejam associados a este trace
  context.with(trace.setSpan(context.active(), currentSpan), async () => {
    try {
      console.log('Iniciando processamento em Servico A...');

      // Obtém o contexto de propagação atual para injetar nos cabeçalhos da próxima requisição
      const carrier = {};
      propagation.inject(context.active(), carrier);

      // Chama o Servico B, injetando o contexto de rastreamento nos cabeçalhos
      const responseServicoB = await axios.get(`${servicoBUrl}/dados`, {
        headers: carrier // Injeta o contexto de rastreamento aqui
      });

      const dadosDoServicoB = responseServicoB.data;
      console.log('Dados recebidos do Servico B:', dadosDoServicoB);

      // Processa os dados...
      const resultado = `Servico A processou com sucesso. Dados do B: ${dadosDoServicoB.message}`;

      // Define o status do span como sucesso
      currentSpan.setStatus({ code: SpanStatusCode.OK });
      currentSpan.addEvent('Servico B chamado com sucesso.'); // Adiciona um evento ao span

      res.json({ message: resultado });

    } catch (error) {
      console.error('Erro ao processar em Servico A:', error);
      // Define o status do span como erro
      currentSpan.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message });
      currentSpan.recordException(error as Error); // Registra a exceção no span
      res.status(500).json({ message: 'Erro interno no Servico A' });
    } finally {
      // Finaliza o span atual. IMPORTANTE: Sempre finalizar o span!
      currentSpan.end();
    }
  });
});

app.listen(port, () => {
  console.log(`Servico A escutando na porta ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Serviço B (servico-b.ts):

// servico-b.ts
import express, { Request, Response } from 'express';
import { trace, context, propagation, SpanKind, SpanStatusCode } from '@opentelemetry/api';
import './otel.config'; // Importa a configuração do OpenTelemetry

const app = express();
const port = 3001;

// Pega o tracer do OpenTelemetry
const tracer = trace.getTracer('servico-b-tracer');

app.get('/dados', (req: Request, res: Response) => {
  // Extrai o contexto de rastreamento dos cabeçalhos da requisição recebida
  const extractedContext = propagation.extract(context.active(), req.headers);

  // Cria um span para a requisição recebida em servico-b, associando-o ao trace existente
  // Se o contexto foi extraído, ele será usado como contexto pai. Caso contrário, um novo trace será iniciado.
  const currentSpan = tracer.startSpan('processar-request-servico-b', {
    kind: SpanKind.SERVER,
    attributes: {
      'http.method': req.method,
      'http.url': req.url,
      'http.target': req.originalUrl,
      'http.host': req.headers.host,
      'net.peer.ip': req.socket.remoteAddress,
    },
    // Usa o contexto extraído para linkar este span ao trace original
    parent: extractedContext && trace.getSpanContext(extractedContext) ? extractedContext : undefined,
  });

  // Ativa o contexto atual
  context.with(trace.setSpan(context.active(), currentSpan), () => {
    try {
      console.log('Iniciando processamento em Servico B...');

      // Simula algum processamento
      const dadosProcessados = { message: 'Dados do Servico B mockados!' };

      // Define o status do span como sucesso
      currentSpan.setStatus({ code: SpanStatusCode.OK });
      currentSpan.addEvent('Processamento em Servico B concluído.');

      res.json(dadosProcessados);

    } catch (error) {
      console.error('Erro ao processar em Servico B:', error);
      // Define o status do span como erro
      currentSpan.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message });
      currentSpan.recordException(error as Error);
      res.status(500).json({ message: 'Erro interno no Servico B' });
    } finally {
      // Finaliza o span atual
      currentSpan.end();
    }
  });
});

app.listen(port, () => {
  console.log(`Servico B escutando na porta ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Observações importantes:

  • Propagação de Contexto: A mágica acontece em propagation.inject e propagation.extract. O axios em servico-a envia os cabeçalhos de rastreamento, e servico-b os lê para continuar o mesmo trace.
  • Span Kind: SpanKind.SERVER indica que o span iniciou devido a uma requisição externa. SpanKind.CLIENT seria usado para chamadas de saída (como a chamada axios em servico-a, se não estivéssemos usando a instrumentação automática).
  • Gerenciamento do Ciclo de Vida do Span: É crucial iniciar (startSpan) e finalizar (end) cada span. Usamos try...catch...finally para garantir que currentSpan.end() seja sempre chamado.
  • Status e Eventos: Definir o SpanStatusCode e adicionar events (como recordException) fornece informações valiosas sobre o que aconteceu durante a execução do span.
  • Backend de Agregação: Os traces configurados acima enviarão os dados para um coletor OTLP (como o OpenTelemetry Collector). Você precisará de um backend de visualização (como Jaeger, Zipkin ou Grafana Tempo) para inspecionar os traces.

Conclusão

O rastreamento distribuído não é mais um luxo, mas uma necessidade em arquiteturas modernas. Ao implementar práticas como a propagação de contexto e a instrumentação adequada, ganhamos visibilidade essencial para depurar, otimizar e entender o comportamento complexo de nossos sistemas distribuídos. Ferramentas como o OpenTelemetry fornecem uma base sólida para construir essa observabilidade, capacitando equipes a navegar com confiança no labirinto de microsserviços. Lembre-se: o que não pode ser medido, não pode ser melhorado. E no mundo distribuído, o rastreamento é a nossa principal ferramenta de medição.

Top comments (0)