Observabilidade no NKPParte 3: OpenTelemetry Operator

Publicado
Leitura
11 min

Testado emNKP 2.19OpenTelemetry Operator 0.114.1Jaeger 2.57.4Istio 1.25.0Lab em Em outra versão, confira a matriz de compatibilidade e as release notes antes de aplicar.

Read in English

Capa da série Observabilidade no NKP, com a parte 3, OpenTelemetry, em destaque

Resumo: o OpenTelemetry Operator, do catálogo do NKP, instrumentou uma loja em Python com uma anotação no deployment, sem tocar no código. Em minutos havia traces no Jaeger, métricas no Prometheus e logs com trace_id. No Grafana, um clique no trace com erro abriu os logs daquela mesma compra no Loki da Parte 1. No caminho, quatro pontos de atenção que viraram checklist: o Jaeger do catálogo vem com o Istio como dependência, o exportador Prometheus está na distribuição contrib do Collector, os traces ficam mais confortáveis de analisar no Grafana e o Prometheus do NKP seleciona os ServiceMonitors por um rótulo específico. Esta é a parte 3, e última, da série.

Onde paramos

Nas partes anteriores, ligamos os logs no cluster de workload e o NKP Insights nos dois clusters. Tudo isso olha para a infraestrutura. Falta a aplicação: por que uma compra demorou, em qual serviço ela falhou, qual log explica o erro.

ParteTema
1Os conceitos e o que o NKP já entrega
2NKP Insights: o cluster apontando os próprios pontos de atenção
3OpenTelemetry Operator: traces, métricas e logs de uma aplicação (este post)

Os conceitos em cinco minutos

O OpenTelemetry padroniza como uma aplicação gera métricas, logs e traces. O Collector é o “carteiro”: recebe os dados, processa e entrega para um ou mais destinos. Ele é montado em três partes:

PartePapelNo lab
ReceiversPor onde os dados entramOTLP, nas portas 4317 (gRPC) e 4318 (HTTP)
ProcessorsO que fazer no meio do caminhoLimite de memória e envio em lotes
ExportersPara onde vãoTraces para o Jaeger, métricas para o Prometheus

O OpenTelemetry Operator leva isso para o Kubernetes com recursos próprios:

RecursoPara quê
OpenTelemetryCollectorCria e mantém Collectors, em um de quatro modos
InstrumentationA “receita” de instrumentação automática: para onde mandar, amostragem, linguagem
TargetAllocator e OpAMPBridgeDistribuir alvos de coleta e gerenciar Collectors remotamente

Os modos do Collector:

ModoQuando usar
DeploymentUm ponto central que recebe de todas as aplicações. O que usamos.
DaemonSetUm por nó, para coletar dados locais (logs de nó, métricas do host)
SidecarUm por pod, junto da aplicação
StatefulSetQuando o Collector precisa de identidade fixa, como no Target Allocator

A instrumentação automática por anotação existe para Java, Python, Node.js, .NET e Go. Nas quatro primeiras, o operador injeta as bibliotecas no pod; no Go, o mecanismo é outro.

O que precisou ser habilitado

Objetivo: ter o operador, um destino para os traces e um Collector no cluster de workload.

OpenTelemetry Operator

No catálogo, ele é Nutanix Supported, mas não faz parte da plataforma central. Por isso a tela de Enable tem um seletor de versão (havia 0.102.0 e 0.114.1), que os apps da plataforma não têm.

Página do OpenTelemetry Operator no catálogo: versão 0.114.1, Nutanix Supported e os pré-requisitos

Pré-requisito declaradoNo lab
Kubernetes 1.34 ou superior1.36.2
cert-manager 1.19.3 ou superior1.20.3, já instalado
kube-prometheus-stack 82.13.6 ou superiorjá instalado

Jaeger, com o Istio como dependência

O Jaeger 2.57.4 está no catálogo, na categoria Service Mesh. Ao habilitar, o NKP avisou que faltava uma dependência: o Istio-Helm Service Mesh.

Aviso de dependência ao habilitar o Jaeger: falta o Istio-Helm Service Mesh no cluster

Para o OpenTelemetry, o Jaeger não precisa do Istio; a dependência existe porque o NKP posiciona o Jaeger para mostrar os traces do service mesh. Desta vez, segui o aviso de dependências, como recomendei na Parte 1. Mas o Istio instala o istio-cni em todos os nós, e o cluster usa o Cilium como rede. Antes de seguir, conferi se os dois conviviam:

Terminal window
$ kubectl -n kube-system get cm cilium-config -o jsonpath='{.data.cni-exclusive}'
false

false quer dizer que o Cilium aceita outro plugin de rede encadeado. Em cerca de seis minutos estava tudo no ar, com todos os pods de sistema saudáveis:

ComponenteO que subiu
Istio 1.25.0istiod (2), gateway (2), istio-cni (6, um por nó); o ztunnel ficou sem pods
JaegerJaeger Operator e uma instância já rodando, no namespace istio-system
OpenTelemetry OperatorOperador no namespace opentelemetry e 5 CRDs novos

E um ponto positivo: o Kommander já publica a tela do Jaeger em https://lab.exemplo.local/dkp/jaeger/, atrás do mesmo login do NKP.

Boa prática: escolha o backend de traces pelo objetivo

O Istio mexe na rede dos nós, e no lab conviveu sem conflito com o Cilium. Se o objetivo for só traces de aplicação, avalie também instalar um backend de traces direto, sem o service mesh; se o service mesh já está nos planos, o Jaeger do catálogo entrega os dois juntos.

O Collector

O Collector recebe OTLP e entrega os traces ao Jaeger e as métricas a um endpoint que o Prometheus coleta:

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel
namespace: observability
spec:
mode: deployment
# a imagem padrão (otelcol-k8s) não tem o exportador prometheus
image: ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.152.1
config:
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: 80, spike_limit_percentage: 25}
batch: {}
exporters:
otlp_grpc/jaeger:
endpoint: jaeger-jaeger-operator-jaeger-collector.istio-system.svc.cluster.local:4317
tls: {insecure: true}
prometheus:
endpoint: 0.0.0.0:8889
resource_to_telemetry_conversion: {enabled: true}
service:
pipelines:
traces: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp_grpc/jaeger]}
metrics: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [prometheus]}

A minha primeira versão não tinha a linha image, e o pod entrou em crashloop, com uma mensagem que já indica o ajuste:

Error: failed to get config: ... 'exporters' unknown type: "prometheus" for id: "prometheus"
(valid values: [nop otlp_grpc otlp otlphttp loadbalancing debug otlp_http file otelarrow])

O operador usa por padrão a distribuição otelcol-k8s, mais enxuta, com OTLP e os componentes essenciais; o exportador Prometheus fica na contrib. A própria mensagem lista o que existe. Com a imagem da distribuição contrib, o Collector subiu na hora. Um detalhe que vale conhecer: o NKP Insights da Parte 2 registrou esse crashloop cerca de dois minutos depois, antes de eu investigar.

Confira a distribuição antes de montar a configuração

A otelcol-k8s é leve e cobre OTLP, mas muitos receivers e exporters só existem na contrib. Escolha a imagem em função dos componentes que a configuração usa.

A aplicação de exemplo

Uma loja com dois serviços em Python (Flask): a loja recebe a compra e consulta o estoque, que falha de propósito em 10% das vezes. Um terceiro pod, o cliente, faz uma compra a cada dois segundos. Este é o código da loja, sem nenhuma linha de OpenTelemetry:

import logging, random, os, requests
from flask import Flask, jsonify
app = Flask(__name__)
log = logging.getLogger("loja")
BACKEND = os.environ.get("BACKEND", "http://estoque:8080")
ITENS = ["teclado", "monitor", "mouse", "headset"]
@app.route("/comprar")
def comprar():
item = random.choice(ITENS)
r = requests.get(f"{BACKEND}/estoque/{item}", timeout=2)
if r.status_code != 200:
log.warning("compra de %s falhou: estoque respondeu %s", item, r.status_code)
return jsonify(item=item, status="indisponivel"), 502
log.info("compra de %s ok (%s em estoque)", item, r.json()["quantidade"])
return jsonify(item=item, status="ok")

Sem instrumentação, o log da loja era só a linha de acesso do servidor web:

198.51.100.36 - - [26/Oct/2026 15:27:26] "GET /comprar HTTP/1.1" 200 -

Instrumentar sem tocar no código

São duas ações. A primeira é a receita, um recurso Instrumentation no namespace da aplicação:

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: auto
namespace: demo
spec:
exporter:
endpoint: http://otel-collector.observability.svc.cluster.local:4318
propagators: [tracecontext, baggage]
sampler:
type: parentbased_traceidratio
argument: "1"
python:
env:
- {name: OTEL_PYTHON_LOG_CORRELATION, value: "true"}

A segunda é uma anotação no template do deployment:

Terminal window
kubectl -n demo patch deploy loja --type merge -p \
'{"spec":{"template":{"metadata":{"annotations":{"instrumentation.opentelemetry.io/inject-python":"true"}}}}}'

O pod foi recriado e o operador fez o resto sozinho:

O que o operador injetouExemplo
Um init containeropentelemetry-auto-instrumentation-python (imagem autoinstrumentation-python:0.63b1), que copia as bibliotecas para o pod
PYTHONPATH estendidoMantendo o caminho que a aplicação já usava
Cerca de 15 variáveis OTEL_*OTEL_SERVICE_NAME=loja (tirado do nome do deployment), endpoint, protocolo, amostragem
Atributos do Kubernetesnamespace, deployment, ReplicaSet, pod e nó

E o log da loja mudou:

INFO [loja] [loja.py:14] [trace_id=a2f2d6259a11b0e7ef0bf91290b751ac span_id=efdcaa454b4a1f8f
resource.service.name=loja trace_sampled=True] - compra de mouse ok (6 em estoque)

Cada mensagem agora carrega o trace_id da compra. As linhas de acesso do servidor web saem com trace_id=0, porque são escritas depois que a requisição termina.

A versão que o operador deduz

O atributo service.version ficou 3.12-slim, a tag da imagem do Python. O operador usa a tag da imagem como versão; numa aplicação real, essa tag costuma ser a versão do app.

Os traces

No Jaeger, cada compra virou um trace com três spans: a requisição na loja, a chamada HTTP de saída e a requisição no estoque. O mapa de dependências foi desenhado sozinho, a partir dos traces:

Jaeger System Architecture: loja chamando estoque, 3089 chamadas

Para analisar os traces, preferi o Grafana Logging, que já tem o Loki e deixa trace e log lado a lado. A interface do Jaeger desta versão tem pouco contraste (texto claro sobre fundo claro desde o primeiro login, nos modos claro e escuro), e o Grafana resolveu isso também. Adicionei o Jaeger como fonte de dados, apontando para o serviço interno de consulta:

http://jaeger-jaeger-operator-jaeger-query.istio-system.svc.cluster.local:16686/dkp/jaeger

No Grafana, o mesmo trace com erro fica bem mais legível:

Trace com erro no Grafana: loja, a chamada HTTP e o estoque, os três marcados com erro

E o span do estoque mostra a falha completa, sem nenhuma linha de instrumentação no código:

Atributos do span do estoque: error true, http.status_code 503, http.target /estoque/mouse e os atributos do Kubernetes

Do trace ao log, em um clique

Na fonte de dados do Jaeger, a seção Trace to logs liga os traces ao Loki:

CampoValor
Data sourceLoki
Span start / end time shift-1m / 1m
Tagsk8s.namespace.name as namespace
Filter by trace IDligado

O mapeamento da tag é o detalhe que faz funcionar: no trace o atributo se chama k8s.namespace.name; no Loki, o rótulo se chama namespace.

Com isso, o span ganha o botão Logs for this span. Um clique, e o Grafana abre o Loki ao lado, filtrado pelo trace_id:

Grafana com o trace à esquerda e, à direita, as duas linhas de log do mesmo trace_id: o ERROR do estoque e o WARN da loja

Duas linhas, a história inteira de um erro:

ERROR [estoque] [trace_id=f5ef3278...] - falha ao consultar estoque de mouse
WARN [loja] [trace_id=f5ef3278...] - compra de mouse falhou: estoque respondeu 503

O cliente pediu um mouse, a loja consultou o estoque, o estoque falhou e a loja registrou. Os logs vieram pela pilha da Parte 1 (Fluent Bit, Fluentd e Loki); o trace, pela pilha desta parte. O trace_id liga os dois.

As métricas

Faltava o terceiro pilar. O Collector publica as métricas na porta 8889, e um ServiceMonitor diz ao Prometheus para coletá-las. Para o Prometheus do NKP coletar, falta um detalhe de configuração:

Terminal window
$ kubectl get prometheus -A -o jsonpath='{.items[*].spec.serviceMonitorSelector}'
{"matchLabels":{"prometheus.kommander.d2iq.io/select":"true"}}

O Prometheus do NKP seleciona os ServiceMonitors pelo rótulo prometheus.kommander.d2iq.io/select: "true", o que deixa explícito o que ele coleta. Os exemplos da internet usam release: kube-prometheus-stack, que aqui não é selecionado: sem o rótulo do NKP, a consulta volta vazia e sem mensagem de erro. Vale colocar o rótulo desde o primeiro manifesto.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: otel-prom
namespace: observability
labels: {prometheus.kommander.d2iq.io/select: "true"}
spec:
selector: {matchLabels: {app: otel-prom}}
endpoints:
- {port: prom, interval: 30s}

