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.