Python

Guia Completo de Observabilidade em Python: OpenTelemetry, Sentry e Profiling com py-spy

16 min de leitura

Guia Completo de Observabilidade em Python: OpenTelemetry, Sentry e Profiling com py-spy

O Que é Observabilidade e Por Que Você Precisa Disso Observabilidade é a capacidade de entender o estado interno de um sistema a partir de seus sinais externos. Diferentemente de monitoramento tradicional, que responde a perguntas predefinidas ("o servidor está up?"), observabilidade permite fazer perguntas arbitrárias sobre o comportamento da sua aplicação ("por que essa requisição levou 3 segundos?"). Em aplicações Python modernas, especialmente em arquiteturas de microsserviços, você precisa de três pilares: logs estruturados, métricas e traces distribuídos. Saber apenas que uma requisição falhou não é suficiente — você precisa rastrear por onde passou, quanto tempo levou em cada etapa e quais recursos consumiu. Este artigo cobre três ferramentas que formam a base de observabilidade profissional: OpenTelemetry (padrão da indústria), Sentry (tratamento de erros), e py-spy (profiling de performance). OpenTelemetry: O Padrão de Observabilidade O Que é OpenTelemetry OpenTelemetry (OTel) é um projeto CNCF que padroniza como você coleta telemetria — traces, métricas e logs — sem ficar preso

<h2>O Que é Observabilidade e Por Que Você Precisa Disso</h2>

<p>Observabilidade é a capacidade de entender o estado interno de um sistema a partir de seus sinais externos. Diferentemente de monitoramento tradicional, que responde a perguntas predefinidas (&quot;o servidor está up?&quot;), observabilidade permite fazer perguntas arbitrárias sobre o comportamento da sua aplicação (&quot;por que essa requisição levou 3 segundos?&quot;).</p>

<p>Em aplicações Python modernas, especialmente em arquiteturas de microsserviços, você precisa de três pilares: <strong>logs estruturados</strong>, <strong>métricas</strong> e <strong>traces distribuídos</strong>. Saber apenas que uma requisição falhou não é suficiente — você precisa rastrear por onde passou, quanto tempo levou em cada etapa e quais recursos consumiu. Este artigo cobre três ferramentas que formam a base de observabilidade profissional: OpenTelemetry (padrão da indústria), Sentry (tratamento de erros), e py-spy (profiling de performance).</p>

<h2>OpenTelemetry: O Padrão de Observabilidade</h2>

<h3>O Que é OpenTelemetry</h3>

<p>OpenTelemetry (OTel) é um projeto CNCF que padroniza como você coleta telemetria — traces, métricas e logs — sem ficar preso a um fornecedor específico. Você instrui seu código uma vez e pode enviar dados para Jaeger, Datadog, New Relic, ou qualquer backend compatível.</p>

<p>Existem três conceitos fundamentais: um <strong>span</strong> representa uma unidade de trabalho (como uma requisição HTTP), um <strong>trace</strong> é uma árvore de spans conectados mostrando o fluxo completo, e <strong>instrumentação</strong> é o processo de adicionar código que coleta esses dados.</p>

<h3>Instalação e Configuração Básica</h3>

<pre><code class="language-bash">pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-jaeger opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests</code></pre>

<p>Aqui está uma aplicação Flask completa com OpenTelemetry:</p>

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

from opentelemetry import trace, metrics

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import BatchSpanProcessor

from opentelemetry.exporter.jaeger.thrift import JaegerExporter

from opentelemetry.instrumentation.flask import FlaskInstrumentor

from opentelemetry.instrumentation.requests import RequestsInstrumentor

import requests

Configurar exporter para Jaeger (local, porta 6831)

jaeger_exporter = JaegerExporter(

agent_host_name=&quot;localhost&quot;,

agent_port=6831,

)

Configurar provider e adicionar exporter

trace_provider = TracerProvider()

