TypeScript

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

13 min de leitura

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

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

<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 &quot;o que está errado?&quot;, observabilidade responde &quot;por que está errado?&quot; 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 &quot;@opentelemetry/sdk-node&quot;;

import { getNodeAutoInstrumentations } from &quot;@opentelemetry/auto-instrumentations-node&quot;;

import { OTLPTraceExporter } from &quot;@opentelemetry/exporter-trace-otlp-http&quot;;

import { Resource } from &quot;@opentelemetry/resources&quot;;

import { SemanticResourceAttributes } from &quot;@opentelemetry/semantic-conventions&quot;;

import { ConsoleSpanExporter, BatchSpanProcessor } from &quot;@opentelemetry/sdk-trace-node&quot;;

const resource = Resource.default().merge(

new Resource({

[SemanticResourceAttributes.SERVICE_NAME]: &quot;minha-api&quot;,

[SemanticResourceAttributes.SERVICE_VERSION]: &quot;1.0.0&quot;,

})

);

const sdk = new NodeSDK({

resource: resource,

traceExporter: new OTLPTraceExportHTTPSender({

// Envia para Jaeger, Datadog, Honeycomb, etc

url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || &quot;http://localhost:4318/v1/traces&quot;,

}),

instrumentations: [getNodeAutoInstrumentations()],

});

sdk.start();

console.log(&quot;OpenTelemetry iniciado&quot;);

