Destino Dev

Blog

Instrumentando FastAPI com OpenTelemetry: traces, métricas e logs correlacionados

Por John Pires · 12 de março de 2026

Instrumentando FastAPI com OpenTelemetry: traces, métricas e logs correlacionados

1) Motivacao e trade-offs

Porque instrumentar

Observability não é um extra: é ferramenta de correção rápida e prevenção. Em aplicações FastAPI, traces ajudam a entender latência entre endpoints, métricas quantificam tráfego e erros, e logs correlacionados permitem seguir a execução com contexto humano. Juntos permitem reduzir MTTR e priorizar correções com dados.

Trade-offs práticos

Instrumentar adiciona dependências, overhead e complexidade operacional. Cada span tem custo; métricas de alta cardinalidade ou registros muito frequentes podem sobrecarregar backends. Escolha amostragem, agregações e exportadores com base em requisitos de latência, custo e volume.

Decisões típicas:

  • Sampling: traces completos em staging, taxa reduzida em produção.
  • Métricas: exporte agregados, evite etiquetas de alta cardinalidade.
  • Logs: correlacione ids, não replique span fields nos logs.

Visibilidade vs Performance

Instrumentação deve ser incremental. Comece por traces de alto nível e métricas SLO, depois adicione spans críticos (DB, chamadas externas). Use processadores assíncronos para não bloquear requests.

Resumo executivo

Instrumente com propósito: trace para latência, métricas para saúde e logs para inspecionar falhas. Configure amostragem e exportadores para equilibrar custo e fidelidade.

2) Fundamentos tecnicos

Componentes OpenTelemetry

OpenTelemetry tem três pilares no stack Python: API, SDK e Exporters. API define interfaces de uso; SDK provê implementações (TracerProvider, MeterProvider); Exporters enviam dados a collector/observability backend (OTLP, Jaeger, Prometheus).

O papel do Resource e providers

Resource descreve atributos da instrumentação: service.name, service.instance.id, deployment.environment. Configure TracerProvider e MeterProvider com esse Resource antes de instrumentar frameworks. Se instanciar providers depois, instrumentação automática pode usar providers default, levando a dados perdidos.

Instrumentacao manual vs automática

Auto-instrumentation é rápida (ex: FastAPIInstrumentor, instrumentadores de httpx/sqlalchemy). Perfil prático: use auto para cobertura rápida e adicione spans manuais onde precisa de contexto e tags customizadas.

Correlacao logs-trace

Python ainda não tem um spec de logs OpenTelemetry totalmente unificado, então a prática comum é enriquecer logs com trace_id/span_id via Formatter ou contextvars. Isso permite buscar logs por trace em backends tradicionais (ELK, Loki).

3) Exemplo 1: caso simples

Objetivo do exemplo

Mostrar como instrumentar um app FastAPI com traces exportados via OTLP HTTP para um collector local. Inclui configuração básica de TracerProvider e FastAPIInstrumentor para traces automáticos de rotas.

Codigo: FastAPI + OTLP exporter (traces)

Este trecho cria TracerProvider com BatchSpanProcessor e instrumenta a app FastAPI. O collector espera em localhost:4318 (OTLP HTTP).

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

# Configura provider e exporter
resource = Resource.create({
  'service.name': 'minha-api',
  'service.version': '1.0.0'
})
provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(endpoint='http://localhost:4318/v1/traces')
provider.add_span_processor(BatchSpanProcessor(notlp_exporter))
trace.set_tracer_provider(provider)

# App FastAPI
app = FastAPI()
FastAPIInstrumentor().instrument_app(app)

@app.get('/items/{item_id}')
async def read_item(item_id: int):
    return {'item_id': item_id}

O que este código faz: inicializa TracerProvider com recurso customizado, adiciona um BatchSpanProcessor que envia spans via OTLP HTTP e instrumenta a app FastAPI para gerar spans automáticos por request.

Resultado: cada chamada a /items/{id} gera um span root com atributos HTTP. O collector em localhost:4318 receberá spans e você poderá visualizar no backend configurado.

Explicacoes e cuidados

