Voltar para o blog

OTTL v1 na prática: transformações de traces, métricas e logs no Collector

Uma plataforma recebe spans com nomes diferentes para a mesma operação, métricas com dimensões inconsistentes e logs contendo dados que não deveriam chegar ao backend. Corrigir cada aplicação é o destino desejável, mas nem sempre acompanha a velocidade necessária para uma migração ou resposta operacional. O Collector oferece um ponto de transformação anterior à exportação, útil para qualidade, governança e controle de dados. (opentelemetry.io)

Este guia propõe uma implementação para uma API de checkout: normalizar nomes de spans, enriquecer uma métrica própria e transformar logs estruturados. O cenário é ilustrativo; não representa resultados medidos em uma empresa.

O que mudou com OTTL v1

O anúncio de 27 de outubro de 2026 estabelece OTTL estável para traces, métricas e logs a partir do Collector Contrib v0.164.0. A garantia de compatibilidade em 1.x abrange a linguagem, as funções padrão e a API Go estável. Recursos experimentais de xottl, funções lambda e profiles não recebem a mesma garantia. (opentelemetry.io)

Isso não torna todos os componentes do Collector estáveis: o transform processor ainda aparece como beta para esses três sinais na documentação da versão utilizada. Este artigo adota a imagem 0.164.0 como baseline reproduzível, não uma tag flutuante. (github.com)

OTTL significa OpenTelemetry Transformation Language, uma linguagem específica de domínio para trabalhar com estruturas de telemetria. Ela não é SQL sobre o backend nem um mecanismo de consulta histórica: integra componentes que processam os dados recebidos. (github.com)

Conceitos que orientam uma regra correta

Uma instrução combina função, argumentos e, opcionalmente, uma condição where:

set(span.attributes["platform.team"], "payments") where resource.attributes["service.name"] == "checkout-api"

Os caminhos identificam campos; os colchetes acessam mapas. Uma chave ausente retorna nil. O contexto determina quais caminhos estão disponíveis. Portanto, antes de escrever uma regra, identifique o tipo e a localização do dado, em vez de presumir que tudo está em attributes. (github.com)

Os contextos relevantes para este exemplo são:

  • `resource`: identidade da origem, acessível a partir dos contextos dos sinais.
  • `scope`: informações do escopo de instrumentação.
  • `span`: nome, atributos, identificadores e outros campos de um span. (github.com)
  • `metric`: metadados da métrica, como nome, descrição e unidade. (github.com)
  • `datapoint`: atributos e valores de um ponto de métrica. (github.com)
  • `log`: corpo, atributos, severidade e campos de correlação de um registro. O log.cache permite guardar resultados intermediários durante a transformação. (github.com)

Funções como set, delete_key e keep_keys modificam dados. ParseJSON converte uma string JSON; IsString ajuda a verificar tipos. Uma string não é necessariamente JSON válido, e o parsing pode falhar. (github.com)

Arquitetura: onde transformar

Para o cenário proposto:

API e workers
  → OTLP/HTTP
  → Collector gateway
  → memory_limiter
  → transform/platform
  → batch
  → exporter
  → backend de observabilidade

OTLP é o OpenTelemetry Protocol. Receivers recebem dados, processors executam etapas intermediárias e exporters entregam o resultado. Declarar um componente não o ativa: ele precisa aparecer em service.pipelines, cuja ordem de processors define o fluxo. (opentelemetry.io)

A proposta posiciona o limitador de memória antes das transformações. Ele pode recusar dados sob pressão; não substitui o limite do contêiner nem garante ausência de encerramento por falta de memória. Dimensione também o comportamento de retry dos emissores. (github.com)

Configuração completa do Collector

Salve o conteúdo abaixo como collector.yaml.

Escopo: laboratório e homologação com dados sintéticos. O receiver não usa TLS e o exporter debug imprime telemetria. Não publique esse endpoint nem encaminhe dados reais para este laboratório.

