DevOps & CI/CD

O que Todo Dev Deve Saber sobre OpenTelemetry: Instrumentação Unificada para Métricas, Logs e Traces

18 min de leitura

O que Todo Dev Deve Saber sobre OpenTelemetry: Instrumentação Unificada para Métricas, Logs e Traces

O que é OpenTelemetry e por que você precisa dele OpenTelemetry é um framework open-source mantido pela Cloud Native Computing Foundation (CNCF) que oferece um padrão único para coletar, processar e exportar dados de observabilidade de suas aplicações. Antes dele, você precisava integrar múltiplas bibliotecas diferentes para rastrear métricas, logs e traces — cada uma com sua própria API, convenção e configuração. OpenTelemetry unifica tudo isso em um único SDK agnóstico a fornecedores. A essência do OpenTelemetry é ser vendor-agnostic. Você instrumenta sua aplicação uma única vez e escolhe depois para onde enviar os dados: pode ser Prometheus para métricas, ELK para logs, Jaeger para traces, ou qualquer outro backend. Isso reduz acoplamento e oferece flexibilidade real. Além disso, instrumentar desde cedo em um projeto é muito mais barato do que tentar adicionar observabilidade depois, quando a aplicação já está em produção. Os três pilares: Traces, Métricas e Logs O que é um Trace? Um trace é o registro completo

<h2>O que é OpenTelemetry e por que você precisa dele</h2>

<p>OpenTelemetry é um framework open-source mantido pela Cloud Native Computing Foundation (CNCF) que oferece um padrão único para coletar, processar e exportar dados de observabilidade de suas aplicações. Antes dele, você precisava integrar múltiplas bibliotecas diferentes para rastrear métricas, logs e traces — cada uma com sua própria API, convenção e configuração. OpenTelemetry unifica tudo isso em um único SDK agnóstico a fornecedores.</p>

<p>A essência do OpenTelemetry é ser <strong>vendor-agnostic</strong>. Você instrumenta sua aplicação uma única vez e escolhe depois para onde enviar os dados: pode ser Prometheus para métricas, ELK para logs, Jaeger para traces, ou qualquer outro backend. Isso reduz acoplamento e oferece flexibilidade real. Além disso, instrumentar desde cedo em um projeto é muito mais barato do que tentar adicionar observabilidade depois, quando a aplicação já está em produção.</p>

<h2>Os três pilares: Traces, Métricas e Logs</h2>

<h3>O que é um Trace?</h3>

<p>Um trace é o registro completo de uma requisição viajando através de múltiplos serviços em uma arquitetura distribuída. Ele é composto por <strong>spans</strong>, que são unidades de trabalho nomeadas dentro dessa jornada. Quando você faz uma chamada HTTP para um serviço A, que depois chama um banco de dados, e depois chama um serviço B, cada uma dessas operações é um span dentro de um único trace. O valor real de traces aparece em sistemas onde você tem dezenas ou centenas de serviços — sem eles, é impossível entender onde uma requisição falhou ou ficou lenta.</p>

<pre><code class="language-python"># Exemplo básico de trace com Python

from opentelemetry import trace

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import BatchSpanProcessor

from opentelemetry.exporter.jaeger.thrift import JaegerExporter

Configurar exportador Jaeger

jaeger_exporter = JaegerExporter(

agent_host_name=&quot;localhost&quot;,

agent_port=6831,

)

Configurar provider

trace.set_tracer_provider(TracerProvider())

trace.get_tracer_provider().add_span_processor(

BatchSpanProcessor(jaeger_exporter)

)

Obter tracer

tracer = trace.get_tracer(__name__)

Criar um span

with tracer.start_as_current_span(&quot;processar_pagamento&quot;) as span:

span.set_attribute(&quot;usuario_id&quot;, 123)

span.set_attribute(&quot;valor&quot;, 99.99)

seu código aqui

print(&quot;Processando pagamento...&quot;)</code></pre>

<h3>O que são Métricas?</h3>

