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.cachepermite 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 observabilidadeOTLP é 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.yamlEm 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\"}"
}
}]
}]
}]
}
EOFO 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:
| Entrada | Resultado esperado |
|---|---|
Span de servidor com http.route=/orders/{id} | Nome igual à rota |
| Span sem rota | Nome preservado |
Métrica checkout.orders.count | Descrição e dimensão adicionadas; valor preservado |
| Outra métrica | Sem alteração por essas regras |
| Log JSON conhecido | Evento e severidade promovidos |
| Log malformado ou body não textual | Corpo 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 observabilitySalve 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-gatewayO 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=100Ainda 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
| Aspecto | Desenvolvimento | Produção proposta |
|---|---|---|
| Dados | Sintéticos | Contrato e classificação documentados |
| Exportação | debug temporário | Backend autenticado e criptografado |
| Capacidade | Pequena e manual | Testes de carga, reserva e autoscaling |
| Disponibilidade | Uma réplica pode bastar | Réplicas e distribuição por falha |
| Mudanças | Iteração local | Revisão, canário e rollback |
| Recuperação | Reinício manual | Filas, 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)
| Sintoma | Possível causa | Investigação | Correção proposta |
|---|---|---|---|
| Collector não inicia | Função, caminho ou YAML inválido | validate e logs de inicialização | Corrigir regra e validar com a mesma imagem |
| Regra não altera dados | Campo em contexto diferente ou condição falsa | Fixture mínima e inspeção do payload | Ajustar caminho e condição |
| Erros de parsing | JSON inválido ou tipo inesperado | Fixture do formato recebido | Tratar tipos e definir fallback |
| Séries se tornam inconsistentes | Alteração de dimensões | Comparar identidade antes/depois | Reverter e redesenhar agregação |
| Fila cresce | Backend lento ou indisponível | Fila, rede e erros de exportação | Corrigir gargalo antes de escalar |
| Pod reinicia | Memória, throttling ou probe | Eventos, estado anterior e recursos | Ajustar 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 observabilityEm 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.
Artigos relacionados
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 conversaReceba os novos artigos por e-mail
Um e-mail quando sair artigo novo sobre infraestrutura, backend e IA. Sem spam, cancele quando quiser.