process.on(&quot;SIGTERM&quot;, () =&gt; {

sdk.shutdown()

.then(() =&gt; console.log(&quot;Tracing finalizado&quot;))

.catch((log) =&gt; console.log(&quot;Erro ao finalizar tracing&quot;, log))

.finally(() =&gt; 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 &quot;./tracing&quot;; // Sempre primeiro!

import express from &quot;express&quot;;

const app = express();

app.listen(3000, () =&gt; console.log(&quot;Server rodando na porta 3000&quot;));</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 &quot;@opentelemetry/api&quot;;

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

async function processarPagamento(usuarioId: string, valor: number) {

const span = tracer.startSpan(&quot;processar_pagamento&quot;, {

attributes: {

&quot;usuario.id&quot;: usuarioId,

&quot;pagamento.valor&quot;: valor,

&quot;pagamento.moeda&quot;: &quot;BRL&quot;,

},

});

try {

// Operação de negócio

const resultado = await chamarAPICartao(usuarioId, valor);

span.setAttributes({

&quot;pagamento.status&quot;: &quot;sucesso&quot;,

&quot;pagamento.id&quot;: resultado.transactionId,

});

span.addEvent(&quot;pagamento_confirmado&quot;, {

&quot;confirmacao.timestamp&quot;: new Date().toISOString(),

});

return resultado;

} catch (error) {

span.recordException(error as Error);

span.setStatus({ code: 2, message: &quot;Erro ao processar pagamento&quot; });

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 &quot;@opentelemetry/api&quot;;

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

async function buscarDadosUsuario(usuarioId: string) {

const mainSpan = tracer.startSpan(&quot;buscar_dados_usuario&quot;);

return context.with(trace.setSpan(context.active(), mainSpan), async () =&gt; {

try {

// Span pai: buscar_dados_usuario

const childSpan1 = tracer.startSpan(&quot;consultar_banco_dados&quot;);

const usuario = await buscarDoDatabase(usuarioId);

childSpan1.end();

const childSpan2 = tracer.startSpan(&quot;enriquecer_perfil&quot;);

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 &quot;express&quot;;

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

const app = express();

const tracer = trace.getTracer(&quot;minha-api&quot;);

app.use((req: Request, res: Response, next: NextFunction) =&gt; {

const span = tracer.startSpan(&quot;http_request&quot;, {

attributes: {

&quot;http.method&quot;: req.method,

&quot;http.url&quot;: req.url,

&quot;http.target&quot;: req.path,

&quot;http.client_ip&quot;: req.ip,

&quot;http.user_agent&quot;: req.get(&quot;user-agent&quot;),

},

});

res.on(&quot;finish&quot;, () =&gt; {

span.setAttributes({

&quot;http.status_code&quot;: res.statusCode,

});

span.end();

});

context.with(trace.setSpan(context.active(), span), () =&gt; {

next();

});

});

app.get(&quot;/usuarios/:id&quot;, async (req: Request, res: Response) =&gt; {

const span = trace.getActiveSpan()!;

span.setAttributes({

&quot;usuario.id&quot;: 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 &quot;axios&quot;;

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

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

async function chamarServicoExterno(url: string, dados: any) {

const span = tracer.startSpan(&quot;chamada_api_externa&quot;, {

attributes: {

&quot;http.url&quot;: url,

&quot;http.method&quot;: &quot;POST&quot;,

},

});

return context.with(trace.setSpan(context.active(), span), async () =&gt; {

try {

const resposta = await axios.post(url, dados, {

timeout: 5000,

});

span.setAttributes({

&quot;http.response_content_length&quot;: JSON.stringify(resposta.data).length,

&quot;http.status_code&quot;: 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 &quot;sqlite3&quot;;

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

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

async function executarQuery(db: Database, sql: string, params: any[]) {

const span = tracer.startSpan(&quot;db_query&quot;, {

attributes: {

&quot;db.system&quot;: &quot;sqlite&quot;,

&quot;db.statement&quot;: sql,

&quot;db.params_count&quot;: params.length,

},

});

return context.with(trace.setSpan(context.active(), span), async () =&gt; {

return new Promise((resolve, reject) =&gt; {

const startTime = Date.now();

db.all(sql, params, (err: any, rows: any[]) =&gt; {

const duration = Date.now() - startTime;

if (err) {

span.recordException(err);

span.setStatus({ code: 2, message: err.message });

reject(err);

} else {

span.setAttributes({

&quot;db.rows_affected&quot;: rows?.length || 0,

&quot;db.duration_ms&quot;: 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 &quot;bull&quot;;

import { trace, context, Context } from &quot;@opentelemetry/api&quot;;

const tracer = trace.getTracer(&quot;background-jobs&quot;);

const filaPagamentos = new Bull(&quot;pagamentos&quot;);

// Enfileirar job com contexto

export async function enfileiraProcessamentoPagamento(usuarioId: string) {

const span = tracer.startSpan(&quot;enfileirar_pagamento&quot;, {

attributes: {

&quot;usuario.id&quot;: 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) =&gt; {

// Restaura contexto do job

const spanContexto = job.data.traceContext;

const span = tracer.startSpan(&quot;processar_pagamento_background&quot;, {

attributes: {

&quot;usuario.id&quot;: job.data.usuarioId,

&quot;job.id&quot;: job.id,

},

});

return context.with(trace.setSpan(context.active(), span), async () =&gt; {

try {

await processarPagamento(job.data.usuarioId);

span.addEvent(&quot;pagamento_processado_com_sucesso&quot;);

} 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: &quot;https://api.honeycomb.io/v1/traces&quot;,

headers: {

&quot;x-honeycomb-team&quot;: 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Dominando Arrays, Tuplas e Enums em TypeScript na Prática em Projetos Reais
Dominando Arrays, Tuplas e Enums em TypeScript na Prática em Projetos Reais

Arrays em TypeScript: Fundamentos e Aplicações Práticas Arrays são um dos tip...

Como Usar Testes End-to-End com Playwright e TypeScript: Page Objects Tipados em Produção
Como Usar Testes End-to-End com Playwright e TypeScript: Page Objects Tipados em Produção

Entendendo Testes End-to-End e a Importância do Playwright Testes end-to-end...

Dominando Project References em TypeScript: Monorepos e Builds Incrementais em Projetos Reais
Dominando Project References em TypeScript: Monorepos e Builds Incrementais em Projetos Reais

Project References: A Estrutura Fundamental do TypeScript Project References...