Use BatchSpanProcessor para não bloquear. Se estiver em ambiente serverless, prefira exportadores síncronos com flush no shutdown. Garanta que a instrumentação ocorra antes de criar rotas que possam iniciar threads ou conexões.

4) Exemplo 2: caso avancado do mundo real

Contexto real

Aplicação FastAPI que faz queries a um banco via SQLAlchemy, realiza chamadas HTTP a um serviço externo com httpx, e precisa exportar métricas e traces, além de correlacionar logs para debug operacional.

Componentes a cobrir

Vamos combinar:

  • Traces automáticos em FastAPI e httpx.
  • Spans manuais para queries SQL críticas.
  • Métricas: contador de requisições e histograma de latência.
  • Logs correlacionados com trace_id/span_id via Formatter.

Codigo: app completo com spans manuais, metrics e logs correlacionados

O código abaixo demonstra configuração integrada. É funcional com dependências recentes de opentelemetry e assume um collector OTLP em localhost.

import logging
import time
import httpx
from fastapi import FastAPI, Depends
from sqlalchemy import create_engine, text
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.metrics import set_meter_provider, get_meter
from opentelemetry.sdk.metrics import MeterProvider, Counter, Histogram
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.trace import get_tracer

# Logging setup com correlacao
from opentelemetry.trace import get_current_span
class OTelFormatter(logging.Formatter):
    def format(self, record):
        span = get_current_span()
        ctx = span.get_span_context()
        record.trace_id = format(ctx.trace_id, '032x') if ctx.trace_id else ''
        record.span_id = format(ctx.span_id, '016x') if ctx.span_id else ''
        return super().format(record)

handler = logging.StreamHandler()
handler.setFormatter(OTelFormatter('[%(asctime)s] %(levelname)s %(trace_id)s %(span_id)s %(message)s'))
root = logging.getLogger()
root.setLevel(logging.INFO)
root.handlers = [handler]
logger = logging.getLogger('app')

# Trace provider
resource = Resource.create({'service.name': 'api-com-db'})
tracer_provider = TracerProvider(resource=resource)
otlp_trace = OTLPSpanExporter(endpoint='http://localhost:4318/v1/traces')
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_trace))
trace.set_tracer_provider(tracer_provider)
tracer = get_tracer(__name__)

# Metrics provider
metric_exporter = OTLPMetricExporter(endpoint='http://localhost:4318/v1/metrics')
reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=5000)
meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
set_meter_provider(meter_provider)
meter = get_meter(__name__)
request_counter = meter.create_counter('http.server.requests')
latency_hist = meter.create_histogram('http.server.request_latency_ms')

# DB engine
engine = create_engine('sqlite:///./test.db', connect_args={'check_same_thread': False})

app = FastAPI()
FastAPIInstrumentor().instrument_app(app)
HTTPXClientInstrumentor().instrument()

@app.get('/search')
async def search(q: str):
    start = time.time()
    with tracer.start_as_current_span('search.handler') as span:
        span.set_attribute('query', q)
        # Span manual para DB
        with tracer.start_as_current_span('db.query') as db_span:
            db_span.set_attribute('db.system', 'sqlite')
            with engine.connect() as conn:
                result = conn.execute(text('SELECT 1'))
                _ = result.fetchone()
        # External call
        async with httpx.AsyncClient() as client:
            resp = await client.get('https://httpbin.org/get')
            span.set_attribute('httpbin.status', resp.status_code)
        duration_ms = (time.time() - start) * 1000
        request_counter.add(1, {'route': '/search'})
        latency_hist.record(duration_ms, {'route': '/search'})
        logger.info('search executed', extra={'q': q})
        return {'q': q, 'status': resp.status_code}

O que acontece:

  • Traces automáticos para a rota gerados por FastAPIInstrumentor.
  • Spans manuais 'search.handler' e 'db.query' para detalhar latências críticas.
  • Métricas: contador e histograma exportados periodicamente via OTLPMetricExporter.
  • Logs com trace_id/span_id adicionados pelo OTelFormatter.

Notas de implementacao

