DevOps & CI/CD

Dominando Os Três Pilares da Observabilidade: Métricas, Logs e Traces em Projetos Reais

17 min de leitura

Dominando Os Três Pilares da Observabilidade: Métricas, Logs e Traces em Projetos Reais

Os Três Pilares da Observabilidade: Métricas, Logs e Traces A observabilidade é a capacidade de entender o estado interno de um sistema baseado apenas nas informações que ele emite para o exterior. Diferente de monitoramento tradicional — que se concentra em alertas predefinidos — observabilidade oferece ferramentas para responder perguntas que você não tinha planejado fazer. Para alcançar esse nível de insight, precisamos de três componentes fundamentais: métricas, logs e traces. Cada um deles fornece uma perspectiva diferente do comportamento da sua aplicação. Neste artigo, vamos explorar profundamente cada pilar, entender como funcionam, quando usá-los e como implementá-los de forma prática. Você aprenderá não apenas a teoria, mas também receberá exemplos de código real que pode aplicar imediatamente em seus projetos. Métricas: A Visão Agregada do Sistema O que são métricas? Métricas são medições numéricas agregadas coletadas ao longo do tempo. Elas respondem perguntas como: "Quantas requisições por segundo minha API está recebendo?", "Qual é o uso de memória?", "Quantos

<h2>Os Três Pilares da Observabilidade: Métricas, Logs e Traces</h2>

<p>A observabilidade é a capacidade de entender o estado interno de um sistema baseado apenas nas informações que ele emite para o exterior. Diferente de monitoramento tradicional — que se concentra em alertas predefinidos — observabilidade oferece ferramentas para responder perguntas que você não tinha planejado fazer. Para alcançar esse nível de insight, precisamos de três componentes fundamentais: métricas, logs e traces. Cada um deles fornece uma perspectiva diferente do comportamento da sua aplicação.</p>

<p>Neste artigo, vamos explorar profundamente cada pilar, entender como funcionam, quando usá-los e como implementá-los de forma prática. Você aprenderá não apenas a teoria, mas também receberá exemplos de código real que pode aplicar imediatamente em seus projetos.</p>

<h2>Métricas: A Visão Agregada do Sistema</h2>

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

<p>Métricas são medições numéricas agregadas coletadas ao longo do tempo. Elas respondem perguntas como: &quot;Quantas requisições por segundo minha API está recebendo?&quot;, &quot;Qual é o uso de memória?&quot;, &quot;Quantos erros ocorreram na última hora?&quot;. Métricas são eficientes porque armazenam pouco dado — você não guarda cada valor individual, mas sim valores agregados (média, percentil 95, máximo, mínimo).</p>

<p>Existem quatro tipos principais de métricas: contadores (counter), medidores (gauge), histogramas (histogram) e resumos (summary). Um contador sempre aumenta, um medidor pode subir e descer, histogramas rastreiam distribuição de valores e resumos fornecem quantis pré-calculados.</p>

<h3>Implementando métricas com Prometheus e Python</h3>

<p>Vamos criar um exemplo real de uma API Flask que expõe métricas para o Prometheus:</p>

<pre><code class="language-python">from flask import Flask, jsonify

from prometheus_client import Counter, Histogram, Gauge, generate_latest

import time

import random

app = Flask(__name__)

Definindo as métricas