extensions:
  health_check:
    endpoint: 0.0.0.0:13133

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

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 384
    spike_limit_mib: 96

  transform/platform:
    error_mode: ignore
    trace_statements:
      - context: span
        statements:
          - 'set(span.name, span.attributes["http.route"]) where span.kind == SPAN_KIND_SERVER and IsString(span.attributes["http.route"])'
          - 'delete_key(span.attributes, "user.email")'
          - 'delete_key(span.attributes, "http.request.header.authorization")'

    metric_statements:
      - context: metric
        statements:
          - 'set(metric.description, "Number of completed checkout orders") where metric.name == "checkout.orders.count"'
      - context: datapoint
        statements:
          - 'set(datapoint.attributes["platform.pipeline"], "normalized") where metric.name == "checkout.orders.count"'

    log_statements:
      - context: log
        statements:
          - 'merge_maps(log.cache, ParseJSON(log.body), "upsert") where IsString(log.body)'
          - 'set(log.attributes["event.name"], "checkout.failed") where log.cache["event"] == "checkout.failed"'
          - 'set(log.severity_number, SEVERITY_NUMBER_ERROR) where log.cache["level"] == "ERROR"'
          - 'set(log.severity_text, "ERROR") where log.cache["level"] == "ERROR"'
          - 'keep_keys(log.attributes, ["event.name"])'
          - 'set(log.body, "[body omitted by policy]")'

  batch:
    timeout: 1s
    send_batch_size: 512

exporters:
  debug:
    verbosity: detailed

service:
  extensions: [health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, transform/platform, batch]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, transform/platform, batch]
      exporters: [debug]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, transform/platform, batch]
      exporters: [debug]

Traces: normalização sem reescrever a correlação

Neste contrato didático, o nome do span de servidor passa a ser a rota parametrizada fornecida pela instrumentação, por exemplo /orders/{id}. Uma rota ausente não dispara a alteração. A regra não modifica trace_id, span_id ou relações de parentesco; esses campos existem no contexto, mas não precisam ser alterados para normalizar nomes. (github.com)

A remoção das duas chaves é deliberadamente limitada. Não representa uma política completa de proteção: nomes de spans, eventos, resource attributes e outros campos também precisam entrar na revisão de dados sensíveis. Recomendo corrigir a captura indevida na origem e usar o gateway como defesa adicional.

Métricas: metadados e dimensões são coisas diferentes

O bloco metric modifica a descrição; o bloco datapoint acrescenta uma dimensão controlada. checkout.orders.count e platform.pipeline são nomes próprios deste exemplo, não convenções semânticas oficiais. O valor numérico não é alterado. (github.com)\n Não remova user.id, pod ou outra dimensão de métricas indiscriminadamente para reduzir cardinalidade. Pontos anteriormente distintos podem passar a representar a mesma série. Tampouco mudar unit converte valores, buckets ou temporariedade. A identidade e a semântica de agregação precisam permanecer coerentes. (opentelemetry.io)

Minha preferência é eliminar dimensões inadequadas na instrumentação. Quando isso não for possível, trate a alteração como migração: defina agregação, continuidade histórica, consultas afetadas e critérios de validação.

Logs: extrair seletivamente e tratar o corpo original

O parsing ocorre no cache, não diretamente nos atributos exportados. O exemplo promove apenas um evento conhecido e o nível ERROR; depois restringe atributos e substitui o corpo inteiro. Não copie automaticamente todas as chaves de um JSON não controlado. As funções utilizadas suportam esse fluxo de conversão e edição. (github.com)

A política é propositalmente destrutiva e se aplica a todos os logs deste pipeline. Mesmo quando o parsing falha, o corpo deve ser substituído nas instruções seguintes. Isso sacrifica mensagem e stack trace. Avalie se o contrato da aplicação tolera essa perda antes de adotá-lo.

O exemplo também não sanitiza resource attributes nem valida um event.name previamente existente. Para produção, proponho uma lista de valores permitidos e testes cobrindo todos os campos exportados, não apenas o corpo.

Erros: disponibilidade não equivale a proteção

ignore registra o erro e continua; silent continua sem registrá-lo; propagate devolve o erro ao pipeline e pode provocar descarte do payload. As instruções seguem a ordem configurada. Erros de sintaxe continuam exigindo correção da configuração. (github.com)

Escolha explicitamente a política. Para dados sensíveis, não considere ignore uma garantia de bloqueio: se a transformação obrigatória falhar, é necessário ter uma saída segura, testada, que não exporte o conteúdo proibido.

Validação e teste de integração

Primeiro valide com a mesma imagem usada no deploy. O Collector oferece o subcomando validate; isso verifica configuração, mas não prova o resultado sobre todos os formatos de dados. (opentelemetry.io)

