← voltar ao índice

· 27 min de leitura · por Tiago

OpenTelemetry Collector no Kubernetes em produção

Introdução

No artigo anterior, vimos como instrumentar uma aplicação .NET com OpenTelemetry, correlacionar logs, métricas e traces, enviar telemetria via OTLP e colocar um OpenTelemetry Collector entre a aplicação e os backends de observabilidade.

Essa arquitetura funciona muito bem para começar. O problema aparece quando ela deixa de ser um experimento e passa a receber tráfego real.

Em produção, o Collector deixa de ser apenas um processo que recebe dados e passa a fazer parte de uma cadeia crítica. Se ele ficar sem memória, traces podem desaparecer. Se o backend de observabilidade ficar indisponível, filas podem crescer até consumir todos os recursos. Se o tail sampling for escalado de forma incorreta, partes do mesmo trace podem cair em instâncias diferentes e a decisão de amostragem deixa de enxergar a operação completa.

Também surgem perguntas que não aparecem em ambientes pequenos:

Qual deve ser a topologia dos Collectors? Um por aplicação, um por nó ou um conjunto centralizado?

Onde devemos coletar logs do Kubernetes?

Onde faz sentido enriquecer telemetria com metadados de pod e namespace?

Como escalar horizontalmente sem quebrar tail sampling?

O que acontece quando Tempo, Datadog, Azure Monitor, Elastic ou outro backend fica fora do ar?

Como monitorar o próprio pipeline de observabilidade?

Neste artigo, vamos evoluir a arquitetura anterior para um desenho de produção baseado em OpenTelemetry Collector no Kubernetes. O foco não será apenas configuração. Vamos entender por que Agent, Gateway, filas, retries, armazenamento persistente e roteamento por TraceId existem, quais problemas resolvem e quais novos riscos introduzem.

A ideia central é simples: observabilidade também é um sistema distribuído. Portanto, ela precisa ser projetada com os mesmos cuidados que aplicamos às aplicações que estamos tentando observar.

O Collector passa a ser parte da plataforma

É comum começar com uma arquitetura assim:

Aplicação .NET
    |
    | OTLP
    v
OpenTelemetry Collector
    |
    v
Backend de observabilidade

Para um ambiente pequeno, isso pode ser suficiente.

O problema é que um único Collector começa a acumular responsabilidades. Ele recebe telemetria de todas as aplicações, coleta logs, consulta a API do Kubernetes, adiciona metadados, faz batching, aplica sampling, transforma atributos, mantém filas e exporta para um ou mais backends.

À medida que o volume aumenta, cada responsabilidade possui uma característica diferente.

Coleta de logs é local ao nó porque os arquivos dos containers estão naquele nó.

Métricas de host também são naturalmente locais.

Tail sampling precisa enxergar todos os spans de um trace antes de decidir se ele será mantido.

Credenciais para backends externos deveriam ficar em poucos lugares.

Processamento pesado deveria poder escalar independentemente da quantidade de nós do cluster.

Essa diferença de responsabilidades é o motivo pelo qual uma arquitetura madura normalmente separa Collectors em funções.

Uma topologia bastante útil é:

Aplicações
    |
    | OTLP
    v
Agents, um por nó
    |
    | OTLP interno
    v
Gateways
    |
    | processamento centralizado
    v
Backends

O Agent fica próximo da origem da telemetria. No Kubernetes, geralmente é executado como DaemonSet, portanto existe uma instância em cada nó.

O Gateway fica centralizado. Normalmente é executado como Deployment ou StatefulSet, com uma ou mais réplicas.

Essa separação não é obrigatória. Ela existe para resolver problemas concretos de escala, segurança e operação.

Agent, Gateway ou os dois?

Antes de criar YAMLs, precisamos entender as opções.

Somente Agent

No modelo Agent, cada nó executa seu Collector e envia os dados diretamente aos backends.

Pod A ----\nPod B ----- Agent do nó 1 ---- Backend
Pod C ----/

Pod D ----\nPod E ----- Agent do nó 2 ---- Backend

Esse modelo é simples e funciona muito bem quando a maior parte do trabalho é local.

O Agent pode ler logs em /var/log/pods, coletar métricas do kubelet e enriquecer dados com informações do nó e dos pods.

A desvantagem aparece quando existe lógica centralizada.

Imagine que todas as instâncias precisem conhecer credenciais de um backend externo. Agora cada Agent possui esse segredo.

Se você quiser alterar uma regra de filtragem, todos os Agents precisam receber a configuração.

Tail sampling também se torna complicado porque spans do mesmo trace podem ter sido produzidos em nós diferentes.

Somente Gateway

