Observabilidade com TypeScript: OpenTelemetry e Tipos de Trace na Prática Já leu

O que é Observabilidade e por que OpenTelemetry? Observabilidade é a capacidade de entender o comportamento interno de um sistema através de seus dados externos — logs, métricas e traces. Diferente de monitoramento tradicional, que responde "o que está errado?", observabilidade responde "por que está errado?" ao dar visibilidade profunda do fluxo de requisições e comportamento da aplicação. OpenTelemetry (OTel) é um padrão aberto e neutro para instrumentar, gerar, coletar e exportar dados de observabilidade. Em vez de ficar preso a uma única ferramenta (New Relic, DataDog, Jaeger), você escreve seu código uma única vez e pode enviar dados para qualquer backend compatível. TypeScript, sendo executado em Node.js, é ideal para implementar observabilidade em aplicações web modernas, permitindo rastrear requisições desde o cliente até o banco de dados. Conceitos Fundamentais: Traces, Spans e Contexto O que é um Trace? Um trace é um registro completo do caminho percorrido por uma requisição através de sua aplicação. Imagine uma requisição HTTP chegando

O que é Observabilidade e por que OpenTelemetry?

Observabilidade é a capacidade de entender o comportamento interno de um sistema através de seus dados externos — logs, métricas e traces. Diferente de monitoramento tradicional, que responde "o que está errado?", observabilidade responde "por que está errado?" ao dar visibilidade profunda do fluxo de requisições e comportamento da aplicação.

OpenTelemetry (OTel) é um padrão aberto e neutro para instrumentar, gerar, coletar e exportar dados de observabilidade. Em vez de ficar preso a uma única ferramenta (New Relic, DataDog, Jaeger), você escreve seu código uma única vez e pode enviar dados para qualquer backend compatível. TypeScript, sendo executado em Node.js, é ideal para implementar observabilidade em aplicações web modernas, permitindo rastrear requisições desde o cliente até o banco de dados.

Conceitos Fundamentais: Traces, Spans e Contexto

O que é um Trace?

Um trace é um registro completo do caminho percorrido por uma requisição através de sua aplicação. Imagine uma requisição HTTP chegando ao seu backend: ela passa pelo middleware de autenticação, consulta um banco de dados, chama uma API externa, processa dados e retorna uma resposta. Um trace captura toda essa jornada em um único identificador (trace ID) que conecta todas as operações.

O que é um Span?

Um span é uma unidade individual dentro de um trace. Cada operação — uma chamada de banco de dados, uma requisição HTTP, um processamento de negócio — é um span. Um span contém metadados críticos: nome da operação, timestamps de início e fim, atributos customizados, eventos e status de sucesso ou falha. Uma requisição típica gera múltiplos spans aninhados, formando uma árvore de execução.

Propagação de Contexto

Quando sua requisição viaja entre serviços (microsserviços, funções serverless, queues), o contexto de trace precisa ser propagado. OpenTelemetry usa headers padronizados (como traceparent do W3C Trace Context) para manter o trace ID consistente através de toda a cadeia de chamadas. Sem propagação correta, você perde a visibilidade do fluxo entre serviços.

Implementação Prática: Configurando OpenTelemetry em TypeScript

Setup Inicial com Node SDK

Comece instalando as dependências essenciais:

npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto \
  @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/resources @opentelemetry/semantic-conventions \
  @opentelemetry/instrumentation-http @opentelemetry/instrumentation-express

Crie um arquivo tracing.ts na raiz do seu projeto:

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 { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { ConsoleSpanExporter, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";

const resource = Resource.default().merge(
  new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: "minha-api",
    [SemanticResourceAttributes.SERVICE_VERSION]: "1.0.0",
  })
);