docker run --rm \
  -v "$PWD/collector.yaml:/etc/otelcol-contrib/config.yaml:ro" \
  otel/opentelemetry-collector-contrib:0.164.0 \
  validate --config=/etc/otelcol-contrib/config.yaml

docker run --rm --name ottl-lab \
  -p 127.0.0.1:4318:4318 \
  -v "$PWD/collector.yaml:/etc/otelcol-contrib/config.yaml:ro" \
  otel/opentelemetry-collector-contrib:0.164.0 \
  --config=/etc/otelcol-contrib/config.yaml

Em outro terminal, envie um registro sintético usando o envelope JSON de OTLP/HTTP e o caminho /v1/logs. O timestamp é gerado no envio. (opentelemetry.io)

NOW_NS="$(date +%s)000000000"
curl --fail-with-body \
  -H 'Content-Type: application/json' \
  http://127.0.0.1:4318/v1/logs \
  --data-binary @- <<EOF
{
  "resourceLogs": [{
    "resource": {
      "attributes": [{
        "key": "service.name",
        "value": {"stringValue": "checkout-api"}
      }]
    },
    "scopeLogs": [{
      "scope": {"name": "ottl-lab"},
      "logRecords": [{
        "timeUnixNano": "$NOW_NS",
        "body": {
          "stringValue": "{\"event\":\"checkout.failed\",\"level\":\"ERROR\",\"email\":\"synthetic@example.invalid\"}"
        }
      }]
    }]
  }]
}
EOF

O resultado esperado é corpo substituído, event.name=checkout.failed e severidade ERROR. Confirme no debug exporter, que permite inspecionar dados processados. Este guia fornece um procedimento reproduzível, não afirma que esses comandos foram executados no seu ambiente. (opentelemetry.io)

Para a integração contínua, proponho estas fixtures:

EntradaResultado esperado
Span de servidor com http.route=/orders/{id}Nome igual à rota
Span sem rotaNome preservado
Métrica checkout.orders.countDescrição e dimensão adicionadas; valor preservado
Outra métricaSem alteração por essas regras
Log JSON conhecidoEvento e severidade promovidos
Log malformado ou body não textualCorpo substituído; nenhum dado bruto exportado

Compare a estrutura OTLP normalizada, ignorando campos variáveis como timestamps. Inclua múltiplos registros no mesmo resource e testes negativos de vazamento.

Kubernetes: implantação completa para homologação

Crie o namespace e o ConfigMap a partir do arquivo anterior:

kubectl create namespace observability
kubectl create configmap otel-config-v1 \
  --from-file=collector.yaml \
  -n observability

Salve o manifesto abaixo como collector-k8s.yaml. Ele depende do ConfigMap criado acima e não requer Operator ou CRD, isto é, definição de recurso customizado.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: otel-gateway
  namespace: observability
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-gateway
  namespace: observability
spec:
  replicas: 2
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: otel-gateway
  template:
    metadata:
      labels:
        app: otel-gateway
    spec:
      serviceAccountName: otel-gateway
      automountServiceAccountToken: false
      terminationGracePeriodSeconds: 60
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: otel-gateway
      containers:
        - name: collector
          image: otel/opentelemetry-collector-contrib:0.164.0
          args:
            - --config=/etc/otelcol-contrib/collector.yaml
          ports:
            - name: otlp-http
              containerPort: 4318
            - name: health
              containerPort: 13133
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 512Mi
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: [ALL]
          startupProbe:
            httpGet:
              path: /
              port: health
            periodSeconds: 5
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /
              port: health
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: health
            periodSeconds: 10
            failureThreshold: 3
          volumeMounts:
            - name: config
              mountPath: /etc/otelcol-contrib
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: otel-config-v1
---
apiVersion: v1
kind: Service
metadata:
  name: otel-gateway
  namespace: observability
spec:
  type: ClusterIP
  selector:
    app: otel-gateway
  ports:
    - name: otlp-http
      port: 4318
      targetPort: otlp-http
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: otel-gateway
  namespace: observability
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: otel-gateway

O Deployment define atualização gradual e recursos iniciais para teste; não são valores universais de capacity planning. O ConfigMap versionado faz parte do contrato de implantação. (kubernetes.io)

As probes consultam a extensão health_check. Startup controla a inicialização, readiness a elegibilidade para receber tráfego e liveness a recuperação por reinício. Esse endpoint não comprova entrega no backend. (github.com)