No modelo Gateway, as aplicações enviam diretamente para um serviço central.

Pod A ---\nPod B ----\nPod C ----- Gateway ---- Backend
Pod D ----/
Pod E ---/

Esse desenho reduz a quantidade de componentes e centraliza políticas, credenciais e processamento.

Ele funciona muito bem quando a aplicação já envia logs, métricas e traces via OTLP e você não precisa ler arquivos locais nem coletar dados específicos de cada host.

O problema é que algumas fontes de telemetria são naturalmente locais. Para ler logs de containers com filelog, por exemplo, o Collector precisa ter acesso aos arquivos do nó. Um Gateway central não consegue montar simultaneamente o filesystem de todos os nós.

Agent mais Gateway

A combinação dos dois modelos costuma funcionar melhor em clusters médios e grandes.

Pods do nó
    |
    v
Agent
    |
    | OTLP
    v
Gateway
    |
    v
Backends

O Agent cuida do que é local. O Gateway cuida do que é central.

Isso cria mais componentes para operar, mas também permite que cada camada seja dimensionada de acordo com sua função.

O ponto importante é não adotar essa arquitetura apenas porque parece mais sofisticada. Em um cluster pequeno, com poucas aplicações e sem necessidade de coleta local, um Gateway único pode ser melhor justamente por ser mais simples.

Complexidade também é custo operacional.

Configurando o Agent como DaemonSet

Vamos começar pela camada mais próxima das aplicações.

O Helm Chart oficial do OpenTelemetry Collector permite executar o Collector como DaemonSet e possui presets úteis para Kubernetes.

Um values-agent.yaml pode começar assim:

mode: daemonset

image:
  repository: otel/opentelemetry-collector-k8s

presets:
  logsCollection:
    enabled: true
    includeCollectorLogs: false

  kubernetesAttributes:
    enabled: true

  kubeletMetrics:
    enabled: true

config:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: ${env:MY_POD_IP}:4317
        http:
          endpoint: ${env:MY_POD_IP}:4318

  processors:
    memory_limiter:
      check_interval: 1s
      limit_percentage: 75
      spike_limit_percentage: 15

    batch:
      send_batch_size: 8192
      timeout: 5s

  exporters:
    otlp/gateway:
      endpoint: otel-gateway.observability.svc.cluster.local:4317
      tls:
        insecure: true
      sending_queue:
        enabled: true
        queue_size: 2048
      retry_on_failure:
        enabled: true
        initial_interval: 1s
        max_interval: 10s
        max_elapsed_time: 5m

  service:
    pipelines:
      traces:
        receivers:
          - otlp
        processors:
          - memory_limiter
          - k8sattributes
          - batch
        exporters:
          - otlp/gateway

      metrics:
        receivers:
          - otlp
          - kubeletstats
        processors:
          - memory_limiter
          - k8sattributes
          - batch
        exporters:
          - otlp/gateway

      logs:
        receivers:
          - otlp
          - filelog
        processors:
          - memory_limiter
          - k8sattributes
          - batch
        exporters:
          - otlp/gateway

O primeiro ponto importante é o modo daemonset.

Cada nó recebe uma instância do Collector. Isso permite que o preset logsCollection monte os caminhos necessários e leia os arquivos de log dos containers daquele nó.

Também evita um problema clássico: se dois Collectors lerem os mesmos arquivos, os mesmos logs podem ser enviados duas vezes.

includeCollectorLogs: false evita que o Collector leia seus próprios logs através do mesmo pipeline que está exportando. Essa configuração parece pequena, mas protege contra loops. Um Collector pode registrar que exportou um log, capturar esse novo log, exportá-lo novamente e gerar uma explosão de volume.

O preset kubernetesAttributes adiciona o k8sattributes processor e as permissões necessárias para enriquecer os sinais com metadados como namespace, pod e nó.

Esse enriquecimento é extremamente útil porque transforma uma investigação baseada apenas em service.name em algo operacionalmente útil.

Você passa a conseguir perguntar:

Qual pod apresentou erro?

O problema ocorreu somente em um namespace?

A latência começou depois de uma nova réplica?

Existe correlação com um nó específico?

O memory_limiter aparece antes de processamentos mais caros. A ideia é criar uma barreira contra crescimento descontrolado de memória. Ele não substitui resource requests e limits do Kubernetes, mas ajuda o Collector a aplicar backpressure e recusar dados antes de chegar a uma situação de OOMKilled.

O batch reduz a quantidade de requisições enviadas ao próximo estágio. Em vez de exportar cada pequeno conjunto imediatamente, o Collector agrupa dados.