const sdk = new NodeSDK({
  resource: resource,
  traceExporter: new OTLPTraceExportHTTPSender({
    // Envia para Jaeger, Datadog, Honeycomb, etc
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318/v1/traces",
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

console.log("OpenTelemetry iniciado");

process.on("SIGTERM", () => {
  sdk.shutdown()
    .then(() => console.log("Tracing finalizado"))
    .catch((log) => console.log("Erro ao finalizar tracing", log))
    .finally(() => process.exit(0));
});

Importante: Este arquivo deve ser carregado antes de qualquer outro código da sua aplicação. No seu index.ts ou entry point:

import "./tracing"; // Sempre primeiro!
import express from "express";

const app = express();
app.listen(3000, () => console.log("Server rodando na porta 3000"));

Criando Spans Customizados

Instrumentação automática captura requisições HTTP e banco de dados, mas operações de negócio específicas precisam de spans manuais. Use a API OpenTelemetry para isso:

import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("minha-aplicacao", "1.0.0");

async function processarPagamento(usuarioId: string, valor: number) {
  const span = tracer.startSpan("processar_pagamento", {
    attributes: {
      "usuario.id": usuarioId,
      "pagamento.valor": valor,
      "pagamento.moeda": "BRL",
    },
  });

  try {
    // Operação de negócio
    const resultado = await chamarAPICartao(usuarioId, valor);

    span.setAttributes({
      "pagamento.status": "sucesso",
      "pagamento.id": resultado.transactionId,
    });

    span.addEvent("pagamento_confirmado", {
      "confirmacao.timestamp": new Date().toISOString(),
    });

    return resultado;
  } catch (error) {
    span.recordException(error as Error);
    span.setStatus({ code: 2, message: "Erro ao processar pagamento" });
    throw error;
  } finally {
    span.end();
  }
}

Note que o span é criado, atributos são adicionados, eventos são registrados e o span é finalizado. Se uma exceção ocorre, ela é registrada automaticamente.

Spans Aninhados (Parent-Child)

Quando você quer rastrear sub-operações dentro de um span principal, use a API de contexto:

import { context, trace } from "@opentelemetry/api";

const tracer = trace.getTracer("minha-aplicacao");

async function buscarDadosUsuario(usuarioId: string) {
  const mainSpan = tracer.startSpan("buscar_dados_usuario");

  return context.with(trace.setSpan(context.active(), mainSpan), async () => {
    try {
      // Span pai: buscar_dados_usuario

      const childSpan1 = tracer.startSpan("consultar_banco_dados");
      const usuario = await buscarDoDatabase(usuarioId);
      childSpan1.end();

      const childSpan2 = tracer.startSpan("enriquecer_perfil");
      const perfil = await buscarPerfil(usuarioId);
      childSpan2.end();

      return { usuario, perfil };
    } finally {
      mainSpan.end();
    }
  });
}

Aqui, buscar_dados_usuario é o span pai, e consultar_banco_dados e enriquecer_perfil são spans filhos. A hierarquia é preservada nos traces.

Tipos de Trace e Padrões Avançados

Traces de Requisições HTTP

Requisições HTTP são automaticamente instrumentadas, mas você pode adicionar contexto customizado:

import express, { Request, Response, NextFunction } from "express";
import { trace, context } from "@opentelemetry/api";

const app = express();
const tracer = trace.getTracer("minha-api");

app.use((req: Request, res: Response, next: NextFunction) => {
  const span = tracer.startSpan("http_request", {
    attributes: {
      "http.method": req.method,
      "http.url": req.url,
      "http.target": req.path,
      "http.client_ip": req.ip,
      "http.user_agent": req.get("user-agent"),
    },
  });

  res.on("finish", () => {
    span.setAttributes({
      "http.status_code": res.statusCode,
    });
    span.end();
  });

  context.with(trace.setSpan(context.active(), span), () => {
    next();
  });
});

app.get("/usuarios/:id", async (req: Request, res: Response) => {
  const span = trace.getActiveSpan()!;
  span.setAttributes({
    "usuario.id": req.params.id,
  });

  const usuario = await buscarUsuario(req.params.id);
  res.json(usuario);
});

Traces de Chamadas a APIs Externas

Integre rastreamento para chamadas HTTP outbound usando bibliotecas como Axios ou Fetch com instrumentação OTel:

import axios from "axios";
import { trace, context } from "@opentelemetry/api";

const tracer = trace.getTracer("api-client");

async function chamarServicoExterno(url: string, dados: any) {
  const span = tracer.startSpan("chamada_api_externa", {
    attributes: {
      "http.url": url,
      "http.method": "POST",
    },
  });

  return context.with(trace.setSpan(context.active(), span), async () => {
    try {
      const resposta = await axios.post(url, dados, {
        timeout: 5000,
      });

      span.setAttributes({
        "http.response_content_length": JSON.stringify(resposta.data).length,
        "http.status_code": resposta.status,
      });

      return resposta.data;
    } catch (error: any) {
      span.recordException(error);
      span.setStatus({
        code: 2,
        message: `Erro na API externa: ${error.message}`,
      });
      throw error;
    } finally {
      span.end();
    }
  });
}

Traces de Operações de Banco de Dados

Embora instrumentação automática capture queries, contexto customizado ajuda na investigação:

import { Database } from "sqlite3";
import { trace, context } from "@opentelemetry/api";

const tracer = trace.getTracer("database-client");

async function executarQuery(db: Database, sql: string, params: any[]) {
  const span = tracer.startSpan("db_query", {
    attributes: {
      "db.system": "sqlite",
      "db.statement": sql,
      "db.params_count": params.length,
    },
  });

  return context.with(trace.setSpan(context.active(), span), async () => {
    return new Promise((resolve, reject) => {
      const startTime = Date.now();

      db.all(sql, params, (err: any, rows: any[]) => {
        const duration = Date.now() - startTime;

        if (err) {
          span.recordException(err);
          span.setStatus({ code: 2, message: err.message });
          reject(err);
        } else {
          span.setAttributes({
            "db.rows_affected": rows?.length || 0,
            "db.duration_ms": duration,
          });
          resolve(rows);
        }

        span.end();
      });
    });
  });
}

Traces Assíncronos (Filas e Background Jobs)

Em operações assíncronas (filas, workers), você precisa propagar contexto manualmente:

import Bull from "bull";
import { trace, context, Context } from "@opentelemetry/api";

const tracer = trace.getTracer("background-jobs");
const filaPagamentos = new Bull("pagamentos");

// Enfileirar job com contexto
export async function enfileiraProcessamentoPagamento(usuarioId: string) {
  const span = tracer.startSpan("enfileirar_pagamento", {
    attributes: {
      "usuario.id": usuarioId,
    },
  });

  // Serializa contexto para enviar com a mensagem
  const contextoserialized = trace.getSpanContext(span);

  await filaPagamentos.add(
    { usuarioId },
    {
      jobId: `pagamento-${usuarioId}-${Date.now()}`,
      data: {
        traceContext: contextoserialized,
      },
    }
  );

  span.end();
}

// Processar job com contexto restaurado
filaPagamentos.process(async (job) => {
  // Restaura contexto do job
  const spanContexto = job.data.traceContext;

  const span = tracer.startSpan("processar_pagamento_background", {
    attributes: {
      "usuario.id": job.data.usuarioId,
      "job.id": job.id,
    },
  });

  return context.with(trace.setSpan(context.active(), span), async () => {
    try {
      await processarPagamento(job.data.usuarioId);
      span.addEvent("pagamento_processado_com_sucesso");
    } catch (error) {
      span.recordException(error as Error);
      throw error;
    } finally {
      span.end();
    }
  });
});

Exportando e Visualizando Traces

Configuração com Jaeger (Local Development)

Para desenvolvimento local, use Jaeger em Docker:

docker run -d \
  -p 16686:16686 \
  -p 4318:4318 \
  jaegertracing/all-in-one

Seu arquivo tracing.ts já envia para http://localhost:4318/v1/traces. Acesse Jaeger em http://localhost:16686 para visualizar traces em tempo real.

Exportação para Produção (Honeycomb, Datadog)

Altere apenas a configuração do exporter sem mudar seu código de instrumentação:

// Para Honeycomb
const sdk = new NodeSDK({
  resource: resource,
  traceExporter: new OTLPTraceExporter({
    url: "https://api.honeycomb.io/v1/traces",
    headers: {
      "x-honeycomb-team": process.env.HONEYCOMB_API_KEY,
    },
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

// Para Datadog (exemplo alternativo)
// Basta mudar a URL e headers do exporter

Essa flexibilidade é o poder real do OpenTelemetry: você instrumenta uma vez e escolhe o backend depois.

Conclusão

Aprendemos que observabilidade é mais que logs: traces conectam requisições através de toda sua arquitetura, revelando gargalos e falhas com precisão. OpenTelemetry padroniza coleta de dados, libertando você de vendor lock-in — sua instrumentação funciona com qualquer backend.

Por fim, spans são blocos de construção, e entender spans pai-filho, contexto propagado e tipos de trace (HTTP, database, async) permite rastrear qualquer fluxo de negócio. Comece instrumentando requisições HTTP, adicione spans customizados de negócio e evolua com padrões avançados conforme sua aplicação cresce.

Referências


Artigos relacionados