A distribuição por hostname é preferencial porque usa ScheduleAnyway; não garante separação. O PodDisruptionBudget limita interrupções voluntárias, mas não protege contra toda falha de nó ou zona. (kubernetes.io)

kubectl apply -f collector-k8s.yaml
kubectl rollout status deployment/otel-gateway -n observability
kubectl get pods,svc,pdb -n observability
kubectl logs -n observability -l app=otel-gateway \
  --all-containers=true --tail=100

Ainda não é produção: há duas réplicas, mas não há autenticação, TLS, NetworkPolicy, armazenamento durável ou exportação para um backend. A porta interna também não deve ser tratada como fronteira de confiança.

Produção: decisões e trade-offs

Segurança e isolamento

Minha recomendação é liberar o pipeline somente após configurar TLS, autenticação entre emissores e gateway, egress restrito e NetworkPolicy compatível com o CNI do cluster. CNI significa Container Network Interface. Credenciais devem vir de Secrets ou de integração com um gerenciador de segredos, nunca do repositório. A orientação oficial destaca proteção de endpoints e menor privilégio. (opentelemetry.io)

Neste desenho, não há consulta à API Kubernetes; por isso, não proponho permissões RBAC, ou Role-Based Access Control, ao ServiceAccount. Se você adicionar enriquecimento Kubernetes, revise separadamente as permissões necessárias.

Também recomendo retirar debug e testar o exporter real com certificados, rotação de credenciais e falhas de autenticação. Logs detalhados de OTTL podem incluir o contexto anterior às transformações, expondo justamente os dados que a regra pretende remover. (github.com)

Disponibilidade e recuperação

Para um serviço crítico, proponho três réplicas distribuídas por zonas, capacidade para perder uma delas e uma política explícita de autoscaling. Verifique a distribuição real das conexões; adicionar réplicas não resolve automaticamente todo desequilíbrio. Se houver tail sampling ou processamento com estado, a estratégia de roteamento precisa considerar esse estado. (opentelemetry.io)

Configure filas e retries no exporter. Fila em memória não sobrevive ao desaparecimento do processo; fila persistente exige armazenamento e planejamento de recuperação. Mesmo assim, fila cheia, expiração de retry e falhas de armazenamento continuam sendo cenários de perda. (opentelemetry.io)

Defina como requisitos separados: disponibilidade do gateway, atraso máximo aceitável e perda tolerada. Backup de configuração não recupera telemetria já descartada.

Observabilidade do próprio Collector

Acompanhe throughput recebido e exportado, falhas, CPU, memória, reinícios, ocupação de filas e atraso de entrega. Métricas úteis incluem otelcol_exporter_queue_size e otelcol_exporter_queue_capacity. Confirme os nomes e sufixos efetivamente expostos pela versão e pelo exporter utilizados. (opentelemetry.io)

Proponho dashboards por sinal e alertas para fila persistentemente crescente, ausência inesperada de exportação e erros de transformação. Acrescente um teste sintético ponta a ponta: um processo saudável não é evidência suficiente de que os dados chegaram íntegros.

Performance e FinOps

Use ensaios com tamanho e diversidade de payload representativos. Transformações avançadas podem aumentar o custo de processamento; não estime capacidade apenas por registros por segundo. (opentelemetry.io)

No orçamento, contabilize CPU, memória, réplicas ociosas de contingência, tráfego entre zonas e ingestão no backend. Minha proposta é comparar o custo total antes e depois das regras, sem presumir que menos bytes compensam qualquer consumo adicional. Para workloads críticos, avalie uma base de capacidade não interrompível antes de depender de Spot.

Desenvolvimento versus produção

AspectoDesenvolvimentoProdução proposta
DadosSintéticosContrato e classificação documentados
Exportaçãodebug temporárioBackend autenticado e criptografado
CapacidadePequena e manualTestes de carga, reserva e autoscaling
DisponibilidadeUma réplica pode bastarRéplicas e distribuição por falha
MudançasIteração localRevisão, canário e rollback
RecuperaçãoReinício manualFilas, runbooks e restauração ensaiada

Evite promover o YAML de laboratório sem revisar esses requisitos. Não aceite ausência de requests, probes, observabilidade ou plano de retorno como simplificação permanente.

Troubleshooting

Use a sequência sintoma → hipótese → investigação → correção. As ferramentas oficiais incluem logs internos, inspeção de componentes e exportação local controlada. (opentelemetry.io)