Isso melhora eficiência, mas o tamanho do batch deve ser compatível com o backend. Batches grandes demais podem gerar payloads rejeitados ou aumentar picos de memória.

Como as aplicações encontram o Agent local?

Ter um DaemonSet não significa automaticamente que cada aplicação enviará dados ao Collector do mesmo nó.

Uma opção é expor o Agent usando a rede do host ou criar mecanismos de descoberta específicos. Outra alternativa é deixar as aplicações enviarem para um Service que distribui entre Agents.

A segunda opção é mais simples, mas perde parte da característica de proximidade.

Quando o objetivo principal do Agent é coletar logs e métricas do nó, isso pode não ser um problema. A aplicação pode continuar enviando OTLP diretamente para um Gateway, enquanto o Agent cuida apenas da coleta local.

Por isso existem duas arquiteturas válidas:

Aplicação ---- Gateway
     |
logs do container
     |
     v
   Agent ------ Gateway

ou:

Aplicação ---- Agent local ---- Gateway

A primeira reduz a dependência da aplicação em descobrir o Agent local.

A segunda concentra todo o tráfego de telemetria do nó no Agent e pode ser útil quando existe isolamento de rede ou necessidade de processamento local.

A decisão deve considerar topologia de rede, facilidade de operação e necessidade real de processamento por nó.

Criando um Gateway central

O Gateway recebe telemetria dos Agents ou diretamente das aplicações.

Um Collector configurado como Gateway pode ter uma configuração semelhante a esta:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
    spike_limit_percentage: 15

  batch:
    send_batch_size: 8192
    send_batch_max_size: 16384
    timeout: 5s

exporters:
  otlp/traces:
    endpoint: tempo.observability.svc.cluster.local:4317
    tls:
      insecure: true
    sending_queue:
      enabled: true
      queue_size: 5000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 10m

  otlp/metrics:
    endpoint: metrics-backend.observability.svc.cluster.local:4317
    tls:
      insecure: true
    sending_queue:
      enabled: true
      queue_size: 5000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 10m

  otlp/logs:
    endpoint: logs-backend.observability.svc.cluster.local:4317
    tls:
      insecure: true
    sending_queue:
      enabled: true
      queue_size: 5000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 10m

service:
  pipelines:
    traces:
      receivers:
        - otlp
      processors:
        - memory_limiter
        - batch
      exporters:
        - otlp/traces

    metrics:
      receivers:
        - otlp
      processors:
        - memory_limiter
        - batch
      exporters:
        - otlp/metrics

    logs:
      receivers:
        - otlp
      processors:
        - memory_limiter
        - batch
      exporters:
        - otlp/logs

Essa configuração já separa os três sinais em destinos diferentes.

Esse detalhe é importante porque OpenTelemetry não obriga logs, métricas e traces a terminarem no mesmo backend.

Uma arquitetura bastante comum é usar um backend especializado para cada sinal.

traces  -> Tempo
metrics -> Prometheus compatível
logs    -> Loki

Outra organização pode enviar tudo para uma plataforma SaaS.

A aplicação não precisa conhecer essa decisão. Ela envia OTLP para uma camada interna estável e o Gateway decide o roteamento.

Esse desacoplamento é uma das maiores vantagens arquiteturais do Collector.

O que acontece quando o backend fica fora do ar?

Esse é um dos pontos mais importantes de uma implantação de produção.

Imagine que o backend de traces fique indisponível por dez minutos.

As aplicações continuam produzindo telemetria.

Os Agents continuam recebendo.

Os Gateways continuam recebendo.

Mas o último estágio não consegue exportar.

Sem uma estratégia explícita, o pipeline começa a acumular dados em memória até atingir limites ou descartar telemetria.

O Collector possui mecanismos de fila e retry justamente para lidar com falhas temporárias.

No exemplo anterior usamos:

sending_queue:
  enabled: true
  queue_size: 5000

retry_on_failure:
  enabled: true
  initial_interval: 5s
  max_interval: 30s
  max_elapsed_time: 10m

A fila absorve uma indisponibilidade curta.

O retry tenta novamente usando intervalos progressivos.

Mas isso não cria armazenamento infinito.

Se o backend permanecer indisponível e a fila atingir sua capacidade, novos dados poderão ser descartados.

A pergunta correta não é "como impedir qualquer perda?", porque isso pode exigir recursos ilimitados.

A pergunta correta é:

Quanto tempo de indisponibilidade queremos absorver?

Qual volume de telemetria entra por segundo?

Quanto de memória ou disco estamos dispostos a reservar?

Qual tipo de telemetria pode ser perdido?

Um ambiente que recebe 1.000 spans por segundo possui um problema completamente diferente de outro que recebe 500.000.

