Observabilidade com OpenTelemetry: logs, métricas e traces centralizados
1. Introdução à Observabilidade e OpenTelemetry
A observabilidade em sistemas distribuídos modernos apoia-se em três pilares fundamentais: logs, métricas e traces. Logs registram eventos discretos com contexto textual, métricas fornecem agregações numéricas sobre o comportamento do sistema em intervalos de tempo, e traces rastreiam o fluxo de requisições através de múltiplos serviços. Sem a integração desses três elementos, diagnosticar problemas em arquiteturas de microsserviços torna-se uma tarefa quase impossível.
OpenTelemetry surge como o padrão aberto mais adotado pela indústria para coleta e exportação de telemetria. Mantido pela Cloud Native Computing Foundation (CNCF), ele unifica a instrumentação de aplicações, eliminando a necessidade de agentes proprietários para cada backend de observabilidade. A centralização dos dados de telemetria proporciona visibilidade unificada, correlação entre eventos e debugging ágil, reduzindo o tempo médio de resolução de incidentes.
2. Arquitetura do OpenTelemetry: Componentes e Fluxo de Dados
A arquitetura do OpenTelemetry é composta por três camadas principais:
SDKs e APIs — Bibliotecas que permitem instrumentar aplicações em linguagens como Go, Java, Python, Node.js e .NET. Elas fornecem interfaces para criar spans, registrar métricas e emitir logs estruturados.
Collectors — Componentes centrais que recebem, processam e exportam telemetria. Podem ser configurados como agentes (sidecar) ou gateways centralizados.
Exporters e backends — Conectores que enviam dados para sistemas como Prometheus, Jaeger, Loki, Elasticsearch e Grafana.
O fluxo típico de dados segue este pipeline:
Aplicação → SDK OpenTelemetry → Collector → Backend
O Collector aplica transformações, filtragem e sampling antes de encaminhar os dados para múltiplos destinos simultaneamente.
3. Coleta e Gerenciamento de Logs com OpenTelemetry
A instrumentação de logs com OpenTelemetry vai além do registro textual simples. Cada entrada de log deve carregar contexto enriquecido, incluindo trace_id e span_id, permitindo correlação direta com traces distribuídos.
Exemplo de configuração do Collector para pipeline de logs:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
loki:
endpoint: http://loki:3100/loki/api/v1/push
labels:
attributes:
- service.name
- trace_id
service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [loki]
Para enviar logs estruturados a partir de uma aplicação Python:
from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
logger_provider = LoggerProvider()
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter())
)
set_logger_provider(logger_provider)
handler = LoggingHandler()
logger = logging.getLogger("meu-servico")
logger.addHandler(handler)
tracer = trace.get_tracer("meu-servico")
with tracer.start_as_current_span("operacao-critica"):
logger.error("Falha na conexão com banco de dados", extra={"db": "postgres"})
4. Métricas: Monitoramento de Desempenho e Saúde do Sistema
OpenTelemetry suporta três tipos principais de métricas: contadores (counter), histogramas (histogram) e gauges (asynchronous gauge). Contadores acumulam valores monotônicos, histogramas registram distribuições estatísticas, e gauges representam valores instantâneos.
Exemplo de código para exportar métricas para Prometheus:
from opentelemetry import metrics
from opentelemetry.exporter.prometheus import PrometheusMetricsExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
exporter = PrometheusMetricsExporter()
reader = PeriodicExportingMetricReader(exporter, export_interval_ms=5000)
provider = MeterProvider(metric_readers=[reader])
metrics.set_meter_provider(provider)
meter = metrics.get_meter("api-gateway", version="1.0")
request_counter = meter.create_counter(
name="http.requests.total",
description="Total de requisições HTTP",
unit="1"
)
request_duration = meter.create_histogram(
name="http.request.duration",
description="Duração das requisições HTTP",
unit="ms"
)
# Em cada requisição
request_counter.add(1, {"method": "GET", "endpoint": "/users"})
request_duration.record(245.3, {"method": "GET", "endpoint": "/users"})
No Prometheus, as métricas aparecem como:
http_requests_total{method="GET",endpoint="/users"} 1
http_request_duration_ms_count{method="GET",endpoint="/users"} 1
http_request_duration_ms_sum{method="GET",endpoint="/users"} 245.3
5. Tracing Distribuído: Rastreamento de Requisições entre Serviços
Tracing distribuído permite rastrear uma requisição completa através de múltiplos microsserviços. Cada unidade de trabalho é representada por um span, e spans são organizados em traces hierárquicos.
A propagação de contexto utiliza o padrão W3C Trace Context, transmitido via headers HTTP:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: congo=t61rcWkgMzE
Exemplo de instrumentação de microsserviços em Python:
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
# Instrumentação automática do requests
RequestsInstrumentor().instrument()
tracer = trace.get_tracer(__name__)
@tracer.start_as_current_span("processar-pedido")
def processar_pedido(pedido_id):
with tracer.start_as_current_span("validar-estoque"):
# Chamada ao serviço de estoque
response = requests.get(f"http://estoque:5000/verificar/{pedido_id}")
with tracer.start_as_current_span("calcular-frete"):
response = requests.post("http://frete:5000/calcular", json={"pedido": pedido_id})
return {"status": "concluido"}
No Jaeger, o trace exibe a árvore completa de spans com durações individuais, permitindo identificar gargalos de performance.
6. Correlação entre Logs, Métricas e Traces na Prática
A verdadeira potência da observabilidade surge quando logs, métricas e traces são correlacionados. O identificador comum é o trace_id, que permeia todos os três pilares.
Exemplo de dashboard integrado no Grafana:
# Consulta para logs de erro com trace_id
{service="api-gateway"} |= "ERROR" | json | trace_id != ""
# Consulta para métricas de latência do mesmo trace
rate(http_request_duration_ms_bucket{le="500"}[5m])
# Link direto para Jaeger a partir do trace_id
https://jaeger.example.com/trace/${trace_id}
Cenário prático de debugging:
- Um alerta dispara indicando latência alta (métrica)
- O dashboard mostra o trace_id das requisições lentas
- Ao clicar no trace_id, o Jaeger exibe que o span "consulta-banco" está demorando 3 segundos
- Os logs correlacionados mostram "query_timeout" e "connection_pool_exhausted"
- A causa raiz é identificada: pool de conexões subdimensionado
7. Boas Práticas e Desafios na Implementação
Instrumentação manual vs. automática — A instrumentação automática (auto-instrumentation) cobre bibliotecas comuns como HTTP, gRPC e bancos de dados, reduzindo o esforço inicial. A instrumentação manual é necessária para lógica de negócio específica.
Gerenciamento de volume de dados — Sampling é essencial para controlar custos. Head-based sampling decide no início do trace quais requisições serão amostradas. Tail-based sampling analisa traces completos antes de decidir, preservando informações valiosas.
# Configuração de sampling no Collector
processors:
probabilistic_sampler:
hash_seed: 42
sampling_percentage: 10
tail_sampling:
policies:
- name: error-policy
type: status_code
status_code: ERROR
sampling_percentage: 100
- name: slow-policy
type: latency
threshold_ms: 2000
sampling_percentage: 100
Segurança e performance — O overhead do OpenTelemetry é mínimo (tipicamente <5% de CPU). Para ambientes críticos, utilize criptografia TLS na comunicação entre SDK e Collector, e configure rate limiting no receiver.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
transport: tls
max_recv_msg_size_mib: 16
# Rate limiting
limit_config:
throttle: 1000
Desafios comuns incluem versionamento de schemas, consistência de atributos entre times e latência na propagação de contexto entre serviços assíncronos (filas, eventos). A adoção de naming conventions e a utilização de semantic conventions do OpenTelemetry mitigam esses problemas.
Referências
- OpenTelemetry Documentation — Documentação oficial completa com guias de instrumentação, configuração de collectors e exportadores para todos os pilares de observabilidade.
- OpenTelemetry Collector Configuration — Referência detalhada sobre configuração de receivers, processors e exporters no Collector, incluindo pipelines para logs, métricas e traces.
- Grafana Labs - OpenTelemetry and Grafana — Guia prático para integrar OpenTelemetry com Grafana, Loki, Tempo e Prometheus, com exemplos de dashboards correlacionados.
- Jaeger Documentation - OpenTelemetry Integration — Tutorial oficial sobre como configurar o Jaeger como backend para traces coletados via OpenTelemetry, incluindo sampling e propagação de contexto.
- CNCF - OpenTelemetry Overview — Visão geral do projeto OpenTelemetry mantido pela Cloud Native Computing Foundation, com casos de uso e especificações técnicas dos três sinais de telemetria.