<h2>O que é Observabilidade e por que OpenTelemetry?</h2>
<p>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.</p>
<p>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.</p>
<h2>Conceitos Fundamentais: Traces, Spans e Contexto</h2>
<h3>O que é um Trace?</h3>
<p>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.</p>
<h3>O que é um Span?</h3>
<p>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.</p>
<h3>Propagação de Contexto</h3>
<p>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 <code>traceparent</code> 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.</p>
<h2>Implementação Prática: Configurando OpenTelemetry em TypeScript</h2>
<h3>Setup Inicial com Node SDK</h3>
<p>Comece instalando as dependências essenciais:</p>
<pre><code class="language-bash">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</code></pre>
<p>Crie um arquivo <code>tracing.ts</code> na raiz do seu projeto:</p>
<pre><code class="language-typescript">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));
});</code></pre>
<p><strong>Importante</strong>: Este arquivo deve ser carregado <strong>antes</strong> de qualquer outro código da sua aplicação. No seu <code>index.ts</code> ou entry point:</p>
<pre><code class="language-typescript">import "./tracing"; // Sempre primeiro!
import express from "express";
const app = express();
app.listen(3000, () => console.log("Server rodando na porta 3000"));</code></pre>
<h3>Criando Spans Customizados</h3>
<p>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:</p>
<pre><code class="language-typescript">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();
}
}</code></pre>
<p>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.</p>
<h3>Spans Aninhados (Parent-Child)</h3>
<p>Quando você quer rastrear sub-operações dentro de um span principal, use a API de contexto:</p>
<pre><code class="language-typescript">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();
}
});
}</code></pre>
<p>Aqui, <code>buscar_dados_usuario</code> é o span pai, e <code>consultar_banco_dados</code> e <code>enriquecer_perfil</code> são spans filhos. A hierarquia é preservada nos traces.</p>
<h2>Tipos de Trace e Padrões Avançados</h2>
<h3>Traces de Requisições HTTP</h3>
<p>Requisições HTTP são automaticamente instrumentadas, mas você pode adicionar contexto customizado:</p>
<pre><code class="language-typescript">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);
});</code></pre>
<h3>Traces de Chamadas a APIs Externas</h3>
<p>Integre rastreamento para chamadas HTTP outbound usando bibliotecas como Axios ou Fetch com instrumentação OTel:</p>
<pre><code class="language-typescript">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();
}
});
}</code></pre>
<h3>Traces de Operações de Banco de Dados</h3>
<p>Embora instrumentação automática capture queries, contexto customizado ajuda na investigação:</p>
<pre><code class="language-typescript">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();
});
});
});
}</code></pre>
<h3>Traces Assíncronos (Filas e Background Jobs)</h3>
<p>Em operações assíncronas (filas, workers), você precisa propagar contexto manualmente:</p>
<pre><code class="language-typescript">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();
}
});
});</code></pre>
<h2>Exportando e Visualizando Traces</h2>
<h3>Configuração com Jaeger (Local Development)</h3>
<p>Para desenvolvimento local, use Jaeger em Docker:</p>
<pre><code class="language-bash">docker run -d \
-p 16686:16686 \
-p 4318:4318 \
jaegertracing/all-in-one</code></pre>
<p>Seu arquivo <code>tracing.ts</code> já envia para <code>http://localhost:4318/v1/traces</code>. Acesse Jaeger em <code>http://localhost:16686</code> para visualizar traces em tempo real.</p>
<h3>Exportação para Produção (Honeycomb, Datadog)</h3>
<p>Altere apenas a configuração do exporter sem mudar seu código de instrumentação:</p>
<pre><code class="language-typescript">// 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</code></pre>
<p>Essa flexibilidade é o poder real do OpenTelemetry: você instrumenta uma vez e escolhe o backend depois.</p>
<h2>Conclusão</h2>
<p>Aprendemos que <strong>observabilidade é mais que logs</strong>: traces conectam requisições através de toda sua arquitetura, revelando gargalos e falhas com precisão. <strong>OpenTelemetry padroniza coleta de dados</strong>, libertando você de vendor lock-in — sua instrumentação funciona com qualquer backend.</p>
<p>Por fim, <strong>spans são blocos de construção</strong>, 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://opentelemetry.io/docs/instrumentation/js/" target="_blank" rel="noopener noreferrer">OpenTelemetry JavaScript Documentation</a></li>
<li><a href="https://www.w3.org/TR/trace-context/" target="_blank" rel="noopener noreferrer">W3C Trace Context Specification</a></li>
<li><a href="https://www.jaegertracing.io/" target="_blank" rel="noopener noreferrer">Jaeger: Open Source End-to-End Distributed Tracing</a></li>
<li><a href="https://www.oreilly.com/library/view/observability-engineering/9781492076438/" target="_blank" rel="noopener noreferrer">Observability Engineering by Yuri Shkuro</a></li>
<li><a href="https://opentelemetry.io/docs/specs/otel/protocol/exporter/" target="_blank" rel="noopener noreferrer">OpenTelemetry Semantic Conventions</a></li>
</ul>
<p><!-- FIM --></p>