Por isso queue_size: 5000 não deve ser tratado como um número mágico.

Ele precisa ser dimensionado com base em carga real.

Quando usar armazenamento persistente

Filas em memória ajudam durante falhas do backend, mas não sobrevivem necessariamente à perda do processo.

Se o pod do Collector morrer, os dados que estavam somente em memória desaparecem.

Para cenários em que essa perda é relevante, o Collector pode usar armazenamento persistente para a sending queue.

Um exemplo simplificado é:

extensions:
  file_storage:
    directory: /var/lib/otelcol/storage

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
    spike_limit_percentage: 15

  batch:
    timeout: 5s

exporters:
  otlp/backend:
    endpoint: telemetry.example.internal:4317
    tls:
      ca_file: /etc/otel/tls/ca.crt
    sending_queue:
      enabled: true
      queue_size: 10000
      storage: file_storage
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 30m

service:
  extensions:
    - file_storage

  pipelines:
    traces:
      receivers:
        - otlp
      processors:
        - memory_limiter
        - batch
      exporters:
        - otlp/backend

Nesse desenho, a fila pode utilizar uma extensão de armazenamento em arquivo.

No Kubernetes, o diretório precisa estar associado a um volume apropriado.

Se você montar apenas emptyDir, os dados podem sobreviver ao restart do processo dentro do mesmo pod, mas não são uma garantia contra recriação do pod em outro nó.

Para uma estratégia realmente persistente, normalmente é necessário combinar o Collector com armazenamento persistente e uma topologia adequada, frequentemente StatefulSet.

Isso introduz outro trade-off.

Persistência aumenta resiliência, mas também aumenta custo, latência de I/O e complexidade operacional.

Nem toda telemetria exige esse nível de durabilidade.

Logs de auditoria, por exemplo, podem ter requisitos muito diferentes de métricas de CPU.

Se um dado é juridicamente ou operacionalmente obrigatório, talvez ele nem devesse depender exclusivamente de um pipeline de observabilidade. Eventos de auditoria críticos frequentemente merecem armazenamento transacional próprio, além da cópia enviada ao stack de observabilidade.

Tail sampling muda a arquitetura

No artigo anterior, vimos que sampling reduz o volume de traces.

Head sampling decide no início da requisição.

Tail sampling espera mais informações do trace antes de decidir.

Essa diferença permite regras muito mais úteis.

Podemos manter:

todos os traces com erro;

todos os traces acima de determinada latência;

traces de clientes ou operações críticas;

uma pequena amostra probabilística do tráfego saudável.

Uma configuração pode ser:

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 5000

    decision_cache:
      sampled_cache_size: 200000
      non_sampled_cache_size: 1000000

    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes:
            - ERROR

      - name: keep-slow-traces
        type: latency
        latency:
          threshold_ms: 1500

      - name: baseline-sample
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

Essa política tenta preservar o que normalmente é mais útil para investigação.

Erros ficam.

Operações lentas ficam.

Uma pequena parcela do tráfego normal também fica para comparação.

O decision_wait determina quanto tempo o processor espera antes de tomar a decisão. Se o valor for muito baixo, spans atrasados podem chegar depois.

Se for muito alto, mais traces precisam ficar em memória simultaneamente.

num_traces limita a quantidade de traces mantidos para decisão. Quando a carga real ultrapassa o dimensionamento, traces podem ser removidos cedo demais.

Isso mostra um ponto importante: tail sampling troca custo de armazenamento no backend por custo de memória e processamento no Collector.

Ele não elimina custo. Ele move o lugar onde parte desse custo acontece.

O erro clássico ao escalar tail sampling

Considere três réplicas de Gateway:

              +--> Gateway 1
Aplicações ---+--> Gateway 2
              +--> Gateway 3

Na frente delas existe um Service Kubernetes comum.

O Service distribui conexões.

Agora imagine um trace distribuído:

Trace ABC

span da API       -> Gateway 1
span do serviço B -> Gateway 2
span do worker    -> Gateway 3

Nenhuma instância enxerga o trace completo.

O Gateway 1 pode enxergar uma requisição aparentemente saudável.

O Gateway 2 pode enxergar o span que contém o erro.

O Gateway 3 pode enxergar uma operação lenta.

A decisão de tail sampling fica fragmentada.

Por isso existe uma regra fundamental: todos os spans de um mesmo trace precisam chegar à mesma instância responsável pela decisão de tail sampling.

Um balanceador round-robin comum não garante isso.

Roteando por TraceId

Uma solução é criar duas funções distintas na camada central.

A primeira recebe traces e faz roteamento determinístico por TraceId.