requisicoes_total = Counter(

&#039;requisicoes_total&#039;,

&#039;Total de requisições recebidas&#039;,

[&#039;metodo&#039;, &#039;endpoint&#039;, &#039;status&#039;]

)

duracao_requisicoes = Histogram(

&#039;duracao_requisicoes_segundos&#039;,

&#039;Duração das requisições em segundos&#039;,

[&#039;endpoint&#039;],

buckets=(0.1, 0.5, 1.0, 2.0, 5.0)

)

usuarios_ativos = Gauge(

&#039;usuarios_ativos&#039;,

&#039;Número de usuários ativos no sistema&#039;

)

erros_processamento = Counter(

&#039;erros_processamento_total&#039;,

&#039;Total de erros durante processamento&#039;,

[&#039;tipo_erro&#039;]

)

@app.route(&#039;/api/usuarios/&lt;int:usuario_id&gt;&#039;, methods=[&#039;GET&#039;])

def obter_usuario(usuario_id):

inicio = time.time()

try:

Simulando processamento

if usuario_id &lt; 0:

raise ValueError(&quot;ID inválido&quot;)

if random.random() &lt; 0.1: # 10% de chance de erro

raise Exception(&quot;Erro ao buscar do banco&quot;)

usuarios_ativos.set(random.randint(50, 200))

resultado = {&#039;id&#039;: usuario_id, &#039;nome&#039;: f&#039;Usuário {usuario_id}&#039;}

status = 200

except ValueError:

erros_processamento.labels(tipo_erro=&#039;validacao&#039;).inc()

resultado = {&#039;erro&#039;: &#039;ID inválido&#039;}

status = 400

except Exception as e:

erros_processamento.labels(tipo_erro=&#039;banco_dados&#039;).inc()

resultado = {&#039;erro&#039;: str(e)}

status = 500

finally:

duracao = time.time() - inicio

duracao_requisicoes.labels(endpoint=&#039;/usuarios/&lt;id&gt;&#039;).observe(duracao)

requisicoes_total.labels(

metodo=&#039;GET&#039;,

endpoint=&#039;/usuarios/&lt;id&gt;&#039;,

status=status

).inc()

return jsonify(resultado), status

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

def metricas():

return generate_latest(), 200, {&#039;Content-Type&#039;: &#039;text/plain; charset=utf-8&#039;}

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

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

<p>Para usar este código, instale as dependências:</p>

<pre><code class="language-bash">pip install flask prometheus-client</code></pre>

<p>Agora acesse <code>http://localhost:5000/metricas</code> para ver as métricas em formato Prometheus. O Prometheus pode ser configurado para coletar essas métricas periodicamente e armazená-las em seu banco de dados time-series.</p>

<h3>Por que as métricas importam</h3>

<p>Métricas são extremamente eficientes para armazenamento e consulta. Uma métrica pode ocupar apenas alguns bytes, enquanto o evento bruto ocuparia quilobytes. Elas são perfeitas para dashboards, alertas e para responder perguntas sobre tendências históricas. Use métricas quando precisar monitorar KPIs, alertar sobre anomalias e entender padrões de longo prazo.</p>

<h2>Logs: O Registro Detalhado dos Eventos</h2>

<h3>Entendendo logs</h3>

<p>Logs são registros textuais de eventos que ocorreram no sistema. Diferente das métricas, logs são pouco estruturados por padrão e podem conter contexto detalhado. Um log típico contém timestamp, nível de severidade (DEBUG, INFO, WARNING, ERROR), a mensagem e, idealmente, contexto adicional. Logs respondem: &quot;O que exatamente aconteceu aqui?&quot;, &quot;Qual foi a mensagem de erro?&quot;, &quot;Qual era o estado das variáveis quando isso falhou?&quot;.</p>

<p>O desafio com logs é que eles geram muito volume. Uma aplicação web pode produzir milhões de logs por dia. Por isso, a estruturação é crucial — logs estruturados (em JSON, por exemplo) são muito mais fáceis de buscar e analisar do que logs em texto livre.</p>

<h3>Implementando logs estruturados em Python</h3>

<p>Vamos criar um exemplo com estruturação de logs usando a biblioteca <code>python-json-logger</code>:</p>

<pre><code class="language-python">import logging

import json

from pythonjsonlogger import jsonlogger

from datetime import datetime

Configurando um logger com saída JSON estruturada

logger = logging.getLogger(__name__)

logger.setLevel(logging.DEBUG)

Handler para arquivo

file_handler = logging.FileHandler(&#039;app.log&#039;)

file_handler.setLevel(logging.DEBUG)

Formatador JSON

formatter = jsonlogger.JsonFormatter(

&#039;%(timestamp)s %(level)s %(name)s %(message)s %(funcName)s %(lineno)d&#039;

)

file_handler.setFormatter(formatter)

logger.addHandler(file_handler)

class ProcessadorPedidos:

def processar_pedido(self, pedido_id, cliente_id, valor):

&quot;&quot;&quot;Processa um pedido de compra com logging estruturado.&quot;&quot;&quot;

contexto = {

&#039;pedido_id&#039;: pedido_id,

&#039;cliente_id&#039;: cliente_id,

&#039;valor&#039;: valor,

&#039;timestamp&#039;: datetime.utcnow().isoformat()

}

try:

logger.info(

&#039;Processamento de pedido iniciado&#039;,

extra=contexto

)

Validação

if valor &lt;= 0:

logger.warning(

&#039;Valor de pedido inválido&#039;,

extra={**contexto, &#039;motivo&#039;: &#039;valor_negativo_ou_zero&#039;}

)

raise ValueError(&quot;Valor deve ser positivo&quot;)

if not self._validar_cliente(cliente_id):

logger.error(

&#039;Cliente não encontrado&#039;,

extra={**contexto, &#039;erro_tipo&#039;: &#039;cliente_invalido&#039;}

)

raise ValueError(&quot;Cliente não existe&quot;)

Processamento

logger.info(

&#039;Validações passaram&#039;,

extra={**contexto, &#039;etapa&#039;: &#039;validacao_concluida&#039;}

)

resultado = self._executar_transacao(pedido_id, valor)

logger.info(

&#039;Pedido processado com sucesso&#039;,

extra={**contexto, &#039;resultado_transacao&#039;: resultado}

)

return resultado

except Exception as e:

logger.error(

&#039;Erro ao processar pedido&#039;,

extra={

**contexto,

&#039;erro_mensagem&#039;: str(e),

&#039;erro_tipo&#039;: type(e).__name__

},

exc_info=True

)

raise

def _validar_cliente(self, cliente_id):

&quot;&quot;&quot;Simula validação de cliente.&quot;&quot;&quot;

return cliente_id &gt; 0

def _executar_transacao(self, pedido_id, valor):

&quot;&quot;&quot;Simula execução de transação.&quot;&quot;&quot;

return {&#039;transacao_id&#039;: f&#039;TRX-{pedido_id}&#039;, &#039;valor_processado&#039;: valor}

Exemplo de uso

processador = ProcessadorPedidos()

try:

processador.processar_pedido(pedido_id=12345, cliente_id=1, valor=99.99)

except Exception:

pass

try:

processador.processar_pedido(pedido_id=12346, cliente_id=0, valor=-10)

except Exception:

pass</code></pre>

<p>Instale a dependência:</p>

<pre><code class="language-bash">pip install python-json-logger</code></pre>

<p>Execute o código e abra o arquivo <code>app.log</code>. Você verá logs estruturados em JSON, fáceis de buscar em ferramentas como Elasticsearch, Splunk ou CloudWatch.</p>

<h3>Quando usar logs</h3>

<p>Use logs para entender o &quot;porquê&quot; dos eventos. Logs são investigativos por natureza — você os procura quando algo deu errado e precisa entender a sequência exata de eventos. Logs são essenciais para debugging, auditoria e conformidade regulatória.</p>

<h2>Traces: A Visão Distribuída das Transações</h2>

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

<p>Traces rastreiam o caminho completo de uma requisição através de múltiplos serviços em uma arquitetura distribuída. Enquanto métricas respondem &quot;com que frequência?&quot;, logs respondem &quot;o que aconteceu?&quot; e traces respondem &quot;qual foi o caminho completo e quanto tempo levou em cada etapa?&quot;. Um trace é composto por spans — unidades de trabalho em um serviço específico. Cada span contém tempo de início, duração, metadados e referências para spans pai/filho.</p>

<p>Traces são críticos em microsserviços porque uma requisição do cliente pode passar por 5, 10 ou mais serviços. Sem traces, é praticamente impossível entender onde está o gargalo ou por que uma requisição ficou lenta.</p>

<h3>Implementando traces com OpenTelemetry em Python</h3>

<p>Vamos criar um exemplo com dois microsserviços que se comunicam:</p>

<pre><code class="language-python"># requirements.txt

Flask==2.3.0

opentelemetry-api==1.18.0

opentelemetry-sdk==1.18.0

opentelemetry-exporter-jaeger==1.18.0

opentelemetry-instrumentation-flask==0.39b0

opentelemetry-instrumentation-requests==0.39b0

requests==2.31.0</code></pre>

<p>Instale com: <code>pip install -r requirements.txt</code></p>

<p><strong>Serviço 1: API Gateway (porta 5001)</strong></p>

<pre><code class="language-python">from flask import Flask, jsonify, request

from opentelemetry import trace, metrics

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import SimpleSpanProcessor

from opentelemetry.exporter.jaeger.thrift import JaegerExporter

from opentelemetry.instrumentation.flask import FlaskInstrumentor

from opentelemetry.instrumentation.requests import RequestsInstrumentor

import requests

import time

Configurando exportador Jaeger

jaeger_exporter = JaegerExporter(

agent_host_name=&#039;localhost&#039;,

agent_port=6831,

)

trace.set_tracer_provider(TracerProvider())

trace.get_tracer_provider().add_span_processor(

SimpleSpanProcessor(jaeger_exporter)

)

app = Flask(__name__)

tracer = trace.get_tracer(__name__)

Instrumentação automática

FlaskInstrumentor().instrument_app(app)

RequestsInstrumentor().instrument()

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

def criar_pedido():

with tracer.start_as_current_span(&#039;criar_pedido_gateway&#039;) as span:

dados = request.json

span.set_attribute(&#039;cliente_id&#039;, dados.get(&#039;cliente_id&#039;))

span.set_attribute(&#039;valor&#039;, dados.get(&#039;valor&#039;))

Chamando serviço de validação

response = requests.post(

&#039;http://localhost:5002/validar&#039;,

json=dados

)

if response.status_code != 200:

return jsonify({&#039;erro&#039;: &#039;Validação falhou&#039;}), 400

Chamando serviço de processamento

response = requests.post(

&#039;http://localhost:5003/processar&#039;,

json={**dados, &#039;validado&#039;: True}

)

return jsonify(response.json()), response.status_code

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

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

<p><strong>Serviço 2: Validador (porta 5002)</strong></p>

<pre><code class="language-python">from flask import Flask, jsonify, request

from opentelemetry import trace

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import SimpleSpanProcessor

from opentelemetry.exporter.jaeger.thrift import JaegerExporter

from opentelemetry.instrumentation.flask import FlaskInstrumentor

import time

jaeger_exporter = JaegerExporter(

agent_host_name=&#039;localhost&#039;,

agent_port=6831,

)

trace.set_tracer_provider(TracerProvider())

trace.get_tracer_provider().add_span_processor(

SimpleSpanProcessor(jaeger_exporter)

)

app = Flask(__name__)

tracer = trace.get_tracer(__name__)

FlaskInstrumentor().instrument_app(app)

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

def validar():

with tracer.start_as_current_span(&#039;validar_pedido&#039;) as span:

dados = request.json

span.set_attribute(&#039;cliente_id&#039;, dados.get(&#039;cliente_id&#039;))

Simulando validação

time.sleep(0.1)

if dados.get(&#039;valor&#039;, 0) &lt;= 0:

span.set_attribute(&#039;validacao&#039;, &#039;falhou&#039;)

return jsonify({&#039;valido&#039;: False, &#039;motivo&#039;: &#039;valor_inválido&#039;}), 400

span.set_attribute(&#039;validacao&#039;, &#039;passou&#039;)

return jsonify({&#039;valido&#039;: True}), 200

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

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

<p><strong>Serviço 3: Processador (porta 5003)</strong></p>

<pre><code class="language-python">from flask import Flask, jsonify, request

from opentelemetry import trace

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import SimpleSpanProcessor

from opentelemetry.exporter.jaeger.thrift import JaegerExporter

from opentelemetry.instrumentation.flask import FlaskInstrumentor

import time

jaeger_exporter = JaegerExporter(

agent_host_name=&#039;localhost&#039;,

agent_port=6831,

)

trace.set_tracer_provider(TracerProvider())

trace.get_tracer_provider().add_span_processor(

SimpleSpanProcessor(jaeger_exporter)

)

app = Flask(__name__)

tracer = trace.get_tracer(__name__)

FlaskInstrumentor().instrument_app(app)

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

def processar():

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

dados = request.json

span.set_attribute(&#039;cliente_id&#039;, dados.get(&#039;cliente_id&#039;))

span.set_attribute(&#039;valor&#039;, dados.get(&#039;valor&#039;))

Simulando processamento de pagamento

time.sleep(0.2)

span.set_attribute(&#039;status_pagamento&#039;, &#039;aprovado&#039;)

return jsonify({

&#039;sucesso&#039;: True,

&#039;pedido_id&#039;: &#039;PED-12345&#039;,

&#039;valor_processado&#039;: dados.get(&#039;valor&#039;)

}), 200

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

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

<p>Para usar este exemplo, você precisa executar o Jaeger em Docker:</p>

<pre><code class="language-bash">docker run -d --name jaeger \

-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \

-p 5775:5775/udp \

-p 6831:6831/udp \

-p 6832:6832/udp \

-p 5778:5778 \

-p 16686:16686 \

-p 14268:14268 \

jaegertracing/all-in-one:latest</code></pre>

<p>Depois execute os três serviços em terminais diferentes e faça uma requisição:</p>

<pre><code class="language-bash">curl -X POST http://localhost:5001/pedido \

-H &quot;Content-Type: application/json&quot; \

-d &#039;{&quot;cliente_id&quot;: 1, &quot;valor&quot;: 99.99}&#039;</code></pre>

<p>Acesse <code>http://localhost:16686</code> para visualizar os traces no Jaeger. Você verá a requisição passando por todos os três serviços com tempo de execução de cada etapa.</p>

<h3>Por que traces são fundamentais</h3>

<p>Em arquiteturas monolíticas, você poderia debugar tudo localmente. Em microsserviços, isso é impossível — os serviços estão espalhados em máquinas diferentes, redes, containers. Traces permitem ver a requisição &quot;voando&quot; através da arquitetura, identificando exatamente onde está o gargalo e como os serviços se relacionam.</p>

<h2>Integrando os Três Pilares</h2>

<h3>A visão holística</h3>

<p>Até agora estudamos cada pilar isoladamente, mas sua força real vem da integração. Métricas lhe alertam que algo está errado (&quot;Taxa de erro subiu 5x&quot;), logs lhe mostram o quê aconteceu (&quot;Exceção: conexão timeout&quot;), e traces lhe mostram exatamente por qual caminho a requisição passou e onde o tempo foi gasto.</p>

<p>Uma abordagem profissional é correlacionar os três. Quando um alerta dispara, você usa o trace ID para buscar todos os logs relacionados a essa requisição. Você vê no trace que o serviço B demorou 5 segundos, e nos logs encontra a razão exata. Isso é observabilidade verdadeira.</p>

<h3>Ferramentas modernas e ecossistema</h3>

<p>O mercado consolidou um ecossistema interessante. <strong>OpenTelemetry</strong> é o padrão aberto que instrumenta aplicações para coletar métricas, logs e traces de forma consistente. <strong>Prometheus</strong> é de facto o padrão para armazenar métricas em aplicações cloud-native. <strong>ELK Stack</strong> (Elasticsearch, Logstash, Kibana) ou <strong>Loki</strong> (especializado em logs) são comuns para armazenar e buscar logs. <strong>Jaeger</strong> ou <strong>Zipkin</strong> armazenam traces.</p>

<p>O ideal é usar essas ferramentas integradas. Quando você está no Grafana (dashboard de métricas), pode clicar em um ponto no gráfico e ser levado ao Jaeger (para ver os traces) ou ao Loki (para ver os logs) — tudo correlacionado pelo mesmo trace ID.</p>

<h2>Conclusão</h2>

<p>Aprendemos que <strong>métricas são a visão agregada e eficiente</strong> para entender saúde geral, alertas e tendências — use-as para monitorar KPIs e responder &quot;com que frequência?&quot; e &quot;qual é o padrão?&quot;. <strong>Logs são registros detalhados e contextuais</strong> que você consulta durante investigações — são indispensáveis para debugging e auditoria, respondendo &quot;o que exatamente aconteceu aqui?&quot;. <strong>Traces são a representação visual do fluxo distribuído</strong> que permite ver requisições viajando entre serviços — fundamentais em microsserviços para entender performance e relacionamentos.</p>

<p>A verdadeira observabilidade não escolhe apenas um pilar — implementa todos os três e os integra através de IDs de rastreamento comuns. Essa abordagem holística transforma você de alguém que &quot;sabe que algo está errado&quot; para alguém que &quot;entende exatamente o quê, quando, onde e por quê&quot;.</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://prometheus.io/docs/introduction/overview/" target="_blank" rel="noopener noreferrer">Prometheus Documentation</a></li>

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

<li><a href="https://www.oreilly.com/library/view/observability-engineering/9781492076438/" target="_blank" rel="noopener noreferrer">Observability Engineering by Charity Majors, Liz Fong-Jones, George Miranda</a></li>

<li><a href="https://sre.google/sre-book/monitoring-distributed-systems/" target="_blank" rel="noopener noreferrer">Google SRE Book - Chapter on Monitoring</a></li>

</ul>

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

Comentários

Mais em DevOps & CI/CD

Como Usar Shell Scripting Avançado: Processos, Sinais, Trap e Scripts Robustos em Produção
Como Usar Shell Scripting Avançado: Processos, Sinais, Trap e Scripts Robustos em Produção

Entendendo Processos em Shell Um processo em Unix/Linux é uma instância de um...

Boas Práticas de Terraform Fundamentos: Providers, Resources, State e Plan para Times Ágeis
Boas Práticas de Terraform Fundamentos: Providers, Resources, State e Plan para Times Ágeis

Introdução ao Terraform Terraform é uma ferramenta de Infrastructure as Code...

Redes e Volumes Avançados no Docker: Bridge, Overlay e Bind Mounts: Do Básico ao Avançado
Redes e Volumes Avançados no Docker: Bridge, Overlay e Bind Mounts: Do Básico ao Avançado

Introdução: A Importância da Comunicação e Persistência de Dados em Container...