<p>Métricas são medições numéricas agregadas colhidas em intervalos regulares. Diferente de traces que olham requisição por requisição, métricas respondem perguntas como &quot;qual é a taxa de erro agora?&quot; ou &quot;qual é a latência p99 nos últimos 5 minutos?&quot;. Existem vários tipos: counters (incrementam), gauges (valores instantâneos), histograms (distribuições de valores) e rate histograms. Métricas são leves e escaláveis — você pode ter bilhões de pontos de métrica em produção sem impacto significativo de performance.</p>

<pre><code class="language-python"># Exemplo de métricas com OpenTelemetry

from opentelemetry import metrics

from opentelemetry.sdk.metrics import MeterProvider

from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader

from opentelemetry.exporter.prometheus import PrometheusMetricReader

Configurar Prometheus como exportador

prometheus_reader = PrometheusMetricReader()

provider = MeterProvider(metric_readers=[prometheus_reader])

metrics.set_meter_provider(provider)

Obter meter

meter = metrics.get_meter(__name__)

Criar um counter (incrementa)

request_counter = meter.create_counter(

name=&quot;requisicoes_total&quot;,

description=&quot;Total de requisições processadas&quot;,

unit=&quot;1&quot;,

)

Criar um histogram (distribui valores)

latencia_histogram = meter.create_histogram(

name=&quot;latencia_ms&quot;,

description=&quot;Latência das requisições em milissegundos&quot;,

unit=&quot;ms&quot;,

)

Usar as métricas

request_counter.add(1, {&quot;metodo&quot;: &quot;GET&quot;, &quot;rota&quot;: &quot;/api/usuarios&quot;})

latencia_histogram.record(45, {&quot;rota&quot;: &quot;/api/usuarios&quot;})</code></pre>

<h3>O que são Logs?</h3>

<p>Logs são registros estruturados de eventos discretos que aconteceram na aplicação. Com OpenTelemetry, você pode estruturar seus logs em formato padronizado e correlacioná-los automaticamente com traces — cada log carrega o trace_id e span_id, permitindo navegar entre logs e traces sem esforço. Logs estruturados são tão importantes quanto o conteúdo em si; enviar <code>{&quot;nivel&quot;: &quot;ERROR&quot;, &quot;mensagem&quot;: &quot;conexão falhou&quot;, &quot;servico&quot;: &quot;database&quot;, &quot;erro&quot;: &quot;timeout&quot;}</code> é muito melhor que uma string texto puro.</p>

<pre><code class="language-python"># Exemplo de logs estruturados com OpenTelemetry

from opentelemetry import logs

from opentelemetry.sdk.logs import LoggerProvider

from opentelemetry.sdk.logs.export import BatchLogRecordProcessor

from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter

import logging

Configurar OTLP como exportador

otlp_exporter = OTLPLogExporter(insecure=True)

logger_provider = LoggerProvider()

logger_provider.add_log_record_processor(BatchLogRecordProcessor(otlp_exporter))

logs.set_logger_provider(logger_provider)

Integrar com logging padrão do Python

logging.basicConfig(level=logging.INFO)

logger = logging.getLogger(__name__)

Logs estruturados

logger.info(

&quot;Usuario criado com sucesso&quot;,

extra={

&quot;usuario_id&quot;: 456,

&quot;email&quot;: &quot;joao@example.com&quot;,

&quot;origem_ip&quot;: &quot;192.168.1.1&quot;

}

)</code></pre>

<h2>Instrumentação: Automática vs Manual</h2>

<h3>Instrumentação Automática</h3>

<p>OpenTelemetry oferece instrumentações automáticas (chamadas de instrumentation libraries) para frameworks populares como Django, FastAPI, Flask, requests HTTP, banco de dados (SQLAlchemy, psycopg2), Redis e muito mais. A ideia é simples: você instala o pacote da instrumentação e ela se conecta aos hooks internos do framework, capturando traces e métricas sem você escrever uma linha de código. Isso é poderoso porque reduz o boilerplate e garante boas práticas desde o início.</p>