A segunda executa o tail sampling.

Agents
  |
  v
Routers
  |
  | hash por TraceId
  v
Tail Sampler 1
Tail Sampler 2
Tail Sampler 3
  |
  v
Backend

O primeiro nível pode usar o load balancing exporter.

Uma configuração conceitual fica assim:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
    spike_limit_percentage: 15

  batch:
    timeout: 2s

exporters:
  loadbalancing:
    routing_key: traceID

    protocol:
      otlp:
        tls:
          insecure: true
        sending_queue:
          enabled: true
          queue_size: 2048

    resolver:
      dns:
        hostname: otel-tail-sampler-headless.observability.svc.cluster.local
        port: 4317

service:
  pipelines:
    traces:
      receivers:
        - otlp
      processors:
        - memory_limiter
        - batch
      exporters:
        - loadbalancing

O objetivo dessa camada não é decidir quais traces ficam.

Ela decide para qual sampler cada TraceId deve ir.

Com roteamento consistente, os spans do trace ABC tendem a chegar à mesma instância do sampler.

A camada seguinte contém o tail_sampling processor.

É importante separar essas responsabilidades porque tail sampling mantém estado temporário em memória. Quando misturamos roteamento e sampling de maneira incorreta, mudanças na quantidade de réplicas podem causar comportamento difícil de prever.

Para descoberta dos samplers, podemos expor um Service headless:

apiVersion: v1
kind: Service
metadata:
  name: otel-tail-sampler-headless
  namespace: observability
spec:
  clusterIP: None

  selector:
    app: otel-tail-sampler

  ports:
    - name: otlp-grpc
      port: 4317
      targetPort: 4317

Um Service headless permite que DNS represente os endpoints dos pods em vez de esconder todos atrás de um único IP virtual.

Isso é importante porque o router precisa conhecer os destinos reais para distribuir traces de forma consciente.

Um Service Kubernetes comum resolve muito bem o problema de balancear requisições stateless. Tail sampling não é completamente stateless durante a janela de decisão, portanto exige uma estratégia diferente.

Escalar não significa apenas aumentar réplicas

Quando vemos CPU alta, a primeira reação no Kubernetes costuma ser criar um HPA.

Para Gateways stateless, isso pode funcionar muito bem.

Para tail samplers, a situação exige mais cuidado.

Adicionar ou remover uma réplica altera o conjunto de destinos do roteamento. Durante a mudança, traces que já estavam em andamento podem ter spans distribuídos de forma diferente.

Além disso, cada sampler mantém traces pendentes em memória. Uma redução brusca de réplicas pode destruir estado ainda não decidido.

Isso não significa que tail sampling não possa escalar horizontalmente.

Significa que o autoscaling precisa ser mais conservador.

Uma configuração de HPA para uma camada stateless pode ser:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: otel-gateway
  namespace: observability
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: otel-gateway

  minReplicas: 3
  maxReplicas: 12

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60

    scaleDown:
      stabilizationWindowSeconds: 600
      policies:
        - type: Percent
          value: 25
          periodSeconds: 120

  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 75

A janela longa de scale down é deliberada.

Reduzir réplicas agressivamente pode economizar alguns recursos, mas aumenta churn exatamente em um componente que está transportando dados operacionais importantes.

Mesmo no Gateway stateless, oscilar continuamente entre duas e oito réplicas pode causar reconexões, redistribuição de carga e picos de fila.

Em tail samplers, o cuidado deve ser ainda maior.

Antes de ativar autoscaling agressivo, meça taxa de traces, quantidade de spans por trace, memória por trace pendente, tempo de decisão e comportamento durante rollout.

Não use CPU e memória como únicos indicadores.

Um Collector pode estar com CPU aceitável e ainda assim possuir filas crescendo porque o backend está lento.

Monitorando o próprio Collector

Um dos paradoxos mais comuns em observabilidade é ter dashboards excelentes da aplicação e nenhum monitoramento do pipeline que produz os dados desses dashboards.

Se o Collector estiver descartando 20% dos traces, a ausência dos dados pode parecer que o sistema está saudável.

Por isso o próprio Collector precisa ser observado.

Ele expõe métricas internas que ajudam a responder perguntas como:

Quantos spans foram recebidos?

Quantos foram recusados?

A fila de exportação está crescendo?

Existem falhas ao enviar dados?

Qual é o consumo de memória?

O tail sampling está descartando traces cedo demais?

Um conjunto de alertas pode começar com consultas semelhantes a estas:

otelcol_exporter_queue_size
/
otelcol_exporter_queue_capacity
> 0.8

Essa expressão procura filas acima de 80% da capacidade.

