Considere o seguinte cenário: o dashboard de monitoramento mostra que a latência P95 da API aumentou de 120ms para 850ms às 15h43. Os logs de erro não mostram nada anormal — as requisições estão completando com sucesso, apenas lentamente. A taxa de erros continua em zero. O time sabe que há um problema, mas não sabe onde.
A API chama um serviço de autenticação, que por sua vez consulta o banco de dados de usuários. Ela também chama um serviço de inventário, que consulta outro banco de dados e um cache Redis. Uma única requisição ao endpoint /checkout pode envolver seis serviços diferentes e doze chamadas de rede.
Com métricas e logs, é possível saber que o problema existe. Com rastreamento distribuído, é possível saber exatamente onde ele está — qual serviço, qual operação, qual linha de código contribuiu com mais tempo para aquela requisição de 850ms.
O rastreamento distribuído captura o caminho completo de uma requisição através de todos os serviços que ela toca, atribuindo a cada segmento um intervalo de tempo chamado span. O conjunto de spans de uma requisição forma um trace — uma árvore que visualiza a causalidade e a temporalidade de toda a operação.
OpenTelemetry: O Padrão da Indústria
Durante anos, cada fornecedor de observabilidade — Datadog, New Relic, Jaeger, Zipkin — tinha seu próprio SDK proprietário. Instrumentar uma aplicação significava depender de um fornecedor específico. Migrar para outro exigia reinstrumentar todo o código.
O OpenTelemetry resolve esse problema definindo um padrão aberto e agnóstico de fornecedor para coleta de telemetria — métricas, logs e traces. Uma aplicação instrumentada com OpenTelemetry pode exportar dados para Jaeger, Zipkin, Datadog, Grafana Tempo ou qualquer outro backend sem alterar o código da aplicação.
A arquitetura do OpenTelemetry tem três componentes principais:
SDK — a biblioteca que roda dentro da aplicação e coleta telemetria. Disponível para Node.js, Python, Java, Go, .NET e outras linguagens.
API — a interface de programação usada para criar spans e registrar atributos. Separada do SDK para que bibliotecas possam ser instrumentadas sem depender de uma implementação específica.
Collector — um processo independente que recebe telemetria de múltiplas aplicações, processa e exporta para um ou mais backends. Atua como um proxy de telemetria.
Instrumentando uma Aplicação Node.js
A instrumentação automática do OpenTelemetry para Node.js captura traces de HTTP, banco de dados, Redis e dezenas de outras bibliotecas sem modificar o código da aplicação. A instrumentação manual complementa com spans específicos do domínio do negócio.
Instalação das dependências:
npm install \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions
Arquivo de instrumentação — deve ser carregado antes de qualquer outro módulo:
// src/instrumentation.js
'use strict';
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
// Define os atributos do recurso — identificam a aplicação nos traces
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'minha-api',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION || '0.0.0',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
});
// Exportador de traces — envia para o OpenTelemetry Collector
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|| 'http://otel-collector:4318/v1/traces',
});
// Exportador de métricas
const metricExporter = new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
|| 'http://otel-collector:4318/v1/metrics',
});
const sdk = new NodeSDK({
resource,
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 15000,
}),
instrumentations: [
getNodeAutoInstrumentations({
// Instrumenta automaticamente: HTTP, Express, PostgreSQL,
// Redis, MongoDB, gRPC e dezenas de outras bibliotecas
'@opentelemetry/instrumentation-http': {
enabled: true,
// Ignora o endpoint de métricas para não poluir os traces
ignoreIncomingRequestHook: (req) => {
return req.url === '/metrics' || req.url === '/health';
},
},
'@opentelemetry/instrumentation-express': {
enabled: true,
},
'@opentelemetry/instrumentation-pg': {
enabled: true,
// Inclui o texto das queries nos spans — cuidado com dados sensíveis
addSqlCommenterCommentToQueries: true,
},
'@opentelemetry/instrumentation-redis-4': {
enabled: true,
},
'@opentelemetry/instrumentation-fs': {
// Desabilitado — gera volume excessivo de spans
enabled: false,
},
}),
],
});
// Inicia o SDK antes de qualquer código da aplicação
sdk.start();
// Garante encerramento gracioso ao desligar
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('OpenTelemetry encerrado'))
.catch((err) => console.error('Erro ao encerrar OpenTelemetry', err))
.finally(() => process.exit(0));
});
Carregando a instrumentação antes da aplicação:
// package.json
{
"scripts": {
"start": "node --require ./src/instrumentation.js src/server.js",
"dev": "nodemon --require ./src/instrumentation.js src/server.js"
}
}
Instrumentação Manual com Spans Customizados
A instrumentação automática captura as camadas de infraestrutura — HTTP, banco de dados, cache. A instrumentação manual adiciona contexto do domínio do negócio — operações específicas, regras de negócio, fluxos complexos:
// src/services/pedido.service.js
const { trace, SpanStatusCode, context } = require('@opentelemetry/api');
const tracer = trace.getTracer('minha-api.pedido-service');
class PedidoService {
async processarPedido(pedidoId, usuarioId) {
// Cria um span para toda a operação de processamento
return tracer.startActiveSpan('pedido.processar', async (span) => {
try {
// Adiciona atributos que ficam visíveis no trace
span.setAttributes({
'pedido.id': pedidoId,
'usuario.id': usuarioId,
'pedido.operacao': 'processamento-completo',
});
// Validação do pedido — span filho
const pedido = await this.validarPedido(pedidoId);
span.setAttributes({
'pedido.valor_total': pedido.valorTotal,
'pedido.qtd_itens': pedido.itens.length,
});
// Verificação de estoque — span filho
await this.verificarEstoque(pedido.itens);
// Processamento de pagamento — span filho
const pagamento = await this.processarPagamento(pedido);
span.setAttributes({
'pagamento.id': pagamento.id,
'pagamento.metodo': pagamento.metodo,
});
// Atualização do banco de dados — capturada automaticamente pelo pg
await this.salvarPedidoProcessado(pedido, pagamento);
// Notificação — span filho
await this.notificarCliente(usuarioId, pedido);
span.setStatus({ code: SpanStatusCode.OK });
return { pedidoId, status: 'processado', pagamentoId: pagamento.id };
} catch (error) {
// Registra o erro no span — aparece destacado no Jaeger/Tempo
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
async validarPedido(pedidoId) {
return tracer.startActiveSpan('pedido.validar', async (span) => {
try {
span.setAttribute('pedido.id', pedidoId);
const pedido = await this.db.query(
'SELECT * FROM pedidos WHERE id = $1',
[pedidoId]
);
if (!pedido) {
throw new Error(`Pedido ${pedidoId} não encontrado`);
}
if (pedido.status !== 'pendente') {
throw new Error(`Pedido ${pedidoId} não está pendente`);
}
span.setAttribute('pedido.status_atual', pedido.status);
return pedido;
} finally {
span.end();
}
});
}
async verificarEstoque(itens) {
return tracer.startActiveSpan('estoque.verificar', async (span) => {
try {
span.setAttribute('estoque.qtd_itens', itens.length);
const resultados = await Promise.all(
itens.map(item => this.redis.get(`estoque:${item.produtoId}`))
);
const itensInsuficientes = itens.filter((item, idx) => {
const estoqueAtual = parseInt(resultados[idx] || '0');
return estoqueAtual < item.quantidade;
});
if (itensInsuficientes.length > 0) {
span.setAttribute(
'estoque.itens_insuficientes',
itensInsuficientes.map(i => i.produtoId).join(',')
);
throw new Error(`Estoque insuficiente para ${itensInsuficientes.length} item(s)`);
}
span.setAttribute('estoque.resultado', 'disponivel');
return true;
} finally {
span.end();
}
});
}
}
module.exports = PedidoService;
Propagação de Contexto entre Serviços
Para que o rastreamento seja realmente distribuído — conectando spans de diferentes serviços em um único trace — o contexto do trace precisa ser propagado nas chamadas entre serviços. O OpenTelemetry faz isso automaticamente via headers HTTP quando a instrumentação automática está ativa, usando o formato W3C TraceContext:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^^ versão ^^ trace-id (128 bits) ^^ span-id (64 bits) ^^ flags
Para verificar que a propagação está funcionando, pode-se inspecionar os headers de uma requisição de saída:
// Exemplo de chamada entre serviços — a instrumentação automática
// injeta os headers de trace automaticamente
const axios = require('axios');
async function chamarServicoDeInventario(produtoId) {
// A instrumentação HTTP injeta automaticamente:
// traceparent: 00-abc123...-def456...-01
// tracestate: (vazio ou com dados do vendor)
const response = await axios.get(
`http://servico-inventario/api/produtos/${produtoId}`
);
return response.data;
}
Para chamadas que não usam HTTP — mensagens em filas, por exemplo — a propagação precisa ser manual:
// Propagando contexto em mensagens para uma fila
const { context, propagation } = require('@opentelemetry/api');
async function publicarEventoPedidoCriado(pedido) {
return tracer.startActiveSpan('fila.publicar', async (span) => {
try {
// Extrai os headers de propagação do contexto atual
const headers = {};
propagation.inject(context.active(), headers);
// Inclui os headers na mensagem da fila
await filaService.publicar('pedidos.criados', {
payload: pedido,
tracingHeaders: headers, // <-- propagação do trace
});
} finally {
span.end();
}
});
}
// No consumidor da fila — extrai o contexto do trace
async function consumirEventoPedidoCriado(mensagem) {
// Restaura o contexto do trace a partir dos headers da mensagem
const contextoParent = propagation.extract(
context.active(),
mensagem.tracingHeaders
);
return context.with(contextoParent, () => {
return tracer.startActiveSpan('fila.consumir', async (span) => {
try {
span.setAttribute('fila.nome', 'pedidos.criados');
await processarPedidoCriado(mensagem.payload);
} finally {
span.end();
}
});
});
}
Configurando o OpenTelemetry Collector
O Collector recebe telemetria das aplicações, processa e exporta para múltiplos backends simultaneamente — permitindo enviar traces para o Grafana Tempo para visualização e para o Datadog para alertas, sem alterar as aplicações:
# observabilidade/otel-collector/otel-collector.yml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Recebe métricas no formato Prometheus (pull)
prometheus:
config:
scrape_configs:
- job_name: 'otel-collector'
scrape_interval: 10s
static_configs:
- targets: ['localhost:8888']
processors:
# Remove spans de baixo valor para reduzir volume
filter/drop_health_checks:
traces:
span:
- 'attributes["http.route"] == "/health"'
- 'attributes["http.route"] == "/metrics"'
# Adiciona atributos do recurso a todos os spans
resource:
attributes:
- action: insert
key: collector.version
value: "0.91.0"
# Agrupa spans em lotes antes de exportar — melhora a performance
batch:
send_batch_size: 1024
timeout: 5s
send_batch_max_size: 2048
# Limita o uso de memória do collector
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
exporters:
# Exporta traces para o Grafana Tempo
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
# Exporta métricas para o Prometheus
prometheus:
endpoint: "0.0.0.0:8889"
namespace: otelcol
# Log dos traces no stdout — útil para debugging
logging:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, filter/drop_health_checks, resource, batch]
exporters: [otlp/tempo, logging]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch]
exporters: [prometheus]
telemetry:
logs:
level: warn
Adicionando o Grafana Tempo ao Stack
O Grafana Tempo é o backend de traces do ecossistema Grafana — gratuito, open source e integrado nativamente com o Grafana:
# Adicionando ao docker-compose.observabilidade.yml
tempo:
image: grafana/tempo:2.4.0
container_name: tempo
command: ["-config.file=/etc/tempo.yml"]
volumes:
- ./observabilidade/tempo/tempo.yml:/etc/tempo.yml
- tempo_data:/tmp/tempo
ports:
- "3200:3200" # HTTP
- "4317:4317" # OTLP gRPC
restart: unless-stopped
otel-collector:
image: otel/opentelemetry-collector-contrib:0.91.0
container_name: otel-collector
volumes:
- ./observabilidade/otel-collector/otel-collector.yml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8889:8889" # Prometheus metrics
depends_on:
- tempo
restart: unless-stopped
volumes:
tempo_data:
# observabilidade/tempo/tempo.yml
stream_over_http_enabled: true
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
max_block_duration: 5m
compactor:
compaction:
block_retention: 168h # 7 dias
storage:
trace:
backend: local
local:
path: /tmp/tempo/blocks
wal:
path: /tmp/tempo/wal
# Gera métricas a partir dos traces — span metrics
metrics_generator:
registry:
external_labels:
source: tempo
storage:
path: /tmp/tempo/generator/wal
remote_write:
- url: http://prometheus:9090/api/v1/write
send_exemplars: true
processors:
- service-graphs
- span-metrics
Correlacionando Logs, Métricas e Traces no Grafana
A observabilidade completa se realiza quando os três pilares estão conectados — é possível ir de um alerta de latência alta para os traces afetados, e de um trace com erro para os logs específicos daquela requisição.
No Grafana, essa correlação é configurada via derived fields no datasource do Loki e exemplars no Prometheus:
# Datasource do Loki com link para traces
- name: Loki
type: loki
url: http://loki:3100
jsonData:
derivedFields:
- matcherRegex: '"traceId":"([a-f0-9]{32})"'
name: TraceID
url: '$${__value.raw}'
datasourceUid: tempo
urlDisplayLabel: 'Ver trace no Tempo'
Para que os logs contenham o trace ID, a aplicação deve injetá-lo automaticamente:
// src/middleware/logging.js — versão com trace correlation
const { trace, context } = require('@opentelemetry/api');
const logger = require('../logger');
function loggingMiddleware(req, res, next) {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
// Cria um logger filho com os IDs do trace injetados
req.log = logger.child({
traceId: spanContext?.traceId,
spanId: spanContext?.spanId,
requestId: req.headers['x-request-id'],
});
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
req.log.info({
msg: 'Requisição concluída',
method: req.method,
url: req.url,
status: res.statusCode,
duration,
});
});
next();
}
Com essa configuração, cada entrada de log contém o traceId da requisição. No Grafana, ao visualizar um log de erro, um clique no link "Ver trace no Tempo" abre o trace completo daquela requisição — com todos os spans, timings e atributos — conectando os três pilares em um único fluxo de investigação.
O Que Vem a Seguir
O próximo artigo fecha o tema abordando alertas inteligentes e cultura de resposta a incidentes — como estruturar runbooks, como conduzir postmortems sem culpa e como usar os dados de observabilidade para melhorar o sistema de forma contínua.
Referências para Aprofundamento
Documentação oficial - OpenTelemetry Documentation — opentelemetry.io — Documentação completa do OpenTelemetry, incluindo guias de instrumentação para todas as linguagens suportadas e referência da especificação. - OpenTelemetry JS — GitHub — Repositório oficial do SDK JavaScript, com exemplos de instrumentação automática e manual para Node.js. - Grafana Tempo Documentation — grafana.com — Documentação do Grafana Tempo, cobrindo configuração, ingestão de traces e integração com Prometheus e Loki.
Conceitos e arquitetura - OpenTelemetry Collector — opentelemetry.io — Documentação do Collector, incluindo configuração de receivers, processors e exporters para múltiplos backends. - W3C TraceContext Specification — w3.org — Especificação oficial do formato de propagação de contexto usado pelo OpenTelemetry entre serviços HTTP.
Observabilidade na prática - Charity Majors — Observability vs Monitoring — Artigo influente de Charity Majors, cofundadora do Honeycomb, sobre a diferença entre monitoramento tradicional e observabilidade moderna.