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:

  1. Um alerta dispara indicando latência alta (métrica)
  2. O dashboard mostra o trace_id das requisições lentas
  3. Ao clicar no trace_id, o Jaeger exibe que o span "consulta-banco" está demorando 3 segundos
  4. Os logs correlacionados mostram "query_timeout" e "connection_pool_exhausted"
  5. 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.