SintomaPossível causaInvestigaçãoCorreção proposta
Collector não iniciaFunção, caminho ou YAML inválidovalidate e logs de inicializaçãoCorrigir regra e validar com a mesma imagem
Regra não altera dadosCampo em contexto diferente ou condição falsaFixture mínima e inspeção do payloadAjustar caminho e condição
Erros de parsingJSON inválido ou tipo inesperadoFixture do formato recebidoTratar tipos e definir fallback
Séries se tornam inconsistentesAlteração de dimensõesComparar identidade antes/depoisReverter e redesenhar agregação
Fila cresceBackend lento ou indisponívelFila, rede e erros de exportaçãoCorrigir gargalo antes de escalar
Pod reiniciaMemória, throttling ou probeEventos, estado anterior e recursosAjustar carga, recursos ou health checks
kubectl get pods -n observability
kubectl describe deployment otel-gateway -n observability
kubectl get events -n observability \
  --sort-by=.metadata.creationTimestamp
kubectl logs -n observability <pod> --previous
kubectl describe pod -n observability <pod>

Substitua <pod> pelo nome real. Em incidentes com dados sensíveis, reproduza o problema com uma fixture sanitizada antes de elevar o nível de logs.

CI/CD, upgrades e rollback

Proponho versionar imagem, configuração, fixtures e resultados esperados como uma unidade. Na integração e entrega contínuas, valide a configuração, execute testes dos três sinais e compare consumo de recursos antes de promover um canário.

Para cada mudança, crie um novo ConfigMap, como otel-config-v2, e altere a referência no Pod template. Preserve o anterior. rollout undo recupera a revisão do Deployment, não o conteúdo histórico de um ConfigMap sobrescrito. (kubernetes.io)

kubectl rollout history deployment/otel-gateway -n observability
kubectl rollout undo deployment/otel-gateway -n observability
kubectl rollout status deployment/otel-gateway -n observability

Em GitOps, reverta também o commit desejado para evitar reconciliação de volta à configuração defeituosa. Defina como gatilhos de rollback: vazamento detectado, alteração semântica inesperada, perda acima do orçamento ou saturação sustentada.

OTTL v1 não dispensa revisão dos changelogs do Collector e de seus componentes. A versão 0.164.0, por exemplo, contém remoções de feature gates e outras mudanças além da estabilização da linguagem. (github.com)

Quando usar e quais alternativas considerar

Recomendo OTTL quando regras condicionais precisam ser compartilhadas por várias equipes e há capacidade de manter contratos e testes. Não o escolheria para esconder indefinidamente uma instrumentação incorreta ou para implementar lógica de negócio.

Para inserções e exclusões simples, attributes ou resource podem ser mais legíveis. Para descarte, considere filter, cuja finalidade é diferente de transformar. metricstransform oferece operações específicas sobre métricas. (opentelemetry.io)

Minha ordem de avaliação é: corrigir a origem quando possível; escolher o processor mais simples que resolva o problema; usar OTTL para necessidades condicionais reais. Transformação centralizada facilita governança, mas concentra responsabilidade operacional no pipeline.

Resumo prático

  • Fixe uma distribuição e versão conhecidas.
  • Escolha contexto e tipos antes de escrever a condição.
  • Trate traces, métricas e logs com contratos separados.
  • Não confunda remoção de atributos com agregação correta.
  • Teste dados inválidos, ausência de campos e vazamentos.
  • Planeje observabilidade, capacidade, segurança e rollback junto com a regra.

Conclusão

A adoção madura de OTTL não termina no statement que funciona. Ela inclui proprietário da regra, contrato de entrada, saída verificável e procedimento de recuperação.

Para DevOps, isso significa transformação versionada e automatizada. Para SRE, significa proteger a confiabilidade do pipeline sem comprometer o significado dos sinais. Para Platform Engineering, significa oferecer padrões reutilizáveis sem retirar das equipes a responsabilidade pela qualidade da instrumentação. O resultado desejado é uma plataforma de observabilidade previsível, não apenas um Collector com mais regras.

Quer aplicar isso no seu ambiente?

Agende uma conversa de 30 minutos sem compromisso. Eu olho o seu cenário e digo por onde começar.

Agendar uma conversa

Receba os novos artigos por e-mail

Um e-mail quando sair artigo novo sobre infraestrutura, backend e IA. Sem spam, cancele quando quiser.