<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: "Quantas requisições por segundo minha API está recebendo?", "Qual é o uso de memória?", "Quantos erros ocorreram na última hora?". 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(
'requisicoes_total',
'Total de requisições recebidas',
['metodo', 'endpoint', 'status']
)
duracao_requisicoes = Histogram(
'duracao_requisicoes_segundos',
'Duração das requisições em segundos',
['endpoint'],
buckets=(0.1, 0.5, 1.0, 2.0, 5.0)
)
usuarios_ativos = Gauge(
'usuarios_ativos',
'Número de usuários ativos no sistema'
)
erros_processamento = Counter(
'erros_processamento_total',
'Total de erros durante processamento',
['tipo_erro']
)
@app.route('/api/usuarios/<int:usuario_id>', methods=['GET'])
def obter_usuario(usuario_id):
inicio = time.time()
try:
Simulando processamento
if usuario_id < 0:
raise ValueError("ID inválido")
if random.random() < 0.1: # 10% de chance de erro
raise Exception("Erro ao buscar do banco")
usuarios_ativos.set(random.randint(50, 200))
resultado = {'id': usuario_id, 'nome': f'Usuário {usuario_id}'}
status = 200
except ValueError:
erros_processamento.labels(tipo_erro='validacao').inc()
resultado = {'erro': 'ID inválido'}
status = 400
except Exception as e:
erros_processamento.labels(tipo_erro='banco_dados').inc()
resultado = {'erro': str(e)}
status = 500
finally:
duracao = time.time() - inicio
duracao_requisicoes.labels(endpoint='/usuarios/<id>').observe(duracao)
requisicoes_total.labels(
metodo='GET',
endpoint='/usuarios/<id>',
status=status
).inc()
return jsonify(resultado), status
@app.route('/metricas', methods=['GET'])
def metricas():
return generate_latest(), 200, {'Content-Type': 'text/plain; charset=utf-8'}
if __name__ == '__main__':
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: "O que exatamente aconteceu aqui?", "Qual foi a mensagem de erro?", "Qual era o estado das variáveis quando isso falhou?".</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('app.log')
file_handler.setLevel(logging.DEBUG)
Formatador JSON
formatter = jsonlogger.JsonFormatter(
'%(timestamp)s %(level)s %(name)s %(message)s %(funcName)s %(lineno)d'
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
class ProcessadorPedidos:
def processar_pedido(self, pedido_id, cliente_id, valor):
"""Processa um pedido de compra com logging estruturado."""
contexto = {
'pedido_id': pedido_id,
'cliente_id': cliente_id,
'valor': valor,
'timestamp': datetime.utcnow().isoformat()
}
try:
logger.info(
'Processamento de pedido iniciado',
extra=contexto
)
Validação
if valor <= 0:
logger.warning(
'Valor de pedido inválido',
extra={**contexto, 'motivo': 'valor_negativo_ou_zero'}
)
raise ValueError("Valor deve ser positivo")
if not self._validar_cliente(cliente_id):
logger.error(
'Cliente não encontrado',
extra={**contexto, 'erro_tipo': 'cliente_invalido'}
)
raise ValueError("Cliente não existe")
Processamento
logger.info(
'Validações passaram',
extra={**contexto, 'etapa': 'validacao_concluida'}
)
resultado = self._executar_transacao(pedido_id, valor)
logger.info(
'Pedido processado com sucesso',
extra={**contexto, 'resultado_transacao': resultado}
)
return resultado
except Exception as e:
logger.error(
'Erro ao processar pedido',
extra={
**contexto,
'erro_mensagem': str(e),
'erro_tipo': type(e).__name__
},
exc_info=True
)
raise
def _validar_cliente(self, cliente_id):
"""Simula validação de cliente."""
return cliente_id > 0
def _executar_transacao(self, pedido_id, valor):
"""Simula execução de transação."""
return {'transacao_id': f'TRX-{pedido_id}', 'valor_processado': 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 "porquê" 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 "com que frequência?", logs respondem "o que aconteceu?" e traces respondem "qual foi o caminho completo e quanto tempo levou em cada etapa?". 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='localhost',
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('/pedido', methods=['POST'])
def criar_pedido():
with tracer.start_as_current_span('criar_pedido_gateway') as span:
dados = request.json
span.set_attribute('cliente_id', dados.get('cliente_id'))
span.set_attribute('valor', dados.get('valor'))
Chamando serviço de validação
response = requests.post(
'http://localhost:5002/validar',
json=dados
)
if response.status_code != 200:
return jsonify({'erro': 'Validação falhou'}), 400
Chamando serviço de processamento
response = requests.post(
'http://localhost:5003/processar',
json={**dados, 'validado': True}
)
return jsonify(response.json()), response.status_code
if __name__ == '__main__':
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='localhost',
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('/validar', methods=['POST'])
def validar():
with tracer.start_as_current_span('validar_pedido') as span:
dados = request.json
span.set_attribute('cliente_id', dados.get('cliente_id'))
Simulando validação
time.sleep(0.1)
if dados.get('valor', 0) <= 0:
span.set_attribute('validacao', 'falhou')
return jsonify({'valido': False, 'motivo': 'valor_inválido'}), 400
span.set_attribute('validacao', 'passou')
return jsonify({'valido': True}), 200
if __name__ == '__main__':
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='localhost',
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('/processar', methods=['POST'])
def processar():
with tracer.start_as_current_span('processar_pagamento') as span:
dados = request.json
span.set_attribute('cliente_id', dados.get('cliente_id'))
span.set_attribute('valor', dados.get('valor'))
Simulando processamento de pagamento
time.sleep(0.2)
span.set_attribute('status_pagamento', 'aprovado')
return jsonify({
'sucesso': True,
'pedido_id': 'PED-12345',
'valor_processado': dados.get('valor')
}), 200
if __name__ == '__main__':
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 "Content-Type: application/json" \
-d '{"cliente_id": 1, "valor": 99.99}'</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 "voando" 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 ("Taxa de erro subiu 5x"), logs lhe mostram o quê aconteceu ("Exceção: conexão timeout"), 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 "com que frequência?" e "qual é o padrão?". <strong>Logs são registros detalhados e contextuais</strong> que você consulta durante investigações — são indispensáveis para debugging e auditoria, respondendo "o que exatamente aconteceu aqui?". <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 "sabe que algo está errado" para alguém que "entende exatamente o quê, quando, onde e por quê".</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><!-- FIM --></p>