Uma fila alta durante poucos segundos pode ser normal. O problema aparece quando ela permanece crescendo ou próxima do limite.

Para falhas de exportação:

sum by (exporter) (
  rate(otelcol_exporter_send_failed_spans_total[5m])
) > 0

Para spans recusados na entrada:

sum by (receiver) (
  rate(otelcol_receiver_refused_spans_total[5m])
) > 0

Os nomes exatos podem variar de acordo com a versão do Collector, o nível de telemetria interna e a forma como as métricas são exportadas. Em Prometheus, métricas monotônicas podem receber o sufixo _total.

Por isso, antes de copiar um alerta, consulte /metrics no Collector e valide o nome real.

Mais importante do que o nome específico é entender os quatro grupos de indicadores que devemos acompanhar.

Entrada

Quanto de telemetria está chegando?

Uma queda abrupta pode significar que as aplicações pararam de enviar, que existe problema de rede ou que um receiver não está configurado corretamente.

Processamento

Os processors estão conseguindo acompanhar o volume?

No tail sampling, observe sinais de traces removidos cedo demais e latência da decisão.

Fila

A fila está crescendo?

Fila crescente significa que a saída está mais lenta do que a entrada.

Exportação

O backend está aceitando dados?

Erros de envio, retries contínuos e timeouts indicam degradação downstream.

Esses quatro pontos formam uma visão muito melhor do pipeline do que olhar apenas CPU e memória do pod.

Memory limiter não corrige Collector subdimensionado

O memory_limiter é essencial, mas frequentemente é interpretado de forma errada.

Ele não cria memória.

Ele não aumenta throughput.

Ele não corrige um exporter lento.

Seu papel é proteger o processo contra crescimento sem controle.

Considere um pod com limite de 1 GiB.

resources:
  requests:
    cpu: 500m
    memory: 512Mi

  limits:
    cpu: "2"
    memory: 1Gi

E um memory_limiter configurado para trabalhar abaixo do limite do container:

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
    spike_limit_percentage: 15

A ideia é deixar margem para o runtime, buffers, exporters e picos temporários.

Configurar o limiter exatamente no limite do container é perigoso porque o Kubernetes pode encerrar o processo antes que o Collector consiga reagir.

Também existe outro erro comum: aumentar uma fila até um valor enorme para "não perder dados".

Filas ocupam recursos.

Uma fila maior pode simplesmente transformar uma falha de backend em um OOMKilled mais lento.

Resiliência precisa ser dimensionada.

Backpressure precisa fazer parte do desenho

Todo sistema possui um limite de throughput.

Se entram 100 mil itens por segundo e o backend consegue processar apenas 80 mil, o sistema acumula 20 mil por segundo.

Nenhuma configuração de retry resolve matemática.

Retries ajudam quando a falha é temporária.

Filas ajudam a absorver picos.

Persistência ajuda a sobreviver a reinícios.

Nenhum desses mecanismos aumenta a capacidade permanente do destino.

Por isso o pipeline precisa possuir uma estratégia de backpressure e descarte.

Dependendo do negócio, pode ser melhor reduzir traces saudáveis do que perder todos os traces.

Pode ser melhor filtrar logs extremamente verbosos antes de enviá-los.

Pode ser melhor aumentar sampling em períodos de tráfego excepcional.

Esse tipo de decisão deve ser explícito.

A pior estratégia é deixar o primeiro OOMKilled decidir aleatoriamente qual telemetria será perdida.

Kubernetes attributes e o problema da associação

O k8sattributes processor consegue enriquecer telemetria com metadados do Kubernetes.

Para isso ele precisa associar a telemetria recebida ao pod correto.

Quando o Collector está próximo da origem, essa associação tende a ser mais simples.

Quando existem proxies, balanceadores ou múltiplos níveis de Collector, o IP observado pode não ser mais o IP original do pod.

Uma abordagem robusta é enviar identificadores do Kubernetes como resource attributes desde a aplicação ou desde uma camada próxima.

O Kubernetes Downward API permite injetar dados como nome, namespace e UID do pod em variáveis de ambiente.

Por exemplo:

env:
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: >-
      k8s.pod.name=$(POD_NAME),
      k8s.pod.uid=$(POD_UID),
      k8s.namespace.name=$(POD_NAMESPACE)

  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name

  - name: POD_UID
    valueFrom:
      fieldRef:
        fieldPath: metadata.uid

  - name: POD_NAMESPACE
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace

Em manifests reais, é comum montar essas variáveis individualmente e construir OTEL_RESOURCE_ATTRIBUTES no mecanismo de deployment ou entrypoint, porque a expansão de variáveis depende de como o container e o manifesto são definidos.