Com o rótulo certo, as métricas apareceram em cerca de um minuto:

sum by (service_name, http_status_code) (rate(http_server_duration_milliseconds_count{service_name=~"loja|estoque"}[5m]))

Requisições por segundo por serviço e código HTTP: estoque com 200 e 503, loja com 200 e 502

MétricaValor
Estoque, respostas 2000,44 por segundo
Estoque, respostas 5030,056 por segundo, cerca de 11%: a falha proposital
Latência p95 do estoquecerca de 240 ms
Latência p95 da lojacerca de 245 ms, porque inclui a chamada ao estoque

É o método RED da Parte 1 (taxa, erros e duração) saindo da aplicação sem código novo.

A mesma falha, por todos os ângulos

PilarO que mostrou
Métricas11% de respostas 503 no estoque
TracesO span do estoque com erro, 503 e /estoque/mouse
Logsfalha ao consultar estoque de mouse, aberto direto do trace
NKP InsightsO crashloop do Collector, detectado sozinho

O custo de tudo isso

No início da série, a CPU reservada (requests) do cluster de workload era de 27,1%. Depois de Ceph, logs, Insights, Istio, Jaeger e OpenTelemetry, chegou a 72,5%, com os limites de CPU em 95%. É um cluster de laboratório, com workers de 8 vCPUs, mas o recado vale para qualquer ambiente: observabilidade entra no dimensionamento desde o começo.


O que aprendi

  1. A instrumentação automática funciona de verdade: uma anotação, nenhuma linha de código, e a aplicação passa a emitir traces, métricas e logs correlacionados.
  2. O Jaeger do catálogo vem com o Istio como dependência. Confira o encadeamento com o Cilium (cni-exclusive=false) e escolha o backend de traces pelo objetivo.
  3. Escolha a distribuição do Collector pelos componentes da configuração. A padrão é enxuta; o exportador Prometheus e muitos outros estão na contrib.
  4. O Prometheus do NKP seleciona os ServiceMonitors pelo rótulo prometheus.kommander.d2iq.io/select: "true". Use-o desde o primeiro manifesto.
  5. O valor está na correlação: trace_id no log e “Trace to logs” no Grafana transformam três ferramentas em uma investigação só.
  6. Planeje os recursos desde o começo: a CPU reservada do cluster foi de 27,1% para 72,5% ao longo da série, quase o triplo.

Checklist do OpenTelemetry no NKP

  • cert-manager e kube-prometheus-stack nas versões exigidas
  • OpenTelemetry Operator habilitado, com a versão escolhida
  • Destino de traces definido (Jaeger do catálogo com Istio, ou outro backend)
  • Collector com a distribuição que tem os componentes da configuração
  • ServiceMonitor com o rótulo prometheus.kommander.d2iq.io/select: "true"
  • Instrumentation por namespace, com amostragem definida
  • Anotação de instrumentação nos deployments
  • Correlação de logs ligada e “Trace to logs” configurado no Grafana
  • Amostragem abaixo de 100% em produção, para controlar volume e custo

O que ainda não testei

  • Um destino real no Alertmanager, para os alertas saírem do destino padrão "null".
  • Uma aplicação que chama o modelo de IA do lab, instrumentada com as convenções do OpenTelemetry para IA generativa (tokens, modelo e latência).
  • Amostragem de produção. No lab, a Instrumentation usou amostragem de 100% (argument: "1"); reduzir para um valor de produção fica para depois.

Fim da série

Em três partes, o lab saiu de “métricas no workload e alertas aguardando um destino” para logs, achados de segurança, traces e métricas de aplicação ligados entre si. Ficaram para depois: configurar um destino real no Alertmanager, instrumentar uma aplicação que chama o modelo de IA do lab com as convenções do OpenTelemetry para IA generativa (tokens, modelo e latência) e reduzir a amostragem para um valor de produção.

Obrigado por acompanhar

Se você está montando observabilidade no NKP e quer trocar ideia, me chame no LinkedIn.

Referências

Nutanix

  • Nutanix Kubernetes Platform Guide 2.19 (portal Nutanix)

Projetos e comunidade

Comentários

Os comentários usam o GitHub Discussions; é preciso ter uma conta no GitHub.