Coloque a configuração de providers no startup do processo, antes de criar clientes/engines que possam iniciar threads. Para aplicações ASGI que usam reload, cuidado com múltiplas inicializações duplicando exportadores; detecte env de reload e execute apenas no processo principal.

Observacoes sobre métricas

MeterProvider e exporters de métricas são relativamente novos em Python. Confirmar compatibilidade de versões entre opentelemetry-sdk, exportadores e instrumentations. Em produção, ajuste export_interval e tamanho de buffer para equilibrar custo e latência.

5) Debugging e troubleshooting

Erros comuns

Listagem de armadilhas que eu vejo com frequência:

  1. Instrumentadores rodando antes do TracerProvider configurado — causa spans sem provider e dados não exportados.
  2. OTLP endpoint incorreto (porta gRPC 4317 vs HTTP 4318) — verificar URL e protocolo.
  3. Versões incompatíveis entre instrumentations e SDK — confira changelogs e use versões alinhadas.
  4. Métricas não aparecendo por usar reader incorreto ou esquecer set_meter_provider.
  5. Logs sem trace_id porque Formatter usa get_current_span fora do contexto do request.
Dica: isole provider e instrumentação em um módulo único e importe ele antes de iniciar a app. Isso reduz chances de inicialização duplicada ou fora de ordem.

Como diagnosticar

Passos práticos:

  • 1) Verifique se spans são criados localmente: adicione um SpanProcessor que escreve spans no stdout temporariamente.
  • 2) Teste exportador com um script simples que cria um span e faz flush no final.
  • 3) Confirme que Collector ou backend está recebendo tráfego: use tcpdump, netstat ou logs do collector.

Debug: testar exporter manualmente

Exemplo rápido para validar que o OTLPTraceExporter alcança o collector. Execute separadamente para isolar problemas de app/ASGI.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint='http://localhost:4318/v1/traces')
provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer('manual-test')

with tracer.start_as_current_span('manual-test-span') as span:
    span.set_attribute('test', True)

print('span created and exported (check collector logs)')

Se o collector não receber, verifique endpoint, firewall, e versão do exporter (HTTP vs gRPC). Use SimpleSpanProcessor para testes porque é síncrono e mais fácil de observar.

Armadilhas especificas de FastAPI/uvicorn

Uvicorn com reload cria processos filhos que podem re-inicializar providers. Para evitar duplicação, verifique variáveis de ambiente como 'PYTHONINSPECT' ou rode sem reload em produção. Outra armadilha: executar instrumentacao no módulo de rotas em vez do módulo principal, o que pode causar instrumentação parcial.

6) Conclusao e passos praticos

Checklist para entrega

Ao finalizar instrumentação de um serviço FastAPI, valide o seguinte:

  • Providers (Tracer/Meter) configurados antes da instrumentação.
  • Batch/Periodic readers configurados para produção.
  • Sampling definido de acordo com custo e necessidade de investigação.
  • Logs correlacionados com trace_id e span_id para facilitar investigação.
  • Métricas com labels limitados para evitar cardinalidade alta.

Recomendações finais

Instrumentação é um processo iterativo. Comece com traces de alto nível e métricas SLO, adicione spans e logs conforme incidentes surgem. Automatize testes para garantir que spans e métricas sejam enviados em pipelines CI quando aplicável.

Implementações bem-sucedidas seguem um padrão: proveito das instrumentações automáticas, spans manuais apenas para casos críticos, métricas agregadas e logs enriquecidos. Isso oferece observabilidade prática com custo controlado.

Recursos adicionais

Consulte a documentação oficial do OpenTelemetry Python, exemplos dos instrumentadores (fastapi, httpx, sqlalchemy) e guias do seu backend (OTel Collector, Grafana Tempo, Prometheus). Mantendo versões compatíveis você evita a maior parte das surpresas operacionais.

Implementando as práticas descritas você terá traces, métricas e logs correlacionados de forma consistente, com baixo risco de overhead descontrolado e capacidade real de investigação de problemas em produção.

Assuntos: Python, opentelemetry, tracing, observability

Leia também