O ponto arquitetural é não depender cegamente do IP remoto quando o caminho possui proxies.

Identificadores estáveis melhoram a associação.

Também precisamos lembrar que o k8sattributes exige permissões RBAC para consultar os recursos necessários.

Dar cluster-admin ao Collector é fácil, mas excessivo.

O ideal é conceder apenas get, list e watch para os recursos realmente usados no enriquecimento.

Segurança do endpoint OTLP

Em exemplos de laboratório usamos:

endpoint: 0.0.0.0:4317

Isso faz o receiver escutar em todas as interfaces do pod.

Em produção, esse comportamento precisa ser combinado com controles de rede.

Não devemos assumir que "está dentro do cluster" significa "é confiável".

Um endpoint OTLP aberto pode receber grandes volumes de dados e se tornar vetor de consumo de CPU, memória e armazenamento.

Uma NetworkPolicy pode limitar quem envia dados ao Gateway:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-otlp-to-gateway
  namespace: observability
spec:
  podSelector:
    matchLabels:
      app: otel-gateway

  policyTypes:
    - Ingress

  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              observability-client: "true"

      ports:
        - protocol: TCP
          port: 4317

        - protocol: TCP
          port: 4318

Nesse modelo, apenas namespaces explicitamente marcados podem acessar OTLP.

Ainda é necessário verificar se o CNI do cluster aplica NetworkPolicy e como os labels de namespace são gerenciados.

Para tráfego externo, use TLS.

Credenciais de backends devem ficar em Secrets, Key Vaults ou mecanismos equivalentes, não diretamente no values.yaml versionado.

Também evite colocar dados sensíveis em atributos de telemetria.

Um customer.email pode parecer útil para busca, mas pode criar problemas de privacidade, retenção e custo.

Observabilidade deve trabalhar preferencialmente com identificadores técnicos e valores controlados.

Rollout do Collector também pode causar perda

O Collector é frequentemente atualizado com a mesma naturalidade de qualquer Deployment.

Mas um rollout encerra pods que podem possuir:

traces aguardando decisão de tail sampling;

dados em sending queues;

conexões em retry;

batches ainda não enviados.

Por isso terminationGracePeriodSeconds importa.

Readiness e liveness também importam.

Um pod não deveria receber tráfego antes de estar pronto para processá-lo.

Durante shutdown, ele precisa de tempo para encerrar pipelines e tentar descarregar dados pendentes.

Em ambientes com alto volume, vale testar rollouts deliberadamente.

Execute carga.

Inicie um rollout.

Meça quantos spans entraram.

Meça quantos chegaram ao backend.

Observe filas.

Observe traces incompletos.

A configuração de produção não deveria ser validada apenas com kubectl get pods.

Todos os pods podem estar Running enquanto o pipeline perde dados.

Armadilhas comuns em ambientes reais

Colocar um Service comum antes do tail sampler

O Service distribui tráfego, mas não preserva todos os spans de um TraceId na mesma réplica.

Para tail sampling horizontal, use uma estratégia de roteamento consciente do trace.

Executar dois leitores dos mesmos logs

Dois DaemonSets ou Collectors apontando para os mesmos arquivos podem duplicar logs e custos.

Defina claramente quem é responsável por cada fonte.

Coletar os logs do próprio Collector sem cuidado

Se o Collector exportar seus logs para stdout e o mesmo pipeline ler stdout novamente, pode surgir um loop.

O preset oficial possui proteção para esse cenário. Não remova essa proteção sem entender o fluxo completo.

Aumentar queue_size até o problema desaparecer

Uma fila enorme pode esconder temporariamente um backend lento enquanto aumenta consumo de memória.

Monitore taxa de entrada, taxa de saída e tempo de recuperação.

Usar tail sampling para tudo

Tail sampling é poderoso, mas mais caro e stateful durante a janela de decisão.

Se você só precisa de uma amostra probabilística de 10%, head sampling ou probabilistic sampling pode ser mais simples e eficiente.

Escalar samplers agressivamente

Tail sampling mantém estado por trace. Scale down agressivo aumenta churn e pode afetar traces em andamento.

Prefira estabilidade e capacidade planejada.

Não observar o Collector

Esse talvez seja o erro mais grave.

Se o pipeline falha silenciosamente, todos os dashboards passam a contar uma história incompleta.

Um guia de decisão prático

Use somente Gateway quando suas aplicações já enviam OTLP diretamente, você não precisa ler logs dos nós e quer uma arquitetura simples com processamento centralizado.

Use somente Agent quando a coleta é predominantemente local e cada nó pode exportar diretamente para o backend sem necessidade de políticas centralizadas complexas.