trace_provider.add_span_processor(BatchSpanProcessor(jaeger_exporter))

trace.set_tracer_provider(trace_provider)

Criar aplicação Flask

app = Flask(__name__)

Instrumentar automaticamente

FlaskInstrumentor().instrument_app(app)

RequestsInstrumentor().instrument()

Obter tracer

tracer = trace.get_tracer(__name__)

@app.route(&quot;/api/process&quot;)

def process():

Criar span manualmente para lógica customizada

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

span.set_attribute(&quot;user.id&quot;, 123)

Simular chamada externa

response = requests.get(&quot;https://api.example.com/data&quot;)

Span filhos são criados automaticamente pela instrumentação

result = {

&quot;status&quot;: &quot;success&quot;,

&quot;status_code&quot;: response.status_code

}

span.set_attribute(&quot;result.size&quot;, len(str(result)))

return jsonify(result)

if __name__ == &quot;__main__&quot;:

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

<p>Quando você executa essa aplicação e faz requisições, o OpenTelemetry coleta automaticamente spans para Flask e requests. Para visualizar, abra <code>http://localhost:16686</code> (UI do Jaeger) — você verá toda a árvore de execução com timings.</p>

<h3>Atributos e Eventos em Spans</h3>

<p>Spans são mais poderosos quando você adiciona contexto. Use atributos para metadados persistentes e eventos para marcos de tempo específicos:</p>

<pre><code class="language-python">@app.route(&quot;/api/checkout&quot;, methods=[&quot;POST&quot;])

def checkout():

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

Atributos: metadados da requisição

span.set_attribute(&quot;checkout.user_id&quot;, 456)

span.set_attribute(&quot;checkout.item_count&quot;, 5)

span.set_attribute(&quot;checkout.total_amount&quot;, 199.99)

Simular processamento

try:

Evento: algo importante aconteceu

span.add_event(&quot;inventory_check_started&quot;)

... verificar inventário

span.add_event(&quot;inventory_check_completed&quot;, attributes={&quot;items_available&quot;: True})

span.add_event(&quot;payment_processing_started&quot;)

... processar pagamento

span.add_event(&quot;payment_processing_completed&quot;, attributes={&quot;payment_id&quot;: &quot;pay_xyz123&quot;})

except Exception as e:

span.record_exception(e)

span.set_status(trace.Status(trace.StatusCode.ERROR))

raise

return jsonify({&quot;order_id&quot;: &quot;ORD-789&quot;})</code></pre>

<p>Isso permite visualizar no Jaeger não apenas quanto tempo cada etapa levou, mas também o contexto específico (qual usuário, quantos itens, qual ID de pagamento).</p>

<h2>Sentry: Capturando e Rastreando Erros</h2>

<h3>O Problema que Sentry Resolve</h3>

<p>Logs tradicionais são volumosos e fáceis de perder. Sentry diferencia-se ao: agrupar erros idênticos automaticamente, capturar contexto de usuário e ambiente, e fornecer alertas inteligentes. Você não monitora logs — você detecta problemas reais antes dos usuários reclamarem.</p>

<h3>Integrando Sentry em uma Aplicação Python</h3>

<pre><code class="language-bash">pip install sentry-sdk</code></pre>

<p>Configuração básica (qualquer aplicação):</p>

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

from sentry_sdk.integrations.flask import FlaskIntegration

from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration

sentry_sdk.init(

dsn=&quot;https://seu_key@sentry.io/seu_project&quot;, # Obtenha em sentry.io

integrations=[

FlaskIntegration(),

SqlalchemyIntegration(),

],

traces_sample_rate=0.1, # Envie 10% dos traces

environment=&quot;production&quot;,

)

from flask import Flask, jsonify

import logging

app = Flask(__name__)

Opcional: capturar logs também

logging.basicConfig(level=logging.WARNING)

@app.route(&quot;/api/divide&quot;)

def divide():

a = int(request.args.get(&quot;a&quot;, 10))

b = int(request.args.get(&quot;b&quot;, 0))

try:

result = a / b # Vai falhar se b=0

except ZeroDivisionError as e:

Capturar contexto customizado

sentry_sdk.capture_exception(e)

return jsonify({&quot;error&quot;: &quot;Division by zero&quot;}), 400

return jsonify({&quot;result&quot;: result})

@app.route(&quot;/api/risky&quot;, methods=[&quot;POST&quot;])

def risky_operation():

Sentry captura automaticamente exceções não tratadas

data = request.json

user_id = data[&quot;user_id&quot;] # Pode gerar KeyError

Adicionar contexto de usuário

sentry_sdk.set_user({

&quot;id&quot;: user_id,

&quot;email&quot;: data.get(&quot;email&quot;),

})

Adicionar tags (facilita filtragem no dashboard)

sentry_sdk.set_tag(&quot;operation&quot;, &quot;risky_operation&quot;)

sentry_sdk.set_tag(&quot;request_type&quot;, data.get(&quot;type&quot;))

Adicionar informações estruturadas

sentry_sdk.set_context(&quot;operation_details&quot;, {

&quot;items_count&quot;: len(data.get(&quot;items&quot;, [])),

&quot;priority&quot;: data.get(&quot;priority&quot;, &quot;normal&quot;),

})

Seu código aqui

return jsonify({&quot;status&quot;: &quot;ok&quot;})

if __name__ == &quot;__main__&quot;:

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

<p>Quando um erro ocorre, Sentry captura automaticamente: stack trace completo, variáveis locais de cada frame, IP do usuário, navegador (se web), variáveis de ambiente (sem valores sensíveis). No dashboard Sentry, você vê tendências — &quot;esse erro apareceu 50 vezes hoje&quot; — e pode configurar alertas.</p>

<h3>Diferenciando Errors de Warnings</h3>

<p>Nem tudo que vai errado é crítico. Use captura manual para informar Sentry sem disparar exceções:</p>

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

@app.route(&quot;/api/check-quota&quot;)

def check_quota():

user_id = request.args.get(&quot;user_id&quot;)

quota = get_user_quota(user_id)

if quota &gt; 0.9: # 90% consumido

Informar sem falhar

sentry_sdk.capture_message(

f&quot;User {user_id} quota at {quota*100:.0f}%&quot;,

level=&quot;warning&quot;

)

if quota &gt;= 1.0:

Algo realmente errado

sentry_sdk.capture_message(

f&quot;User {user_id} exceeded quota&quot;,

level=&quot;error&quot;

)

return jsonify({&quot;error&quot;: &quot;Quota exceeded&quot;}), 429

return jsonify({&quot;quota_remaining&quot;: 1.0 - quota})</code></pre>

<h2>Profiling com py-spy: Encontrando Gargalos</h2>

<h3>Por Que Profiling Importa</h3>

<p>OpenTelemetry te diz <em>quando</em> algo é lento. Sentry te diz <em>que</em> errou. Mas e se uma função leva 5 segundos e ninguém sabe por quê? Profiling analisa <em>onde</em> o CPU gasta tempo. py-spy é um profiler que não requer instrumentação — você o roda e ele tira uma foto do que sua aplicação está fazendo.</p>

<h3>Instalação e Uso Básico</h3>

<pre><code class="language-bash">pip install py-spy</code></pre>

<p>Rodar um profiling de 30 segundos em uma aplicação já em execução:</p>

<pre><code class="language-bash"># Se sua app Python roda com PID 12345

py-spy record -o profile.svg -d 30 12345

Ou perfil de toda a sessão (até Ctrl+C)

py-spy record -o profile.svg python seu_script.py</code></pre>

<p>Isso gera um arquivo SVG interativo. Áreas maiores = mais tempo de CPU. Clique para ver detalhes.</p>

<h3>Exemplo Prático: Aplicação com Gargalo Intencional</h3>

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

from flask import Flask, jsonify

app = Flask(__name__)

def inefficient_algorithm(n):

&quot;&quot;&quot;Algoritmo O(n²) — gargalo intencional&quot;&quot;&quot;

total = 0

for i in range(n):

for j in range(n):

total += i * j

return total

def optimized_algorithm(n):

&quot;&quot;&quot;Versão O(n) — muito mais rápida&quot;&quot;&quot;

return (n (n - 1) // 2) * 2

@app.route(&quot;/api/calculate/&lt;int:size&gt;&quot;)

def calculate(size):

Use py-spy para medir qual versão é mais rápida

start = time.time()

result = inefficient_algorithm(size)

elapsed = time.time() - start

return jsonify({

&quot;result&quot;: result,

&quot;method&quot;: &quot;inefficient&quot;,

&quot;elapsed_seconds&quot;: elapsed

})

if __name__ == &quot;__main__&quot;:

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

<p>Execute assim:</p>

<pre><code class="language-bash"># Terminal 1: rodar a app

python app.py

Terminal 2: gerar profile enquanto faz requisições

py-spy record -o profile.svg $(pgrep -f &quot;python app.py&quot;)

Terminal 3: fazer requisições (em outro terminal)

for i in {1..10}; do curl http://localhost:5000/api/calculate/500; done</code></pre>

<p>Abrindo <code>profile.svg</code> no navegador, você vê que <code>inefficient_algorithm</code> consume 80%+ do CPU. Mudança simples: substituir por <code>optimized_algorithm</code>.</p>

<h3>Análise Estatística com py-spy dump</h3>

<p>Para snapshot instantâneo sem gerar SVG:</p>

<pre><code class="language-bash">py-spy dump --pid 12345</code></pre>

<p>Mostra stack trace de cada thread naquele momento. Útil para descobrir se a app está travada em alguma operação.</p>

<h3>Integrando Profiling em Ambiente de Produção</h3>

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

from functools import wraps

import time

def profile_if_slow(threshold_ms=1000):

&quot;&quot;&quot;Decorator que alerta se função demorar demais&quot;&quot;&quot;

def decorator(func):

@wraps(func)

def wrapper(args, *kwargs):

start = time.perf_counter()

result = func(args, *kwargs)

elapsed_ms = (time.perf_counter() - start) * 1000

if elapsed_ms &gt; threshold_ms:

import sentry_sdk

sentry_sdk.capture_message(

f&quot;{func.__name__} took {elapsed_ms:.0f}ms (threshold: {threshold_ms}ms)&quot;,

level=&quot;warning&quot;

)

return result

return wrapper

return decorator

@profile_if_slow(threshold_ms=500)

def fetch_user_data(user_id):

Se isso demorar mais de 500ms, Sentry avisa

time.sleep(0.6)

return {&quot;id&quot;: user_id, &quot;name&quot;: &quot;User&quot;}</code></pre>

<h2>Integrando Tudo Junto: Um Exemplo Completo</h2>

<p>Para solidificar, aqui está uma aplicação que combina os três:</p>

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

from sentry_sdk.integrations.flask import FlaskIntegration

from flask import Flask, request, jsonify

from opentelemetry import trace

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import BatchSpanProcessor

from opentelemetry.exporter.jaeger.thrift import JaegerExporter

from opentelemetry.instrumentation.flask import FlaskInstrumentor

import time

Sentry

sentry_sdk.init(

dsn=&quot;https://key@sentry.io/project&quot;,

integrations=[FlaskIntegration()],

traces_sample_rate=0.1,

environment=&quot;production&quot;,

)

OpenTelemetry

jaeger_exporter = JaegerExporter(agent_host_name=&quot;localhost&quot;, agent_port=6831)

trace_provider = TracerProvider()

trace_provider.add_span_processor(BatchSpanProcessor(jaeger_exporter))

trace.set_tracer_provider(trace_provider)

Flask

app = Flask(__name__)

FlaskInstrumentor().instrument_app(app)

tracer = trace.get_tracer(__name__)

@app.route(&quot;/api/process-order&quot;, methods=[&quot;POST&quot;])

def process_order():

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

data = request.json

order_id = data.get(&quot;order_id&quot;)

span.set_attribute(&quot;order.id&quot;, order_id)

sentry_sdk.set_context(&quot;order&quot;, {&quot;id&quot;: order_id})

try:

span.add_event(&quot;validating_order&quot;)

validate_order(data) # Pode gerar erro

span.add_event(&quot;processing_payment&quot;)

Simular operação lenta

time.sleep(0.1)

span.set_attribute(&quot;order.status&quot;, &quot;completed&quot;)

return jsonify({&quot;order_id&quot;: order_id, &quot;status&quot;: &quot;success&quot;})

except ValueError as e:

span.record_exception(e)

sentry_sdk.capture_exception(e)

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

def validate_order(data):

if not data.get(&quot;items&quot;):

raise ValueError(&quot;Order must have items&quot;)

if data.get(&quot;total&quot;) &lt; 0:

raise ValueError(&quot;Total must be positive&quot;)

if __name__ == &quot;__main__&quot;:

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

<p>Com isso você tem:</p>

<ul>

<li><strong>Jaeger</strong> mostra a árvore completa de execução (traces e spans)</li>

<li><strong>Sentry</strong> alertar sobre exceções em tempo real</li>

<li><strong>py-spy</strong> consegue profiles CPU quando necessário</li>

<li>Tudo correlacionado: um erro em Sentry pode levar a um trace no Jaeger</li>

</ul>

<h2>Conclusão</h2>

<p>Observabilidade em produção não é luxo — é necessidade. Os três pilares aprendidos resolvem problemas diferentes: <strong>OpenTelemetry oferece rastreamento de requisição ponta-a-ponta</strong>, mostrando exatamente onde o tempo é gasto em arquiteturas complexas. <strong>Sentry captura erros antes de se tornarem crises</strong>, diferenciando problemas críticos de warnings e agrupando automaticamente. <strong>py-spy identifica gargalos de performance</strong> sem overhead em produção, usando amostragem.</p>

<p>A chave é não escolher apenas um — use-os juntos. Quando um cliente reporta &quot;a requisição demorou&quot;, você vai direto ao Jaeger, vê qual step foi lento, roda py-spy naquele endpoint, encontra a função problemática, corrige, e Sentry avisa se regredir. É observabilidade acionável.</p>

<h2>Referências</h2>

<ul>

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

<li><a href="https://docs.sentry.io/platforms/python/" target="_blank" rel="noopener noreferrer">Sentry Python SDK Guide</a></li>

<li><a href="https://github.com/benfred/py-spy" target="_blank" rel="noopener noreferrer">py-spy GitHub Repository</a></li>

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

<li><a href="https://landscape.cncf.io/guide" target="_blank" rel="noopener noreferrer">CNCF Observability Landscape</a></li>

</ul>

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

Comentários

Mais em Python

O que Todo Dev Deve Saber sobre Laços em Python: for, while, comprehensions e o Protocolo de Iteração
O que Todo Dev Deve Saber sobre Laços em Python: for, while, comprehensions e o Protocolo de Iteração

Entendendo Laços: A Base da Iteração em Python Laços são estruturas fundament...

Como Usar Metaclasses em Python: type, __new__ e Controle de Criação de Classes em Produção
Como Usar Metaclasses em Python: type, __new__ e Controle de Criação de Classes em Produção

O que são Metaclasses? Uma metaclasse é uma classe cujas instâncias são class...

pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado
pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado

O Problema do Caos de Dependências Quando começamos a trabalhar com Python, e...