<pre><code class="language-python"># Instalação de instrumentações automáticas

pip install opentelemetry-instrumentation-flask

pip install opentelemetry-instrumentation-sqlalchemy

pip install opentelemetry-instrumentation-requests

from flask import Flask

from opentelemetry.instrumentation.flask import FlaskInstrumentor

from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import BatchSpanProcessor

from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

app = Flask(__name__)

Configurar exportador OTLP (OpenTelemetry Protocol)

otlp_exporter = OTLPSpanExporter(insecure=True)

trace_provider = TracerProvider()

trace_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))

Instrumentar automaticamente

FlaskInstrumentor().instrument_app(app)

SQLAlchemyInstrumentor().instrument()

@app.route(&#039;/api/usuarios/&lt;int:user_id&gt;&#039;)

def get_user(user_id):

Traces de HTTP e DB queries são capturados automaticamente

return {&quot;id&quot;: user_id, &quot;nome&quot;: &quot;João Silva&quot;}

if __name__ == &#039;__main__&#039;:

app.run()</code></pre>

<h3>Instrumentação Manual</h3>

<p>Para lógica customizada específica do seu negócio, você precisa de instrumentação manual. Por exemplo, se você tem uma função que calcula recomendações de produtos ou processa um arquivo pesado, você quer criar spans para isso. A instrumentação manual é simples: peça um tracer ao provider e crie spans nomeados semanticamente. Não é verboso e oferece controle fino.</p>

<pre><code class="language-python"># Instrumentação manual para lógica de negócio

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

def processar_pedido(pedido_id: int, itens: list) -&gt; dict:

&quot;&quot;&quot;Função customizada que precisa de observabilidade&quot;&quot;&quot;

with tracer.start_as_current_span(&quot;processar_pedido&quot;) as span:

span.set_attribute(&quot;pedido_id&quot;, pedido_id)

span.set_attribute(&quot;quantidade_itens&quot;, len(itens))

Validar itens

with tracer.start_as_current_span(&quot;validar_itens&quot;) as sub_span:

for idx, item in enumerate(itens):

if item.get(&quot;quantidade&quot;, 0) &lt;= 0:

sub_span.set_attribute(f&quot;item_{idx}_invalido&quot;, True)

raise ValueError(f&quot;Item {idx} com quantidade inválida&quot;)

Calcular totais

with tracer.start_as_current_span(&quot;calcular_totais&quot;) as sub_span:

total = sum(item[&quot;preco&quot;] * item[&quot;quantidade&quot;] for item in itens)

sub_span.set_attribute(&quot;total_pedido&quot;, total)

Registrar no banco

with tracer.start_as_current_span(&quot;persistir_pedido&quot;) as sub_span:

Simulando gravação

sub_span.set_attribute(&quot;banco_resultado&quot;, &quot;sucesso&quot;)

return {

&quot;pedido_id&quot;: pedido_id,

&quot;status&quot;: &quot;confirmado&quot;,

&quot;total&quot;: total

}

Usar a função

resultado = processar_pedido(1001, [

{&quot;nome&quot;: &quot;Produto A&quot;, &quot;preco&quot;: 50.0, &quot;quantidade&quot;: 2},

{&quot;nome&quot;: &quot;Produto B&quot;, &quot;preco&quot;: 30.0, &quot;quantidade&quot;: 1}

])</code></pre>

<h2>Contexto, Baggage e Propagação</h2>

<h3>Propagação de Contexto em Sistemas Distribuídos</h3>

<p>Um dos maiores desafios em observabilidade distribuída é manter o contexto (trace_id, span_id, baggage) enquanto uma requisição viaja entre serviços. OpenTelemetry resolve isso com <strong>propagadores</strong>, que injetam informações de contexto em headers HTTP ou mensagens. Quando o serviço B recebe a requisição do serviço A, ele extrai esse contexto do header e continua o trace no mesmo ID. Sem propagadores, cada serviço veria isoladamente, quebrando toda a cadeia de observabilidade.</p>

<pre><code class="language-python"># Exemplo de propagação entre dois serviços (A chama B)

===== SERVIÇO A =====

from opentelemetry import trace

from opentelemetry.propagate import inject

import requests

tracer = trace.get_tracer(__name__)

def chamar_servico_b():

with tracer.start_as_current_span(&quot;chamar_servico_b&quot;) as span:

span.set_attribute(&quot;alvo&quot;, &quot;servico_b&quot;)

Injetar contexto nos headers

headers = {}

inject(headers)

Fazer requisição com headers propagados

response = requests.get(

&quot;http://localhost:5001/processar&quot;,

headers=headers

)

return response.json()

===== SERVIÇO B =====

from flask import Flask, request

from opentelemetry.propagate import extract

from opentelemetry.instrumentation.flask import FlaskInstrumentor

app = Flask(__name__)

FlaskInstrumentor().instrument_app(app)

@app.route(&#039;/processar&#039;, methods=[&#039;GET&#039;])

def processar():

Extrair contexto da requisição

ctx = extract(request.headers)

tracer = trace.get_tracer(__name__)

O span criado aqui será filho do span do serviço A

with tracer.start_as_current_span(&quot;processamento&quot;) as span:

span.set_attribute(&quot;tempo_processamento_ms&quot;, 150)

return {&quot;resultado&quot;: &quot;processado&quot;}

if __name__ == &#039;__main__&#039;:

app.run(port=5001)</code></pre>

<h3>Baggage para Metadados de Contexto</h3>

<p>Baggage permite passar dados arbitrários (metadados) junto com o contexto através de serviços, sem incluir no payload principal. Um exemplo comum é passar user_id, tenant_id ou customer_tier. Todos os spans e logs de todos os serviços que tocam nessa requisição terão acesso ao baggage automaticamente, permitindo filtros poderosos em sua ferramenta de observabilidade.</p>

<pre><code class="language-python"># Usando Baggage para propagar tenant_id

from opentelemetry.baggage import set_baggage, get_baggage

def handle_requisicao(tenant_id: str, user_id: str):

Definir baggage

set_baggage(&quot;tenant_id&quot;, tenant_id)

set_baggage(&quot;user_id&quot;, user_id)

Agora, qualquer span ou log criado terá acesso a isso

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span(&quot;processar_dados&quot;) as span:

Baggage é automaticamente incluído como atributo de contexto

tenant = get_baggage(&quot;tenant_id&quot;)

user = get_baggage(&quot;user_id&quot;)

print(f&quot;Processando para tenant={tenant}, user={user}&quot;)

Na sua ferramenta de observabilidade (Grafana, DataDog, etc),

você pode fazer queries como:

SELECT * FROM traces WHERE attributes.baggage.tenant_id = &quot;acme-corp&quot;</code></pre>

<h2>Backends e Exportadores: Onde os Dados Vão</h2>

<p>OpenTelemetry coleta dados mas precisa enviá-los a algum lugar. Você pode usar <strong>OTLP</strong> (OpenTelemetry Protocol), que é agnóstico, ou exportadores específicos. OTLP é o padrão moderno: funciona com gRPC ou HTTP, é eficiente e suportado por ferramentas como Jaeger, Tempo (backend de traces do Grafana), Honeycomb, New Relic, DataDog e muitos outros. Para ambiente de desenvolvimento ou prototipagem rápida, use a pilha aberta: Jaeger para traces, Prometheus para métricas e Loki para logs. Para produção, considere soluções como Grafana Cloud, Honeycomb ou New Relic que oferecem suporte profissional.</p>

<pre><code class="language-python"># Exemplo completo integrando tudo com OTLP e Grafana Loki/Prometheus/Tempo

from opentelemetry import trace, metrics, logs

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.metrics import MeterProvider

from opentelemetry.sdk.logs import LoggerProvider

from opentelemetry.sdk.trace.export import BatchSpanProcessor

from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader

from opentelemetry.sdk.logs.export import BatchLogRecordProcessor

from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter

from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter

from opentelemetry.instrumentation.flask import FlaskInstrumentor

from opentelemetry.instrumentation.requests import RequestsInstrumentor

import logging

from flask import Flask

===== CONFIGURAÇÃO CENTRALIZADA =====

def setup_observability():

Exportadores OTLP (apontam para collector OpenTelemetry)

trace_exporter = OTLPSpanExporter(insecure=True)

metric_exporter = OTLPMetricExporter(insecure=True)

log_exporter = OTLPLogExporter(insecure=True)

Providers

trace_provider = TracerProvider()

trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter))