Use Agent mais Gateway quando você precisa combinar coleta local com políticas centralizadas, reduzir exposição de credenciais, controlar egress ou escalar processamento independentemente dos nós.

Use tail sampling quando o valor de uma decisão depende do trace completo. Erros e latências são bons exemplos.

Prefira sampling probabilístico simples quando uma porcentagem fixa é suficiente.

Use sending queue e retry em exporters de rede. Eles são a primeira camada contra indisponibilidades temporárias.

Use armazenamento persistente quando perder a fila durante restart for inaceitável e o custo operacional fizer sentido.

Considere uma fila externa, como Kafka, apenas quando os requisitos de durabilidade e desacoplamento justificarem operar mais um sistema distribuído.

Não adicione Kafka ao pipeline apenas para dizer que ele é "enterprise". Cada componente novo cria mais uma fonte de falha, custo e manutenção.

Uma estratégia de evolução sem big bang

Uma migração para uma arquitetura robusta de OpenTelemetry não precisa acontecer de uma vez.

Comece fazendo as aplicações enviarem OTLP para um endpoint estável.

Depois introduza um Gateway central e mova credenciais e exporters para ele.

Em seguida, adicione Agents somente onde existe uma razão concreta, como coleta de logs, métricas de host ou isolamento de rede.

Monitore o Collector antes de ativar processamentos mais sofisticados.

Colete métricas de fila, falhas de exportação, memória e throughput.

Somente depois introduza tail sampling.

Primeiro execute com uma única instância de sampler para validar políticas.

Depois adicione a camada de roteamento por TraceId e teste escala horizontal.

Por último, ajuste autoscaling, persistência e políticas de resiliência com base em números reais.

Essa sequência reduz a quantidade de variáveis alteradas ao mesmo tempo.

Quando algo falhar, você saberá qual mudança provavelmente introduziu o problema.

Arquitetura final de referência

Depois dessas decisões, uma arquitetura mais completa pode ficar assim:

                         Kubernetes Cluster

  +----------------------- Node 1 -----------------------+
  |                                                       |
  |  App A ---- OTLP                                      |
  |  App B ---- OTLP ---- Agent DaemonSet                 |
  |                      |                                |
  +----------------------|--------------------------------+
                         |
                         |
  +----------------------- Node 2 -----------------------+
  |                      |                                |
  |  App C ---- OTLP ---- Agent DaemonSet                 |
  |  App D ---- OTLP                                      |
  |                                                       |
  +----------------------|--------------------------------+
                         |
                         v
                 Router Collectors
                         |
                         | TraceId aware
                         v
               Tail Sampling Collectors
                         |
              +----------+----------+
              |          |          |
              v          v          v
            Traces     Metrics     Logs
            backend    backend     backend

Na prática, métricas e logs não precisam passar pelo mesmo caminho de tail sampling.

Você pode criar pipelines diferentes.

traces  -> router -> tail samplers -> trace backend
metrics -> gateway ----------------> metric backend
logs    -> gateway ----------------> log backend

Essa separação evita forçar todos os sinais através de componentes que só fazem sentido para traces.

É uma ideia importante: compartilhar o Collector não significa compartilhar exatamente o mesmo pipeline.

Receivers, processors e exporters podem ser combinados de maneiras diferentes para cada sinal.

Conclusão

Colocar o OpenTelemetry Collector no Kubernetes é fácil.

Operá-lo corretamente em produção é um problema de arquitetura.

Quando o volume cresce, precisamos pensar em topologia, estado, filas, backpressure, memória, persistência, segurança e escalabilidade.

O modelo Agent mais Gateway ajuda a separar responsabilidades. Agents resolvem problemas locais. Gateways centralizam políticas e egress.

Tail sampling permite preservar traces valiosos, mas exige que todos os spans do mesmo trace cheguem à mesma instância de decisão. Por isso um balanceador comum não é suficiente quando existem múltiplos samplers.

Sending queues e retries tornam o pipeline mais resiliente, mas não criam capacidade infinita. Persistência protege contra alguns tipos de perda, mas adiciona custo operacional.

E talvez a lição mais importante seja esta: o sistema de observabilidade também precisa ser observável.

Você precisa saber quando o Collector está recusando dados, quando filas estão crescendo, quando exporters estão falhando e quando políticas de sampling não conseguem acompanhar a carga.

Caso contrário, o pior incidente não será apenas uma aplicação fora do ar.

Será uma aplicação fora do ar enquanto a plataforma que deveria explicar o problema também está perdendo os dados necessários para investigá-lo.

Fontes

← voltar ao índice