RFC 0004 — Seguir as convenções, para chegar aos Four Golden Signals
| Campo | Valor |
|---|---|
| Status | Proposed, em execução |
| Escrita | 15/10/2026 |
| Última revisão | 16/10/2026, com o recorte de escopo e cinco decisões. Ver Linha do tempo |
| Objetivo | Classificar cada workload pelo que ele é, usando as convenções do OpenTelemetry e do Kubernetes, e com isso viabilizar painéis de Four Golden Signals |
| Escopo | As 12 aplicações do namespace apps e os addons de plataforma. O jogo fica fora, ver Escopo |
| Afeta | homelab (otel-collector, platform chart, chart kelbi-app), kelbi-applications (values das 12 aplicações), contabo-monitoring-server (regras, painéis) |
| Depende de | RFC 0003, itens 3, 4 e 5, e a ação A9 do postmortem de 14/10 |
| Relacionada a | ADR 0001 — Métricas k3s via OTel |
Contexto
O objetivo final é um painel de Four Golden Signals: latência, tráfego, erros e saturação. O obstáculo é que hoje nenhuma métrica diz o que cada workload é, então não há como montar a lista de coisas a exibir nem decidir quais sinais se aplicam a cada uma.
A RFC 0003 resolveu o problema anterior, de métrica anônima: toda série tem job e instance. Mas o job responde quem emitiu a métrica, e os 51 valores que existem hoje misturam tudo sem nenhuma marca que separe:
apps/prod-kelbi-api produto HTTP externo
apps/prod-mirim-bot worker, sem HTTP
kelbi/kelbi-channel-1-1 jogo (um de seis)
kube-system/svclb-kelbi-channel-1-1-a3195d97 plataforma
kelbi-pg/kelbi-pg-root-app-kelbi-pg-dev banco
traefik/traefik plataforma
Esta RFC passou por três versões no mesmo dia, e as duas primeiras erraram do mesmo jeito: propuseram inventar labels (app, depois app e role) para informação que as convenções do OpenTelemetry e do Kubernetes já nomeiam. A terceira versão abandona a invenção.
Duas outras coisas mudaram de lugar ao medir.
A primeira é que não existe um painel de Four Golden Signals, existem vários, porque os quatro sinais têm fontes diferentes conforme o tipo de workload. Latência de um CDN é duração de request HTTP; latência de um cronjob é duração da execução; latência de um servidor de jogo é tempo de operação sobre uma conexão TCP aberta. Classificar não é só filtrar o painel, é escolher quais métricas o painel lê.
A segunda é que o painel não é construível hoje para quase nada, e a classificação é o menor dos obstáculos.
Escopo
Por decisão de 16/10/2026, esta RFC cobre as 12 aplicações do namespace apps e os addons de plataforma. Medido no mesmo dia:
| Entra | Pods | Workloads |
|---|---|---|
apps | 13 | 12 Deployments, um deles com duas réplicas |
argocd | 7 | |
external-secrets | 3 | |
cloudflare-tunnel | 2 | |
traefik, velero, monitoring, reloader | 4 | um pod cada |
| Total | 29 |
O jogo fica fora: os namespaces kelbi (9 pods, os seis canais mais entrance, sign e launcher) e kelbi-pg (2 pods, o Postgres e o exporter). Os dois kelbi-845050* existem e estão vazios.
Também fica fora o que o k3s traz por conta própria no kube-system: coredns, local-path-provisioner e os 8 svclb-*, que só existem por causa dos Service do jogo.
O recorte é por namespace, e três workloads caem fora por ele sem que o conteúdo peça isso:
metrics-server, que é addon governado pelokelbi-platforme já carregacomponent=addon,part-of=platformeservice.criticality=medium. Mora nokube-systemporque é lá que o chart o põe, não por não ser nosso. Eu o mantive no escopo, contado nos nove addons, porque excluir um addon já classificado seria arbitrário.homepage(1 pod,home.arcamh.com) e os doispatch-server(viaIngressRoute), que são aplicações HTTP externas com hostname público e namespace próprio. Pela letra da regra saem, e com eles ohttp-externalcai de 10 para 7.
Os três são decisão de uma linha, e estão listados aqui para não saírem por omissão.
O que o recorte muda
Some a única classe que tinha os quatro sinais completos. O Postgres era o caso que o postgres-exporter cobria inteiro, e ele está no kelbi-pg. Dentro do novo escopo, nenhuma classe tem os quatro quadrantes hoje.
E fecha o item 1, que já havia perdido uma das duas motivações. A outra eram os seis canais, que agora estão fora. Para os 29 pods que restam, o nome do Deployment é um service.name aceitável.
Linha do tempo
Quase tudo abaixo aconteceu em 15/10/2026, e três versões desta RFC foram escritas no meio. O registro existe porque as correções de rota importam mais que o texto final.
| Hora (UTC) | O que aconteceu |
|---|---|
| ~17h | Primeira versão, propondo um label app inventado |
| ~18h | Segunda versão. Objetivo real esclarecido: separar plataforma de aplicação para chegar aos Four Golden Signals. Taxonomia de sete classes, e dois labels inventados, app e role |
| ~19h | Medição dos quatro sinais contra o que existe. O painel não é construível hoje para quase nada: saturação é zero para todas as classes, e 14 dos 26 workloads não-plataforma não expõem /metrics |
| ~20h | Terceira versão. As convenções do OpenTelemetry e do Kubernetes já nomeiam tudo o que eu estava inventando: service.name, deployment.environment.name, app.kubernetes.io/component. A invenção sai |
| ~21h | kelbi-app 5.9.0 e 5.9.1: o chart declara a identidade OTel no pod. service.criticality entra, fora do escopo original |
| ~01h | Experimento: removi o transform/service_identity e as duas flags de extração, para medir o que cada um sustentava. Resultado em item 1 e item 4b |
| ~01h30 | kelbi-platform 1.15.0 e 1.16.0: identidade de host via host.name, allowlist de extração, identidade para objeto que não é pod, e descarte de k8s.replicaset.* |
| ~02h | kelbi-platform 1.17.0 e 1.18.0: component=addon e part-of=platform nos addons, e service.criticality em todos os nove |
| 16/10, manhã | Decisão sobre o significado do component: ele carrega a peça arquitetural. A classe de observabilidade deixa de ser label e passa a se deduzir da peça. O inventário do dia mostra que os 8 workloads tratados como conflito já seguiam essa leitura |
| 16/10, manhã | Quatro decisões que estavam abertas: prod-kelbi-jobs é singleton, o kelbi-launcherserver é web por enquanto, service.name pode incluir o ambiente, e o item 9 sai desta RFC |
| 16/10, manhã | Recorte de escopo: o jogo sai. A RFC passa a cobrir as 12 aplicações de apps e os addons de plataforma, 29 pods em vez de 52. Fecha o item 1, e com ele a única classe que tinha os quatro sinais completos |
| 16/10, tarde | Os nomes dos labels decididos e aplicados: component, part_of e environment (kelbi-platform 1.19.0 e 1.20.0). No caminho, a allowlist tinha renomeado quatro labels em silêncio desde a 1.16.0, e a extração crua de annotation duplicava tudo o que o otel_annotations já entregava |
| 16/10, tarde | part-of carrega o produto, e com isso a seção de decisões abertas fica vazia. O kelbi-app 5.10.0 faz component cair em type e remove a annotation morta de service.name. As 12 aplicações passam a declarar peça e produto |
| 16/10, tarde | Correção: o part-of=argocd é seletor que o próprio ArgoCD usa para achar a configuração dele, medido em 11 objetos do namespace. Deixa de ser "fork que não se paga" e passa a ser exceção obrigatória, e com isso o ArgoCD fica sem label que o identifique como plataforma |
Duas coisas mudaram o documento mais que o resto.
O experimento provou que a cadeia OTTL de 14 statements era redundante para pods e a única fonte para o resto, o que nenhuma das três versões anteriores sabia. A cadeia voltou com dez statements, e três itens desta RFC tiveram a premissa corrigida.
A decisão de 16/10 desfez uma divergência que eu havia escolhido aceitar. As três versões anteriores propuseram esticar component para carregar a classe de observabilidade, e a terceira chegou a documentar isso como desvio consciente da convenção. Com a peça no lugar da classe, não há desvio: o label volta ao significado que a documentação do Kubernetes dá a ele, e o único valor fora do padrão no cluster passa a ser o component=kelbi-app que eu mesmo introduzi.
As convenções, e o que já funciona
O que o OpenTelemetry define
| Atributo | Papel | Label no Prometheus |
|---|---|---|
service.name | nome lógico do serviço | job, como <service.namespace>/<service.name> |
service.namespace | agrupamento do service.name | idem |
service.instance.id | instância única | instance |
service.version | versão | service_version |
deployment.environment.name | ambiente de deploy | environment, desde 16/10/2026 |
deployment.environment.name é o atributo estável que substituiu o deployment.environment deprecado. É ele que resolve os itens 4 e 5 da RFC 0003, que falavam de criar um label de ambiente: o label não precisa ser criado, precisa ser preenchido.
A annotation no pod é resource.opentelemetry.io/deployment.environment.name, e o label na métrica é environment, por um resource processor no collector (kelbi-platform 1.20.0). O mesmo vale para component e part_of, que vêm de app.kubernetes.io/* e chegam curtos por tag_name.
A regra que saiu daí: a convenção governa o que se declara no objeto do Kubernetes, e o nome do label é escolhido para quem escreve PromQL. Seguir a convenção na métrica também custaria app_kubernetes_io_component e deployment_environment_name em todo filtro de painel, sem ganho, porque o nome do label não é interface com ferramenta nenhuma de fora.
O custo está em correlação futura: deployment.environment.name é o atributo que ligaria métrica a log e trace. Hoje só existe métrica, e isso está em Fora do escopo. Reverter é apagar um processor.
O que o Kubernetes define
Os labels recomendados e a cobertura medida nos pods dos 52 workloads:
| Label | Papel na convenção | Cobertura |
|---|---|---|
app.kubernetes.io/name | nome da aplicação | 31/52 |
app.kubernetes.io/instance | instância única da aplicação | 31/52 |
app.kubernetes.io/version | versão | 26/52 |
app.kubernetes.io/managed-by | quem gerencia | 27/52 |
app.kubernetes.io/component | o componente dentro da arquitetura | 8/52 |
app.kubernetes.io/part-of | aplicação maior da qual faz parte | 8/52 |
O encaixe entre as duas já existe, e está meio feito
O k8sattributes deriva atributos service.* dos labels recomendados do Kubernetes, e o collector já pede isso. O metadata do processor, lido do ConfigMap em execução, inclui service.namespace, service.name, service.version e service.instance.id.
Medido em 15/10/2026, funciona para um e não para o outro:
| Workload | app.kubernetes.io/name | service.name resultante |
|---|---|---|
prod-halk-en-api | halk-en-api | prod-halk-en-api |
prod-kelbi-web | kelbi-web | prod-kelbi-web |
| Workload | app.kubernetes.io/version | service.version resultante |
|---|---|---|
prod-halk-en-api | 1.16.0 | 1.16.0 ✅ |
service.version vem do label recomendado. service.name não. A primeira versão desta seção culpava a cadeia OTTL da RFC 0003. Está errado, e o experimento de 15/10/2026 mostrou: removendo a cadeia inteira, service.name continuou prod-halk-en-api.
Quem o define é o próprio k8sattributes, pela lista extract.metadata, e ele resolve para o nome do Deployment. O resultado é que o job carrega o nome do Deployment, com o prefixo de ambiente colado nele, em vez do nome da aplicação. Ver item 1.
otel_annotations está ligado, e é independente das flags de extraçãoO ConfigMap em execução tem otel_annotations: true no k8sattributes. Uma annotation resource.opentelemetry.io/<atributo> num pod vira aquele atributo de recurso, sem configuração nenhuma.
Em 15/10/2026, 13 pods passaram a usá-la, via o chart kelbi-app 5.9.0. E o experimento do mesmo dia provou algo que não era óbvio: esse mecanismo sobrevive a desligar extractAllPodLabels e extractAllPodAnnotations. Com as duas flags em false, app_kubernetes_io_* e resource_opentelemetry_io_* desapareceram das métricas, mas deployment_environment_name e service_criticality continuaram chegando.
Esse experimento mediu a independência dos dois mecanismos, e em 16/10/2026 ficou claro que eles também eram redundantes: a extração explícita de resource.opentelemetry.io/* como label cru entregava uma segunda cópia de tudo o que o otel_annotations já entregava limpo. Medido antes de remover, com os pares batendo exatos:
deployment_environment_name 455
resource_opentelemetry_io_deployment_environment_name 455
service_criticality 2087
resource_opentelemetry_io_service_criticality 2087
resource_opentelemetry_io_service_name 455 sem par, a forma limpa nunca vence
resource_opentelemetry_io_service_version 455 sem par
A forma crua existia por uma razão registrada nesta RFC: permitir que o OTTL preferisse a annotation ao valor de extract.metadata, que era o caminho alternativo do item 1. O item fechou no mesmo dia, então a justificativa saiu antes da configuração. São quatro labels por série a menos.
A identidade pela convenção do OpenTelemetry se sustenta sozinha; a pela convenção do Kubernetes depende da extração estar configurada.
A taxonomia, em app.kubernetes.io/component
São duas perguntas diferentes, e a convenção do Kubernetes já tem um label para cada uma:
| Label | A pergunta que responde | Valores em uso no escopo |
|---|---|---|
app.kubernetes.io/component | que peça da arquitetura isto é | addon, web, api, cdn, singleton |
app.kubernetes.io/part-of | de que produto isto faz parte | platform, kelbi, halk |
component carrega a peça arquitetural, no sentido literal da convenção, cujos próprios exemplos são database, cache e server. O cloudflared é um addon. O prod-kelbi-web é um web. O prod-mirim-bot é um singleton.
A classe de observabilidade não é label: ela se deduz da peça. Um cdn é sempre HTTP externo, um primary é sempre banco, um addon é sempre plataforma. Guardar as duas coisas em labels separados duplicaria informação que já está implícita, e informação duplicada é informação que começa a divergir.
O conjunto de valores cresce por desenho
A peça é a unidade de produtização. Hoje o Postgres é a peça primary dentro do produto kelbi; quando o banco for oferecido como produto da plataforma, a peça continua a mesma e só o part-of muda. O mesmo caminho vale para storage e para cdn.
É por isso que o conjunto não é fechado como o de environment e o de criticality, que têm quatro valores cada e rejeitam o quinto no helm template. Cada peça nova é um produto futuro em potencial, e fechar o vocabulário agora fecharia a porta errada.
A versão anterior desta RFC propunha component para a classe de observabilidade (http-external, batch, game) e listava 8 workloads como conflito a resolver, por usarem o label com outro significado. O inventário de hoje mostra o contrário:
component | part-of | Origem |
|---|---|---|
server, repo-server, redis, dex-server, application-controller, applicationset-controller, notifications-controller | argocd | chart upstream do ArgoCD |
primary | nenhum | chart upstream do Postgres |
addon | platform | kelbi-platform 1.14.0 |
kelbi-app | nenhum | kelbi-app 5.9.0 |
Os sete do ArgoCD e o primary são peças da arquitetura, exatamente no sentido da convenção. O part-of=argocd tem um papel a mais, que é seletor do próprio ArgoCD, e está na nota de part-of carrega o produto. O único valor que responde "de onde isto veio" em vez de "o que isto é" é o kelbi-app, que eu mesmo coloquei em 15/10/2026.
Não existe conflito a resolver com chart de terceiro. Existe um chart meu a corrigir.
Da peça para os quatro sinais
O painel não lê a peça para desenhar o gráfico; lê a classe, que sai da peça por esta tabela:
component | Classe de sinal | Workloads no escopo |
|---|---|---|
web, api (público), cdn | HTTP externo | 7 |
api (interno) | HTTP interno | 3 |
singleton | worker contínuo | 2 |
addon | plataforma | 9 governados pelo kelbi-platform. Os 7 pods do ArgoCD são plataforma e não têm addon, por serem release Helm própria com peças próprias |
O api aparece em duas linhas, e é o único caso em que a peça não determina a classe sozinha: prod-kelbi-api tem rota pública e prod-halk-en-api não. A diferença está na existência de HTTPRoute, que é topologia e não nome, então aqui a classe se mede em vez de se declarar.
As classes jogo, banco e cronjob, que as versões anteriores desta RFC listavam, saíram com o recorte de escopo. Elas voltam quando o jogo entrar, e o vocabulário de peça as acomoda sem mudança: channel, entrance, sign e primary são peças como qualquer outra.
O inventário medido, por classe de sinal
HTTP externo (7): têm HTTPRoute com hostname público, medido em 16/10/2026:
apps/prod-kelbi-api launcher.arcamh.com
apps/prod-kelbi-web admin.arcamh.com 2 réplicas
apps/prod-kelbi-cdn cdn.arcamh.com
apps/prod-halk-en-release-cdn halk-en-release.arcamh.com
apps/prod-halk-en-pre-release-cdn halk-en-pre-release.arcamh.com
apps/prod-halk-jp-release-cdn halk-jp-release.arcamh.com
apps/prod-halk-jp-pre-release-cdn halk-jp-pre-release.arcamh.com
HTTP interno (3): ClusterIP sem rota, chamados por outros serviços:
apps/prod-halk-en-api 5000, 4000, 4001
apps/prod-halk-jp-api 5000, 4000, 4001
apps/prod-mhfrontier-cq-tool-api 5001
Worker contínuo (2): Deployment sem Service nenhum:
apps/prod-kelbi-jobs
apps/prod-mirim-bot
Plataforma (16 pods): ArgoCD (7), external-secrets (3), cloudflared (2), e um pod cada de Traefik, Velero, collector e reloader. Mais o metrics-server, com duas réplicas no kube-system, pelo motivo da nota de escopo.
Os quatro sinais, por classe
É a tabela que decide o escopo do trabalho. Cada célula diz se a fonte existe hoje.
| Classe | Latência | Tráfego | Erros | Saturação |
|---|---|---|---|---|
| HTTP externo (7) | Traefik ✅ | Traefik ✅ | Traefik ✅ | falta A9 |
| HTTP interno (3) | ✗ | ✗ | ✗ | falta A9 |
| Worker contínuo (2) | ✗ | ✗ | ✗ | falta A9 |
| Plataforma (16) | parcial | parcial | parcial | falta A9 |
Saturação não existe para ninguém
As quatro recording rules de utilização relativa a limit e request têm zero séries:
otel:container_cpu_limit_utilization:ratio ZERO
otel:container_memory_limit_utilization:ratio ZERO
otel:container_cpu_request_utilization:ratio ZERO
otel:container_memory_request_utilization:ratio ZERO
As métricas são opcionais no receiver kubeletstats e o chart habilita o preset sem configurá-las. É o mesmo achado que o postmortem de 14/10 registrou como causa de sete alertas mortos, e cuja correção é a ação A9 de lá.
O que existe é numerador e denominador separados, e só para parte dos containers: no cluster inteiro, 56 séries de k8s_pod_memory_usage_bytes e k8s_pod_cpu_usage contra 18 de k8s_container_memory_limit_bytes e 17 de k8s_container_cpu_limit. Dá para calcular por divisão para os containers com limite declarado; para os outros 38 não há teto, e saturação sem teto não é saturação. A9 é pré-requisito desta RFC.
Os três sinais de request só existem para quem passa pelo Traefik
traefik_service_requests_total e traefik_service_request_duration_seconds_bucket cobrem 12 services, dos quais 7 são o HTTP externo deste escopo. Os outros cinco são o argocd-server, o kelbi-launcherserver e os dois patchserver, todos fora do recorte, mais o homepage.
O argocd-server é o argumento central contra derivar a classificação da topologia: está atrás do Traefik, tem os três sinais de request, e é plataforma. Estar exposto não diz o que a coisa é.
E o que fica de fora é o resto do produto. Os 3 de HTTP interno e os 2 workers não declaram porta de container nenhuma, logo não expõem /metrics. Para 5 das 12 aplicações, três dos quatro sinais não têm fonte, e nenhum label resolve isso.
Para os prod-halk-*-api, o prod-mhfrontier-cq-tool-api, o prod-kelbi-jobs e o prod-mirim-bot não existe latência, tráfego nem taxa de erro. Existe CPU e memória em bytes. O tráfego deles é pod a pod e não passa pelo Traefik.
Esta RFC entrega a classificação, que destrava o painel das 7 aplicações de HTTP externo. Para as outras cinco, o painel só existe depois de a aplicação emitir métrica própria, que é esforço nos repos e saiu para RFC própria junto com o item 9.
O kelbi-pg era o único caso com os quatro quadrantes cobertos por séries reais, via postgres-exporter: latência em pg_stat_database_blk_read_time_total, tráfego em pg_stat_database_xact_commit_total, erros em pg_stat_database_xact_rollback_total e deadlocks, saturação em pg_stat_activity_count sobre pg_settings_max_connections.
Era o painel mais barato de construir e o primeiro candidato natural, porque não exigia trabalho de coleta nenhum. Sai com o jogo, e nenhuma classe no escopo atual tem os quatro quadrantes.
O CronJob de backup também sai, e ele tinha um problema próprio que vale não perder de vista: o ttl_seconds_after_finished: 10 apaga o Job dez segundos depois de terminar, e sobra uma amostra por execução no Prometheus. O postmortem de 14/10 mediu isso, e a saída é a ação A2 de lá, observar o artefato no S3 e não o Job.
O que falta para as convenções funcionarem
Classificar não é derivável
Testei um classificador automático sobre os 53 workloads do cluster inteiro, usando namespace, tipo de Service, presença de rota e ausência de Service. Ele errou 4 casos, e os quatro são informativos:
| Workload | Meu palpite | É | Por que errou |
|---|---|---|---|
cloudflare-tunnel/cloudflared | worker | addon | não tem Service, e "sem Service é worker" não sabe o que é um túnel |
patch-server/patchserver-en-app | worker | HTTP externo | o Service se chama patchserver-en-service e o nome não casou |
patch-server/patchserver-jp-app | worker | HTTP externo | idem |
kelbi/kelbi-launcherserver | HTTP interno | web | roteado por IngressRoute, que meu parser não leu |
Taxa de acerto de 92%, com os erros nos casos de borda. Um workload no quadrante errado parece um workload saudável, então os quatro passariam sem ninguém notar.
Três dos quatro erros estão fora do escopo atual, e o que sobra dentro é justamente o cloudflared, que é o único addon sem component por defeito do chart upstream. O argumento não depende do recorte: o teste mostra que derivar a classe de topologia erra nos casos que importam.
As aproximações mais simples falham pior:
| Aproximação | Falha em |
|---|---|
| Namespace | argocd é plataforma e kelbi-pg tem o banco do produto; kelbi tem o jogo e o launcherserver |
| Estar atrás do Traefik | argocd-server tem os três sinais de request e é plataforma |
| Chart de origem | addon e aplicação vêm ambos de chart Helm |
O label existe em dois lugares, com valores diferentes
O k8sattributes extrai label de pod, e o pod não carrega o mesmo valor que o Deployment. Vale para o app legado e vale igual para app.kubernetes.io/name onde ele existir:
| Workload | no Deployment | no pod | Qual serve |
|---|---|---|---|
kelbi-channel-1-1 .. -6-1 | app=kelbi-channel | app=kelbi-channel-1-1 | Deployment |
patchserver-en-app | ausente | app=patchserver-en | pod |
cloudflared, reloader, entranceserver, signserver | igual | igual | qualquer |
Nenhuma das duas fontes resolve sozinha. Só do pod mantém os seis canais como seis valores, que é a situação de hoje. Só do Deployment perde os dois patchserver.
A RFC 0003 afirmava que app.kubernetes.io/instance e /name existiam em 100% dos workloads, com 32 contados em 13/10/2026, e usava isso como base para os itens 4 e 5. Medido hoje são 31 de 52 nos pods, e os seis kelbi-channel não têm nenhum dos dois: eles carregam o app legado.
A seção correspondente foi corrigida em 15/10/2026.
Princípio de design
Usar o atributo que a convenção já nomeia, e preencher o que falta. Nenhum label novo. As duas primeiras versões desta RFC propuseram app e role, e os dois já existem como service.name e app.kubernetes.io/component.
Classe é declarada, nunca derivada. O classificador acima errou 4 de 53 com acesso a todo o estado do cluster, e errou nos casos de borda. A informação não existe no cluster: ela existe na cabeça de quem construiu.
service.name é mais grosso que o nome do workload. Para os seis canais, o nome da aplicação é kelbi-channel, e o job tem seis valores porque a cadeia cai no nome do Deployment. A fonte do label importa tanto quanto o nome dele.
Proposta
1. Tirar service.name da lista metadata, ou a annotation nunca vence — premissa corrigida
A primeira versão deste item dizia para pôr app.kubernetes.io/name na cadeia OTTL, acima do nome do workload. A cadeia não é o que decide.
Medido em 15/10/2026, removendo a cadeia inteira: job, instance, service.name, service.namespace e service.instance.id ficaram idênticos. Quem define service.name é o extract.metadata do k8sattributes, que o chart pede com service.name na lista, e ele resolve para o nome do Deployment. A cadeia de 14 statements estava, para pods, repetindo trabalho já feito.
É também por isso que a annotation resource.opentelemetry.io/service.name não venceu quando o chart kelbi-app passou a declará-la: o otel_annotations só preenche atributo que a lista metadata não reivindica. Medido no mesmo dia:
| Atributo | Na lista metadata? | Annotation venceu? |
|---|---|---|
service.name | sim | não |
service.version | sim | não dá para distinguir |
deployment.environment.name | não | sim |
service.criticality | não | sim |
Então o item passa a ser: tirar service.name de extract.metadata, e deixar a annotation ser a fonte. A alternativa é um statement OTTL que prefira resource.opentelemetry.io/service.name, que funciona mas mantém duas fontes disputando o mesmo campo.
Ele tinha dois motivos, e ambos saíram no mesmo dia.
O primeiro era tirar o prefixo prod- do service.name. Incluir o ambiente no nome é aceito, então prod-kelbi-web é valor bom, e o evento D6 que este item previa desaparece junto: nenhuma série é renomeada, nenhuma consulta quebra.
O segundo eram os seis canais. Medido em 16/10/2026, eles são seis Deployments com nomes distintos, de kelbi-channel-1-1 a kelbi-channel-6-1, e os pods não têm app.kubernetes.io/name nenhum. Juntá-los num só service.name exigiria valor declarado, e para o declarado vencer, service.name teria de sair de extract.metadata. Só que os canais estão fora do escopo desde o recorte do mesmo dia.
Para os 29 pods que restam, o nome do Deployment é um service.name aceitável, e não há o que fazer aqui.
Sobra uma limpeza. O chart kelbi-app declara resource.opentelemetry.io/service.name desde a 5.9.0, e essa annotation nunca vence, porque service.name está em extract.metadata. É configuração morta que aparenta estar funcionando, a mesma classe de defeito do Velero anotado com prometheus.io/* por 160 dias. Tirar do chart custa uma linha, e deixar custa a próxima pessoa que for depurar por que o valor declarado não aparece.
2. Ambiente em deployment.environment.name, via annotation — feito
Entregue no chart kelbi-app 5.9.0, em 15/10/2026. Nada a criar e nada a configurar do lado do collector: otel_annotations: true já estava ligado, e a annotation no pod vira o atributo.
Em 16/10/2026 entrou uma linha de configuração, e só uma: o label na métrica passou a ser environment em vez de deployment_environment_name, por um resource processor. A annotation no pod não mudou.
O chart declara quatro atributos e valida dois contra conjunto fechado, falhando o render em valor inesperado:
resource.opentelemetry.io/service.name: kelbi-api
resource.opentelemetry.io/deployment.environment.name: production # development|production|staging|test
resource.opentelemetry.io/service.criticality: critical # critical|high|medium|low
resource.opentelemetry.io/service.version: "1.16.0"
Os dois defaults são os mais estritos de propósito: uma app que ninguém classificou reporta production e critical, porque o modo de falha do default oposto é uma app que não pagina ninguém.
O service.criticality não estava nesta RFC e entrou por decisão de 15/10/2026. Ainda é beta nas semantic conventions, e foi adotado para que roteamento e paginação sejam decididos por aplicação em vez de por regra de alerta.
Isso substitui os itens 4 e 5 da RFC 0003, que propunham criar um label de ambiente. O label não precisava ser criado.
O que ainda não funciona: o service.name declarado ali não vence, pelo motivo do item 1. Os outros três chegam.
3. Peça em app.kubernetes.io/component — chart feito, valores pendentes
O chart kelbi-app 5.9.0 passou a emitir o label, com kelbi-app como padrão e override por values. Em 15/10/2026 são 13 pods com ele.
O vocabulário já existia no chart. O kelbi-app valida type contra web, worker e singleton desde antes desta RFC, e o valor é carga útil: decide se o container declara porta e se há health check. É o mesmo vocabulário da decisão de 16/10.
Entregue no kelbi-app 5.10.0, em 16/10/2026: component cai em type por padrão, e .Values.component sobrescreve quando a peça é mais fina que a forma. O conjunto de valores fica aberto, diferente de type, environment e criticality, que rejeitam valor fora do conjunto no render.
O que falta são os valores. Cada aplicação declara a peça nos seus values, que vivem no repo kelbi-applications e não neste chart. Sem declarar, cada uma herda o type, e as 10 que são web ficariam todas como web, perdendo a distinção entre api, cdn e web.
A diferença entre os dois aparece nas 12 aplicações: prod-kelbi-api e prod-kelbi-cdn são ambas type: web, e são peças diferentes. O type descreve a forma do workload, o component descreve a peça.
| Aplicação | type | component | part-of |
|---|---|---|---|
prod-kelbi-web | web | web | kelbi |
prod-kelbi-api | web | api | kelbi |
prod-kelbi-cdn | web | cdn | kelbi |
prod-kelbi-jobs | singleton | singleton | kelbi |
prod-halk-en-api, prod-halk-jp-api | web | api | halk |
os 4 prod-halk-*-cdn | web | cdn | halk |
prod-mhfrontier-cq-tool-api | web | api | a decidir |
prod-mirim-bot | singleton | singleton | a decidir |
As duas últimas linhas são as únicas em dúvida, e a dúvida é de fronteira de produto, não de peça: o mhfrontier-cq-tool e o mirim-bot podem ser produtos próprios ou partes do kelbi.
O conflito com chart de terceiro que a versão anterior desta RFC apontava não existe, e a medição está na nota da seção da taxonomia: os sete pods do ArgoCD e o primary do Postgres já usam o label no sentido da convenção.
Resta um workload que não alcança o label: o cloudflared, cujo chart upstream declara podLabels e não usa em template nenhum, o mesmo defeito que levou o service.criticality dele a ir por annotation. Medido em 16/10/2026, é o único pod sem component entre os que o kelbi-platform governa.
4. Extrair com allowlist — feito
Entregue no chart kelbi-platform 1.16.0 em 15/10/2026, e corrigido na 1.19.0 e na 1.20.0 em 16/10/2026. As duas flags amplas saíram e sobrou isto:
extract:
labels:
- { from: pod, key: app.kubernetes.io/component, tag_name: component }
- { from: pod, key: app.kubernetes.io/part-of, tag_name: part_of }
Dois labels, e os dois são classificação. Nada mais.
A identidade não passa por aqui, vem de extract.metadata. As annotations do OpenTelemetry também não, vêm do otel_annotations: true. Os dois são do preset, e o merge profundo os preserva. A allowlist cobre só o que o preset não dá de graça.
As flags traziam 41 labels por série, entre eles quatro checksum_* de 64 caracteres e um io_opentelemetry_discovery_metrics_config de 172 caracteres de YAML, que era a configuração de scrape do próprio collector ecoada como label de métrica.
A allowlist original listava cinco chaves sem tag_name, e isso mudou o nome de quatro labels sem que fosse pedido. O preset extractAllPodLabels gerava tag_name: $$1, que produz o nome do label cru; a lista de chaves sem tag_name cai no padrão do k8sattributes, que prefixa com k8s.pod.labels..
Medido em 16/10/2026: app_kubernetes_io_component e os três irmãos em zero séries, com os valores em k8s_pod_labels_app_kubernetes_io_*. Nenhuma regra e nenhum painel os lia, então passou sem consequência. É evento D6 por falta de consumidor, não por acerto.
A 1.19.0 declarou tag_name e, no mesmo passo, tirou três labels da extração:
app.kubernetes.io/namee/instanceeram redundantes. Não alimentamjobneminstance, porque quem alimenta éextract.metadata. Medido antes de remover:service_nameem 801 séries eservice_instance_idem 697, contra 34 de cada label.svccontroller.k3s.cattle.io/svcnamedizia qual Service cadasvclb-*atende, e ossvclb-*saíram do escopo junto com o jogo.
Os nomes curtos foram conferidos contra os 238 labels do servidor antes da escolha: instance é reservado do Prometheus, name está vivo com a auto-telemetria, app tem 44 valores da família app: de recording rules, e role e type pertencem ao service discovery. component e part_of estavam livres.
A 1.20.0 removeu o bloco annotations inteiro, por duplicar o que o otel_annotations já entrega. A medição está em O que o OpenTelemetry define.
O config deste chart é mesclado com o que o preset gera, e se a mescla fosse de substituição em vez de profunda, declarar extract.labels apagaria extract.metadata, otel_annotations, pod_association e filter.node_from_env_var, que é toda a identidade de pod, em silêncio.
Verificado em 15/10/2026 renderizando o chart opentelemetry-collector 0.150.1 com o nosso valuesObject: a mescla é profunda. As 23 entradas de metadata, o otel_annotations: true, o pod_association e o filter sobrevivem, e só labels e annotations são substituídos.
Quem mexer nesse bloco deve repetir a verificação, que é helm template do chart upstream com o valuesObject extraído e uma leitura do ConfigMap relay resultante.
A hipótese natural é que extractAllPodLabels alimentasse churn de cardinalidade, por causa de pod_template_hash, controller_revision_hash, checksum_* e kubectl.kubernetes.io/restartedAt. Medido, não alimentava. Numa janela de 5 dias, os únicos labels que variavam dentro da vida de um mesmo pod eram job, instance, service_name e service_instance_id, todos saída da cadeia de identidade.
A troca por allowlist se justificou por limpeza e por tornar a dependência explícita, não por cardinalidade.
4b. Identidade para objeto do Kubernetes que não é pod — feito
Também na 1.16.0, e é o que o experimento de 15/10/2026 revelou como a parte realmente necessária da cadeia antiga.
Com a cadeia removida, 846 séries ficaram sem job: k8s_replicaset_* (478), k8s_deployment_* (82), k8s_namespace_* (20) e o resto, mais o nó do Hostinger com job e instance vazios. É o defeito D2 da RFC 0003 exatamente como ela o descreveu: o k8sattributes só preenche service.* quando consegue associar um pod, e recurso de nível cluster não tem pod.
A cadeia voltou com dez statements em vez de quatorze, cobrindo deployment, statefulset, daemonset, cronjob, job e namespace, com os dois de host no fim como fallback. Os quatro que tratavam de pod não voltaram, porque a medição provou que o k8sattributes já fazia aquele trabalho.
E as métricas de ReplicaSet passaram a ser descartadas no collector, em vez de identificadas:
filter/unconsumed:
metrics:
metric:
- 'IsMatch(name, "^k8s\\.replicaset\\..*")'
São 478 séries, 239 ReplicaSets para 41 Deployments, porque o Kubernetes guarda as revisões antigas. Nenhum alerta e nenhuma recording rule as consome, o que o hygiene:series_unconsumed:count já media desde 11/10/2026. O único nome disponível é k8s.replicaset.name, que carrega o hash do pod template, então identificar criaria 239 jobs com um novo a cada deploy. As métricas de Deployment respondem a mesma pergunta, têm nome sem hash e são consumidas por duas recording rules.
k8s.hpa.* e k8s.statefulset.* ficam, apesar de a mesma regra de higiene também contá-las como não consumidas: o kelbi-pg é StatefulSet e é o banco produtivo.
4c. A identidade de host passou a usar host.name — feito
Na 1.15.0. O atributo padrão do OpenTelemetry é carimbado com o valor de opentelemetryCollector.host.name, e service.name e service.instance.id passam a derivar dele em vez de recebê-lo direto. A convenção permite nome escolhido pelo operador, então o nome lógico hostinger-k3s-cluster-01 não conflita com a especificação.
O contabo-monitoring-server recebeu o mesmo atributo no mesmo dia, nos processors resource/host e resource/self, então os dois servidores se distinguem por host.name e não só por job.
5. Habilitar as métricas de utilização (A9 do postmortem)
Sem as quatro métricas opcionais do kubeletstats, não há saturação para nenhuma classe. É a dependência mais dura desta RFC, e o retorno é maior que o painel: ressuscita sete alertas que nunca puderam disparar, incluindo o critical de OOMKill.
6. Começar o painel pelas 7 de HTTP externo — premissa corrigida
A versão anterior mandava começar pelo banco, que era o único com os quatro sinais prontos. Com o recorte de escopo o banco saiu, e nenhuma classe tem os quatro quadrantes.
O primeiro painel passa a ser o das 7 aplicações de HTTP externo: três sinais vêm do Traefik sem trabalho de coleta, e o quarto chega com a A9. É o único conjunto do escopo onde a maior parte do caminho já está andada.
Com isso o painel nasce incompleto por um trimestre em vez de nascer completo, e a consequência prática é a do risco lá embaixo: ele tem de declarar a saturação indisponível em vez de omitir o quadrante.
7. Derivar o nome da aplicação das métricas do Traefik
Um transform que extraia a aplicação do label service para o formato @kubernetesgateway, que é <namespace>-<service>-<entrypoint>-<porta> e é parseável.
O formato @kubernetescrd carrega um hash do IngressRoute e não é parseável de volta. São 4 das 12 séries: o launcherserver e os dois patchserver, todos fora do escopo depois do recorte, mais o homepage. Dentro do escopo as 7 usam HTTPRoute, então o transform as cobre inteiras, e o @kubernetescrd deixa de ser bloqueio para virar limitação conhecida de quem entrar depois.
8. Coletar as métricas de Application do ArgoCD
Hoje argocd_* tem zero séries, por duas falhas diferentes:
- O
argocd-application-controller, que expõe o estado por Application na porta 8082, não tem anotação de descoberta nenhuma. - O
argocd-repo-servertemio.opentelemetry.discovery.metrics.8083/enabled, e 8083 é a porta doargocd-server; a dele é a 8084.
É a mesma classe do Velero, que em 15/10/2026 estava anotado para scrape havia 160 dias com a convenção prometheus.io/*, que este stack não lê.
O mapeamento se define depois de ler os nomes reais de label, não antes: em 15/10/2026, escrever uma regra sobre velero_backup_last_successful_timestamp antes de coletar teria falhado, porque essa métrica não existe, e velero_backup_last_status não significa o que o nome sugere.
9. Instrumentar o jogo e os workers — sai desta RFC
Por decisão de 16/10/2026. Fica aqui o escopo levantado, para a RFC própria não recomeçar da estaca zero.
É o item grande, fora do alcance de configuração de coleta, e a única via para o painel cobrir 14 dos 26 workloads não-plataforma. Nenhum deles declara porta de container, então começa por expor um endpoint de métrica.
O mínimo por classe de sinal: jogo com conexões ativas, contagem e taxa de erro de operação, e latência de operação, nos repos Kelbi e Erupe; HTTP interno com as três de request do lado do servidor; worker contínuo com itens processados, falhas e duração do ciclo.
A separação é de ordem de magnitude, não de importância: os outros nove itens são configuração e label, com custo recorrente zero e implementação em horas. Este é código em quatro repos de produto.
10. Guard rails de cobertura
Duas métricas e dois alertas info para o triage, na linha do que a RFC 0003 fez com a família kind: hygiene: workloads sem app.kubernetes.io/component, e séries com part-of diferente de platform sem service.name derivado de app.kubernetes.io/name.
Com a decisão de 16/10, o guard rail não pode conferir o valor do component contra uma lista: o conjunto é aberto, e uma peça nova é legítima. O que ele confere é presença, e um valor específico que é sempre erro, o nome de um chart.
Medido depois da entrega da tarde de 16/10/2026, dentro do escopo sobra 1 workload a reportar: o cloudflared, sem o label por defeito do chart upstream. As 12 aplicações saíram de component=kelbi-app para a peça de cada uma, e os 7 pods do ArgoCD mais os 8 addons restantes já passavam.
Sem isso, as classificações degradam em silêncio a cada workload novo, que é exatamente como a cobertura de app.kubernetes.io/name ficou em 31 de 52 sem ninguém notar.
Decisões tomadas em 16/10/2026
component carrega a peça arquitetural
Está na seção da taxonomia. A classe de observabilidade deixa de ser label e passa a se deduzir da peça.
O prod-kelbi-jobs é singleton
Mesma peça do prod-mirim-bot, apesar de executar trabalho agendado em vez de atender a um bot. singleton é processo único e contínuo sem porta de entrada, e os dois cabem nessa descrição.
O kelbi-launcherserver é web, por enquanto
Ele cai em duas descrições: mora no namespace kelbi, que é o jogo, e serve HTTP com rota pública. A peça é web enquanto não houver razão para distingui-lo, e o "por enquanto" está registrado de propósito: se o launcher ganhar comportamento próprio, launcher é a peça natural, e a troca custa um valor de values.yaml.
Vale notar que meu classificador automático o havia posto em http-internal, que estava errado nas duas leituras.
service.name pode incluir o ambiente
prod-kelbi-web é valor aceito. Casa com o nome da Application do ArgoCD sem transformação nenhuma, e a comparação entre ambientes, que seria o argumento contra, não tem caso de uso hoje: não existe ambiente além de produção no cluster.
O efeito no item 1 está na nota de lá: ele perde uma das duas motivações e o evento D6 que previa.
O item 9 sai desta RFC
Os itens 1 a 8 e 10 são configuração e label, com custo recorrente zero e implementação medida em horas. Instrumentar o jogo e os workers é código em quatro repos de produto, e vira RFC própria.
O número ainda não está escolhido: 0005-sampling.md existe como rascunho no repositório e não é meu.
part-of carrega o produto
Decidido em 16/10/2026, e era a última pergunta que a decisão sobre o component tinha aberto. platform é o nome do produto interno, então os 9 addons com part-of=platform não mudam.
Lê-se bem no caso concreto (o cloudflared faz parte da plataforma) e é a leitura que sustenta a intenção de produtizar: quando o Postgres for produto da plataforma, a peça segue primary e só o part-of muda de kelbi para platform. Com part-of na camada, essa transição não teria onde ser registrada.
Dois custos, os dois aceitos:
- A separação plataforma/aplicação deixa de ter um label só dela. Ela sai da combinação:
part-of=platformcobre os nossos addons, e o resto dos valores depart-ofé aplicação. Um produto novo entra como aplicação por omissão, que é o lado seguro. - O ArgoCD é exceção, e não por economia. Detalhado abaixo.
part-of do ArgoCD é carga útil, não decoraçãoA primeira versão desta decisão dizia que sobrescrever o part-of=argocd dos 7 pods seria "fork de chart de terceiro, que não se paga por um label". O argumento estava errado, e a razão é mais forte: o ArgoCD usa esse label como seletor para encontrar a própria configuração, então o valor é dele e não nosso para reaproveitar.
A evidência está no cluster, medida em 16/10/2026. O label aparece em exatamente 11 objetos, e são precisamente os que o ArgoCD consulta:
configmap/argocd-cm secret/argocd-secret
configmap/argocd-cmd-params-cm secret/argocd-dex-secret
configmap/argocd-rbac-cm secret/argocd-notifications-secret
configmap/argocd-notifications-cm
configmap/argocd-ssh-known-hosts-cm
configmap/argocd-tls-certs-cm
configmap/argocd-gpg-keys-cm
configmap/argocd-redis-health-configmap
E está ausente nos 7 objetos do namespace que o ArgoCD não procura por label: os secrets de repositório, o argocd-initial-admin-secret, o argocd-redis, o release do Helm e o kube-root-ca.crt. A distribuição não é uniforme, é funcional.
Consequência: part-of=argocd fica como está, e o ArgoCD é a única parte da plataforma sem um label que diga isso. Identificá-lo como plataforma depende do namespace, porque nem o part-of nem o component servem: server, repo-server e redis são peças corretas e nenhuma delas é exclusiva de plataforma.
Isso também é um lembrete sobre o alcance desta RFC: label de terceiro pode ter função, e presumir que é só metadado é como presumir que uma annotation é lida. O ArgoCD não é instalado pelo kelbi-platform, é release Helm própria, então não há onde intervir mesmo que se quisesse.
Decisões abertas
Nenhuma. As cinco perguntas desta seção foram respondidas em 16/10/2026, e estão registradas acima.
O que resta é execução, e a lista está no critério de aceitação. O item mais próximo é preencher part-of nas 12 aplicações, que vive nos values do repo kelbi-applications e não neste chart.
Critério de aceitação
Convenções
-
service.nameidentifica o workload. Por decisão de 16/10/2026, o nome do Deployment serve, prefixoprod-incluído, e o item 1 fecha com uma limpeza -
deployment.environment.namepresente, viaresource.opentelemetry.io/deployment.environment.name(kelbi-app5.9.0, 15/10/2026), chegando à métrica comoenvironment(kelbi-platform1.20.0, 16/10/2026): 455 séries, todasproduction - Nenhum atributo chega duplicado. A extração crua de
resource.opentelemetry.io/*saiu na 1.20.0, depois de medir os pares batendo exatos em 455 e 2087 séries -
service.versioncontinua vindo deapp.kubernetes.io/version, como já vinha - Nenhum label novo fora das convenções do OpenTelemetry e do Kubernetes. O
service.criticality, que entrou depois, é atributo do OTel ainda em beta
Classificação
- Todo workload do escopo tem
app.kubernetes.io/componentcom a peça arquitetural, e nenhum tem o nome do chart. Entregue em 16/10/2026 pelokelbi-app5.10.0 e pelos values das 12 aplicações. Medido nas métricas:api4,cdn5,web1,singleton2,addon9, mais as 7 peças do ArgoCD e oprimary. Sobra o cloudflared, na linha abaixo - As 12 aplicações declaram
part-ofcom o produto:kelbi6 ehalk6, medido nas métricas em 16/10/2026 - Os 8 workloads que tinham
componentcom outro significado foram revistos antes de sobrescrever. Medido em 16/10/2026: já estavam corretos, nada a sobrescrever - O cloudflared tem a peça registrada por algum caminho, ou está contado como lacuna conhecida de 1 workload
- A annotation morta
resource.opentelemetry.io/service.namesaiu do chartkelbi-app(5.10.0, 16/10/2026). Ela nunca venceu uma vez, porqueservice.nameestá emextract.metadata - O
argocd-serveré reconhecido como plataforma apesar de estar atrás do Traefik. Ele mantémcomponent=server, que é a peça correta, e nenhum label o identifica como plataforma: opart-of=argocdé seletor que o próprio ArgoCD usa, então o critério depende do namespace. Ver a nota empart-ofcarrega o produto - A extração usa allowlist explícita, e não
extractAllPodLabels/extractAllPodAnnotations(kelbi-platform1.16.0, 15/10/2026) - O cloudflared, único caso de erro do classificador que ficou no escopo, está classificado à mão e conferido
Os quatro sinais
- Saturação: as quatro recording rules de utilização têm série, para todo container com
limitdeclarado - Containers sem
limitsão conhecidos e contados, porque para eles não há saturação - Latência, tráfego e erros por aplicação para as 7 de HTTP externo
- Séries do Traefik sem nome de aplicação (formato
@kubernetescrd) conhecidas e contadas - As 5 aplicações sem fonte de sinal de request são conhecidas e contadas, em vez de aparecerem como painel vazio. A instrumentação delas saiu para RFC própria
Coleta
-
argocd_*chegam, com oargocd-application-controllercoletado na 8082 - A anotação do
argocd-repo-serveraponta para a 8084 - Guard rails de cobertura
- Identidade de host via
host.name, nos dois servidores (kelbi-platform1.15.0 econtabo-monitoring-server, 15/10/2026) - Objetos do Kubernetes que não são pod voltaram a ter
job(kelbi-platform1.16.0) -
k8s.replicaset.*descartado no collector, 478 séries que nenhuma regra consumia -
hygiene:series_without_identity:countde volta a 0, medido em 16/10/2026. Eram 830 durante o experimento de 15/10.hygiene:series_without_job:counttambém em 0 ehygiene:identity_coverage:ratioem 1 - A afirmação de 100% de cobertura de
app.kubernetes.io/*corrigida na RFC 0003, em 15/10/2026
Resultado
- Um painel por classe, com os quatro sinais dela, agrupado por
service.name - A lista de aplicações não inclui plataforma
Riscos
Mudar o conjunto de labels reseta o estado dos alertas. É o defeito D6 da RFC 0003. Em 15/10/2026, acrescentar kind: hygiene a doze alertas devolveu todos para pending e zerou a contagem de crônicos por quase uma hora. As decisões de 16/10 removeram o maior caso deste risco, que era a renomeação dos 13 job do item 1, mas preencher component nas 12 aplicações ainda acrescenta label.
Extrair da origem errada passa no critério e não serve para nada. Se a extração ler só o label do pod num conjunto de réplicas nomeadas individualmente, a cobertura chega a 100% com valores idênticos aos de job, o guard rail do item 10 aprova, e o label não responde nada novo. Foi o erro da segunda versão desta RFC, e o caso que o expôs, os seis canais, saiu do escopo sem que a armadilha saísse com ele.
O painel pode ser construído com três sinais e parecer completo. Latência, tráfego e erro vêm de graça do Traefik; saturação depende do item 5. Um Four Golden Signals sem saturação não avisa antes de doer, que é a função do quarto sinal. Enquanto A9 não estiver feita, o painel deve declarar a saturação indisponível em vez de omitir o quadrante.
component pode ser preenchido no automático. Se alguém derivar o valor por heurística em vez de declarar, o resultado é 92% de acerto com os erros nos casos de borda. O guard rail não pega: cobertura de 100% com valores errados passa no critério.
Fora do escopo
- O jogo, por decisão de 16/10/2026: os namespaces
kelbiekelbi-pg, e nokube-systemo que o k3s traz sozinho. O vocabulário de peça os acomoda quando entrarem, e o Escopo registra o que se perde no caminho. - Instrumentar o jogo e os workers, que era o item 9 e saiu na mesma decisão. O escopo levantado fica registrado no item, para a RFC própria não recomeçar da estaca zero.
- Padronizar as rotas do Traefik em Gateway API. Resolveria o formato
@kubernetescrd, e é mudança de roteamento de produção com escopo próprio. - SLO e alerta sobre os quatro sinais. Esta RFC viabiliza a medição; o que alertar se decide com o painel na mão.
- O painel em si. Esta RFC entrega atributos e coleta; o Grafana vem depois.
- Observar tráfego interno pod a pod. Resolveria os
http-internalsem mexer no código, via proxy ou service mesh, e custa mais que instrumentar. - Correlação com log e trace. Só métrica aqui, embora
service.nameedeployment.environment.namesejam exatamente os atributos que fariam essa correlação funcionar depois.
Consequências
O ganho que não estava à vista nas duas primeiras versões: nenhum label novo para manter. service.name, service.namespace, service.version, deployment.environment.name e app.kubernetes.io/* são convenções com documentação, ferramenta e significado combinado fora deste projeto. Um painel do Grafana, uma biblioteca de instrumentação ou um colaborador novo encontram o que esperam.
Daí sai o painel das 7 aplicações de HTTP externo, completo com a A9. As outras 5 passam a ter uma lacuna nomeada, que é diferente de uma lacuna invisível: hoje ninguém sabe que o prod-mirim-bot não tem taxa de erro, porque nunca houve um painel para mostrar o espaço em branco.
E a triagem melhora. Um alerta que hoje chega dizendo job=apps/prod-mirim-bot passa a dizer o mesmo nome com component=singleton, part-of=kelbi e deployment.environment.name=production, e a diferença entre "um dos doze workloads de apps" e "o bot, worker único, do produto kelbi, em produção" é a diferença entre despachar o alerta e investigar o que ele é.
O custo recorrente é zero em todos os itens.