trace.set_tracer_provider(trace_provider)

metric_reader = PeriodicExportingMetricReader(metric_exporter)

metric_provider = MeterProvider(metric_readers=[metric_reader])

metrics.set_meter_provider(metric_provider)

log_provider = LoggerProvider()

log_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))

logs.set_logger_provider(log_provider)

Instrumentações automáticas

FlaskInstrumentor().instrument()

RequestsInstrumentor().instrument()

Logging padrão integrado

logging.basicConfig(

level=logging.INFO,

format=&#039;%(asctime)s - %(name)s - %(levelname)s - %(message)s&#039;

)

===== APLICAÇÃO =====

setup_observability()

app = Flask(__name__)

logger = logging.getLogger(__name__)

meter = metrics.get_meter(__name__)

Métrica customizada

request_duration = meter.create_histogram(&quot;request_duration_ms&quot;)

@app.route(&#039;/api/health&#039;)

def health():

logger.info(&quot;Health check realizado&quot;)

return {&quot;status&quot;: &quot;healthy&quot;}

@app.route(&#039;/api/process/&lt;int:item_id&gt;&#039;)

def process_item(item_id):

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span(&quot;process_item&quot;) as span:

span.set_attribute(&quot;item_id&quot;, item_id)

logger.info(f&quot;Iniciando processamento&quot;, extra={&quot;item_id&quot;: item_id})

try:

Simular processamento

result = {&quot;item_id&quot;: item_id, &quot;status&quot;: &quot;processed&quot;}

request_duration.record(25)

logger.info(&quot;Processamento concluído com sucesso&quot;)

return result

except Exception as e:

logger.error(f&quot;Erro ao processar: {e}&quot;, exc_info=True)

span.set_attribute(&quot;erro&quot;, str(e))

return {&quot;error&quot;: str(e)}, 500

if __name__ == &#039;__main__&#039;:

app.run(debug=False)</code></pre>

<h2>Boas Práticas e Armadilhas Comuns</h2>

<h3>Escolha Nomes Semanticamente Significativos</h3>

<p>Um dos erros mais comuns é nomear spans de forma genérica como <code>&quot;request&quot;</code>, <code>&quot;query&quot;</code> ou <code>&quot;operation&quot;</code>. Quando você está analisando milhões de traces em produção, nomes genéricos são inúteis. Use nomes que descrevam exatamente o que está acontecendo: <code>&quot;fetch_user_from_db&quot;</code>, <code>&quot;validate_payment_card&quot;</code>, <code>&quot;send_notification_email&quot;</code>. O mesmo vale para atributos — use convenções OpenTelemetry quando possível: <code>http.method</code>, <code>db.system</code>, <code>db.operation</code>, <code>exception.type</code>.</p>

<h3>Evite Carregar Dados Sensíveis em Spans e Logs</h3>

<p>Spans e logs viajam para sistemas de observabilidade centralizados. Nunca inclua senhas, tokens de API, números de cartão de crédito ou PII (Personally Identifiable Information) em spans ou logs. Se você precisa correlacionar eventos com usuários, use IDs anônimos ou hashes. Muitas ferramentas oferecem redação automática, mas contar apenas com isso é arriscado.</p>

<h3>Não Crie Spans para Tudo</h3>

<p>Criar um span para cada linha de código é um anti-padrão que gera ruído e sobrecarga. Spans devem representar unidades de trabalho significativas: uma chamada HTTP, uma query de banco, uma chamada a API externa. Operações internas como looping ou condicionais não precisam de spans. Use a regra prática: &quot;Se isso leva menos de 1ms em média, provavelmente não precisa de span&quot;.</p>

<h3>Configure Sampling em Produção</h3>

<p>Com tracing contínuo em aplicações de alto volume, você coleta bilhões de traces por hora. Armazenar todos é inviável. Use <strong>sampling</strong>: capture 100% dos traces em desenvolvimento, mas em produção capture apenas 10% aleatoriamente, ou use tail-based sampling (capture apenas traces lentos ou com erro). OpenTelemetry oferece <code>TraceIdRatioBased</code> e <code>ParentBased</code> samplers prontos.</p>

<pre><code class="language-python"># Configurar sampling em produção

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.sampling import TraceIdRatioBased

Amostrar 10% de todas as traces

sampler = TraceIdRatioBased(0.1)

trace_provider = TracerProvider(sampler=sampler)

Ou usar ParentBased: herdar a decisão do serviço pai

from opentelemetry.sdk.trace.sampling import ParentBased

sampler = ParentBased(TraceIdRatioBased(0.1))

trace_provider = TracerProvider(sampler=sampler)</code></pre>

<h2>Conclusão</h2>

<p>Você aprendeu que <strong>OpenTelemetry é o padrão unificado para observabilidade</strong> — traces, métricas e logs em um único SDK, agnóstico a fornecedores. Isso elimina lock-in e oferece flexibilidade real para escolher backends depois. A instrumentação automática reduz boilerplate enquanto instrumentação manual oferece controle para lógica de negócio; os dois trabalham juntos. Por fim, propagação de contexto entre serviços distribuídos e boas práticas como naming semântico, sampling e proteção de dados sensíveis são o que transforma raw telemetria em observabilidade acionável. Comece hoje instrumentando sua aplicação — observabilidade não é um luxo, é uma necessidade em sistemas modernos.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://opentelemetry.io/docs/" target="_blank" rel="noopener noreferrer">OpenTelemetry Official Documentation</a></li>

<li><a href="https://github.com/open-telemetry/opentelemetry-python" target="_blank" rel="noopener noreferrer">OpenTelemetry Python SDK - GitHub</a></li>

<li><a href="https://www.cncf.io/blog/2022/08/11/opentelemetry-101/" target="_blank" rel="noopener noreferrer">Getting Started with OpenTelemetry - CNCF</a></li>

<li><a href="https://www.jaegertracing.io/docs/" target="_blank" rel="noopener noreferrer">Distributed Tracing with Jaeger and OpenTelemetry</a></li>

<li><a href="https://www.oreilly.com/library/view/observability-engineering/9781492076438/" target="_blank" rel="noopener noreferrer">Observability Engineering - O&#039;Reilly (Charity Majors, Liz Fong-Jones)</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em DevOps & CI/CD

Dominando Chaos Engineering: Princípios, Chaos Monkey e LitmusChaos em Kubernetes em Projetos Reais
Dominando Chaos Engineering: Princípios, Chaos Monkey e LitmusChaos em Kubernetes em Projetos Reais

Entendendo Chaos Engineering: Fundamentos e Filosofia Chaos Engineering é uma...

Boas Práticas de Conventional Commits, Semantic Versioning e Changelogs Automatizados para Times Ágeis
Boas Práticas de Conventional Commits, Semantic Versioning e Changelogs Automatizados para Times Ágeis

Conventional Commits: A Base Para Versionamento Semântico Um Conventional Com...

Como Usar Estratégias de Branching: Git Flow, Trunk-Based e GitHub Flow em Produção
Como Usar Estratégias de Branching: Git Flow, Trunk-Based e GitHub Flow em Produção

Introdução ao Versionamento de Código e Estratégias de Branching Quando você...