WhyStack Profiler
Métrica e log mostram sintoma: a latência subiu, o erro apareceu. Eles não dizem o que mudou dentro do processo entre a versão que funcionava e a que não funciona.
O WhyStack Profiler mede isso. Ele observa um processo por uma janela curta depois de cada deploy e guarda uma assinatura comportamental compacta: quanto o programa aloca e onde, que chamadas de sistema faz, como as esperas por lock se distribuem, quantos peers de rede ele tem. Comparando duas versões, dá para dizer “esta é a mudança” em vez de “algo piorou”.
Isso custa uma permissão que o resto do WhyStack não pede. Esta página existe para você decidir com a lista na mão.
Ele é um segundo agente, e isso é de propósito
Seção intitulada “Ele é um segundo agente, e isso é de propósito”Seria mais simples ligar uma flag no Agent. Não fazemos isso porque uma flag deixa o privilégio invisível: quem herda o ambiente meses depois não tem como saber, olhando o deployment, que aquele container passou a poder ler a memória de outros processos. Sendo dois artefatos, o privilégio aparece na porta de entrada.
A separação não depende de disciplina. O Agent recusa subir se detectar capability de kernel no próprio processo. E o Profiler não pode ser configurado para ler as suas integrações: a imagem dele não registra connector de dado nenhum, então não existe variável que o transforme num leitor da sua stack. Uma que peça isso é ignorada, e ele diz no log de boot que ignorou. Nenhum dos dois vira o outro por engano de configuração.
Até a versão 1.15.0 esta página prometia mais do que o código entregava, na direção que menos convém: o Profiler subia servindo o conjunto de dados sintético que existe para desenvolvimento. Nada da sua infraestrutura era lido (o conjunto é fabricado), mas ele ocupava lugar de fonte na frota. Corrigido pela raiz: a classe do artefato decide, e não a configuração do deploy.
Antes de tudo: este host serve?
Seção intitulada “Antes de tudo: este host serve?”O portão mais duro aqui é o kernel, e ele reprova hosts que ninguém espera ver reprovados. Por isso ele é a primeira pergunta da qualificação, não a última. Um host que não passa daqui não chega à POC; descobrir isso durante a POC custa a POC.
Rode o preflight no host candidato. Ele só lê /proc e /sys, não muda nada e não precisa de root:
curl -O https://docs.whystack.ai/profiler-preflight.shsh profiler-preflight.shBaixe e leia antes de executar. O script inteiro está publicado logo abaixo, e ninguém deveria rodar
script de terceiro sem olhar. É por isso que não oferecemos um curl | sh.
Os portões que ele decide
Seção intitulada “Os portões que ele decide”| Portão | Por que ele reprova, e como ele falharia calado |
|---|---|
| Kernel ≥ 5.8 | BTF, ring buffer e CAP_BPF chegam nessa versão. O filtro é o kernel, não a distribuição: RHEL 7 / 3.10 está fora, e o fallback sem eBPF é só /proc. |
| BTF compilado | Versão ≥ 5.8 não implica BTF: um kernel construído sem CONFIG_DEBUG_INFO_BTF passa no portão anterior e reprova neste. É por isso que são dois portões e não um. |
| cgroup v2 | Sem a hierarquia unificada não há como escopar uma captura por cgroup. |
| tracefs montado | É onde a biblioteca de eBPF procura os tracepoints. Sem ele os coletores carregam, não anexam, e a captura volta com eixos faltando em vez de vazia, o que se lê como “esse comportamento parou”. |
| Perfil de confinamento | O docker-default só permite ptrace entre containers do mesmo perfil. Herdá-lo custa dois coletores, em silêncio. |
“Não sei” não é aprovação
Seção intitulada ““Não sei” não é aprovação”O script sai com três códigos, e o do meio é o que costuma faltar num checklist de qualificação:
0 tudo passou · 1 algum portão duro reprovou · 2 nada reprovou e algo não pôde ser decidido dali.
O caso normal do 2 são as cinco capabilities: o shell de quem roda o script não é o workload do
Profiler, então o que aquele shell tem não responde o que o Profiler poderá ter. Isso é política de quem
administra o cluster. Um checklist que contasse isso como “ok” entregaria uma aprovação que ninguém
verificou, e é exatamente a falha que o script existe para evitar. (A primeira versão dele cometeu essa
falha ao contrário: checava /sys/kernel/tracing/events com test -d, que falha para usuário sem
privilégio porque o diretório é 0700 root:root, reprovando um host perfeitamente montado. Ele agora lê
/proc/mounts, que é legível por todos.)
A pergunta que vem antes das técnicas
Seção intitulada “A pergunta que vem antes das técnicas”Anexar um coletor privilegiado a software de terceiro (de fornecedor, COTS, legado sem quem o mantenha)
pode violar ou invalidar o contrato de suporte que você tem com quem o vende. É pergunta jurídica, e
por isso ela vem antes das técnicas: nenhum resultado do script a substitui. O script a imprime junto das
outras duas que ele não pode responder (hostPID e a lista de alvos), para que ela não seja esquecida por
já estar “tudo verde”.
O script, na íntegra
Seção intitulada “O script, na íntegra”#!/bin/sh# WhyStack Profiler: host preflight.## Answers one question, before anyone installs anything or grants a capability:# CAN this host run the Profiler at all? It reads; it changes nothing.## Written in English on purpose: this is one canonical file served to readers of# both the Portuguese and the English documentation, and a single script cannot# be bilingual. Each check is explained, in your language, on the page that# publishes this script verbatim: https://docs.whystack.ai/security/profiler/## Exit codes. A check that could not run is NOT a pass:# 0 every gate passed, and nothing was left unknown# 1 at least one hard gate FAILED: this host is out until it is fixed# 2 no gate failed, but something could not be determined from here## Run it as the least privileged user you have. It needs no root: every gate it# can decide is decidable by reading /proc and /sys.
set -u
PASS=0FAIL=0UNKNOWN=0
pass() { printf ' PASS %s\n' "$1"; PASS=$((PASS + 1)); }fail() { printf ' FAIL %s\n ↳ %s\n' "$1" "$2"; FAIL=$((FAIL + 1)); }unknown() { printf ' UNKNOWN %s\n ↳ %s\n' "$1" "$2"; UNKNOWN=$((UNKNOWN + 1)); }
printf 'WhyStack Profiler: host preflight\n'printf 'host: %s kernel: %s arch: %s\n\n' "$(uname -n)" "$(uname -r)" "$(uname -m)"
# ── 1. Kernel ≥ 5.8 ───────────────────────────────────────────────────────────# BTF, the BPF ring buffer and CAP_BPF all land in 5.8. Below it the Profiler# refuses to boot rather than run with half its collectors.release=$(uname -r)major=$(echo "$release" | cut -d. -f1)minor=$(echo "$release" | cut -d. -f2 | cut -d- -f1)case "$major$minor" in *[!0-9]*|'') unknown "kernel >= 5.8" "could not parse a version out of '$release'" ;; *) if [ "$major" -gt 5 ] || { [ "$major" -eq 5 ] && [ "$minor" -ge 8 ]; }; then pass "kernel >= 5.8 ($release)" else fail "kernel >= 5.8 ($release)" \ "this host is out. The gate is the kernel, not the distribution: RHEL 7 / 3.10 cannot run this, and --no-ebpf is only /proc." fi ;;esac
# ── 2. BTF actually compiled in ───────────────────────────────────────────────# Version >= 5.8 does NOT imply BTF: a kernel built without CONFIG_DEBUG_INFO_BTF# passes check 1 and fails here. This is the gate that surprises people, so it is# separate from the version rather than folded into it.if [ -r /sys/kernel/btf/vmlinux ]; then pass "kernel BTF present (/sys/kernel/btf/vmlinux)"else fail "kernel BTF present" \ "no /sys/kernel/btf/vmlinux. The kernel was built without CONFIG_DEBUG_INFO_BTF; the version alone never proved it."fi
# ── 3. cgroup v2 ──────────────────────────────────────────────────────────────if [ -r /sys/fs/cgroup/cgroup.controllers ]; then pass "cgroup v2 unified hierarchy"else fail "cgroup v2 unified hierarchy" \ "no /sys/fs/cgroup/cgroup.controllers. A v1-only or hybrid host cannot scope a capture by cgroup."fi
# ── 4. tracefs mounted ────────────────────────────────────────────────────────# This one earns its own gate because of HOW it fails: without tracefs the eBPF# programs load and never attach, so the capture comes back with axes MISSING# rather than empty, which reads like "nothing changed in this deploy".## It is read from /proc/mounts, which is world-readable, and NOT with# `test -d /sys/kernel/tracing/events`. That was the first version, and it is# wrong in the direction that matters: tracefs is 0700 root:root, so descending# into it as an unprivileged user fails on a host where it is perfectly mounted.# A check that reports FAIL when it could not look is the failure this whole# script exists to prevent.if [ -r /proc/mounts ]; then mounts=$(cat /proc/mounts) if echo "$mounts" | grep -qE '^tracefs +/sys/kernel/tracing '; then pass "tracefs mounted (/sys/kernel/tracing)" elif echo "$mounts" | grep -qE '^(debugfs|tracefs) +/sys/kernel/debug '; then pass "tracefs reachable under /sys/kernel/debug/tracing" else fail "tracefs mounted" \ "no tracefs in /proc/mounts. Collectors would load, attach nothing, and the capture would come back missing axes instead of empty." fielse unknown "tracefs mounted" "could not read /proc/mounts from here"fi
# ── 5. The five capabilities ──────────────────────────────────────────────────# What this shell holds is NOT the answer. The Profiler runs as its own# workload, and whether the five can be granted to it is a policy question for# whoever owns the cluster (PodSecurity / PSP / the container runtime). So this# reports and does not decide.caps=$(grep -m1 '^CapEff:' /proc/self/status 2>/dev/null | awk '{print $2}')if [ -n "${caps:-}" ]; then unknown "CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, CAP_SYS_ADMIN grantable" \ "not decidable from this shell (it holds CapEff=$caps). Ask whoever owns the cluster whether a workload may be granted all five. All five are required: the Profiler fails boot naming the one that is missing, and there is no reduced mode."else unknown "the five capabilities grantable" \ "could not read /proc/self/status. Ask whoever owns the cluster whether a workload may be granted all five."fi
# ── 6. Confinement profile ────────────────────────────────────────────────────# Measured, not theorised: Docker's default seccomp/AppArmor profile denies# ptrace against an unconfined target, and that alone kills two collectors.lsm=$(cat /proc/self/attr/current 2>/dev/null | tr -d '\0')case "${lsm:-}" in ''|unconfined) pass "no restrictive LSM profile on this shell (${lsm:-none})" ;; *) unknown "confinement profile" \ "this shell runs under '$lsm'. Docker's default profile denies ptrace against an unconfined target, which silently costs two collectors. The Profiler needs its own profile decided, not inherited." ;;esac
# ── 7. Questions this script cannot answer ────────────────────────────────────printf '\nNot decidable from a host, and not optional:\n'printf ' • Does your support contract with the vendor of the observed software\n'printf ' ALLOW attaching a privileged collector to it? Doing so can void support.\n'printf ' This is a legal question, and it comes before the technical ones.\n'printf ' • Can the Profiler run with hostPID, so it can see the target process?\n'printf ' • Which processes go on the target list? It is required, it is yours,\n'printf ' and there is no default that means "everything on the node".\n'
printf '\n%d passed, %d failed, %d unknown\n' "$PASS" "$FAIL" "$UNKNOWN"
if [ "$FAIL" -gt 0 ]; then printf 'VERDICT: this host does NOT qualify yet.\n' exit 1fiif [ "$UNKNOWN" -gt 0 ]; then printf 'VERDICT: no gate failed, and %d item(s) could not be decided from here.\n' "$UNKNOWN" printf 'Not a pass. Answer them before the POC, not during it.\n' exit 2fiprintf 'VERDICT: this host qualifies.\n'exit 0O que ele precisa
Seção intitulada “O que ele precisa”| Permissão | Para quê |
|---|---|
CAP_BPF |
Carregar os programas eBPF que contam chamadas de sistema, esperas por lock, alocações e eventos de rede. |
CAP_PERFMON |
Amostrar CPU por perf_event, a 100 Hz por núcleo. |
CAP_SYS_PTRACE |
Anexar os programas acima a um processo que roda como outro usuário (ler /proc/<pid>/ns/* e /proc/<pid>/exe). Sem ela o eBPF carrega e nenhum coletor anexa: a captura sai vazia, que é indistinguível de “nada mudou neste deploy”. |
CAP_DAC_READ_SEARCH |
Ler /sys/kernel/tracing (que é 0700 root:root) e o próprio /proc/self/mem do coletor. A segunda parece estranha e não é: um binário que recebe capability por file capability (que é como este evita rodar como root) passa a ter o próprio /proc/self/* pertencendo ao root. Sem ela caem os tracepoints e a detecção de versão do kernel, ou seja sete dos doze coletores. |
CAP_SYS_ADMIN |
Criar o perf_uprobe do probe de heap, o eixo que carrega func e file:line. É root-equivalente num container com hostPID. Leia o bloco abaixo antes de aprovar. |
hostPID ou shareProcessNamespace |
Enxergar o processo-alvo. Sem isso ele só enxerga a si mesmo. |
/sys/kernel/tracing e /sys/kernel/debug montados |
Onde a biblioteca de eBPF procura os tracepoints. Sem eles o coletor carrega e não anexa, e a captura sai com eixos faltando em vez de vazia. |
| Perfil AppArmor próprio | O docker-default só permite ptrace entre containers do mesmo perfil, e o alvo normalmente não é um. Publicamos um perfil que é o docker-default mais uma linha (ptrace (read) peer=unconfined), e não apparmor=unconfined, que desfaria o confinamento inteiro por causa de uma regra. |
get · list · watch em pods |
Ainda não pedida. Será como ele percebe um deploy quando o gatilho automático existir, e só nos namespaces da sua allowlist. Hoje o Profiler não precisa de permissão nenhuma no Kubernetes. |
| Kernel ≥ 5.8, cgroup v2 | BTF, ring buffer e CAP_BPF. Abaixo disso ele recusa subir em vez de rodar pela metade. |
| Saída de rede | Apenas o túnel do WhyStack. Nenhum outro destino. |
O que ele não pede vale tanto quanto o que pede: nenhuma permissão de escrita no Kubernetes. Em
particular, ele não pede create em pods/ephemeralcontainers, a permissão que a abordagem alternativa
exigiria, e que é genérica de um jeito desconfortável: quem pode anexar container efêmero pode anexar
qualquer imagem a qualquer pod do namespace.
Quais processos ele pode observar
Seção intitulada “Quais processos ele pode observar”Você declara a lista, e ela é obrigatória. Não existe um default que signifique “tudo o que estiver no nó”: um agente que enxerga todos os processos por omissão é privilégio acima da necessidade, e a omissão não deveria ser a forma de conceder o maior escopo possível.
A lista vive num arquivo que você implanta junto com o Profiler, por namespace e workload. Fora dela, ele não anexa, e a recusa aparece no log dele, nomeando o alvo, para que “não capturou” nunca se confunda com “capturou e não achou nada”.
Ela é diferente da denylist de tools num ponto que vale entender: a denylist é composta no control-plane e aplicada na borda; esta lista é inteiramente sua, nunca passa por nós. Ela não diz o que o WhyStack pode ler. Diz em quais processos seus alguém pode encostar.
Quando ele captura
Seção intitulada “Quando ele captura”Por uma janela curta (60 segundos por padrão) sobre um processo de cada vez. Ele não fica medindo o tempo todo.
Hoje o gatilho é explícito. Uma captura é pedida: pelo seu time durante uma investigação, ou pelo seu pipeline depois de um deploy. O Profiler recebe o pedido pela mesma conexão que já usa para tudo, valida o alvo contra a sua lista antes de olhar para qualquer processo, mede, e devolve o resultado.
As duas passam pela mesma fila, por portas diferentes. Pelo app, em Profiling, qualquer membro da
organização pede num formulário: o acesso é o mesmo que abre a página, e a decisão de owner/admin já
foi tomada antes, ao ligar o Profiler. Pelo pipeline, por uma chamada autenticada com um token
rpcap_ que um owner ou admin emite, e que só enfileira pedido de captura: ela não lê nada.
O passo a passo, os campos e os limites estão em Instalar o Profiler → Pedir uma captura. Nos dois casos o registro de capturas guarda quem pediu (a pessoa ou o pipeline, nomeados) ao lado do desfecho.
Em Docker não haverá gatilho automático, mesmo depois. Poderíamos detectar deploys ouvindo o socket do Docker, e decidimos não pedir isso: montar o socket do Docker é equivalente a root na máquina, e seria a maior permissão desta página inteira, e tudo por uma conveniência.
O que sai da sua infraestrutura
Seção intitulada “O que sai da sua infraestrutura”O que atravessa é a assinatura agregada (números e nomes, nunca o que o processo manipula):
- símbolo, arquivo e módulo do ponto do código que alocou memória;
- nome e contagem das chamadas de sistema, com latência média;
- a forma da disputa por locks, sem endereço de memória;
- contadores de rede: quantos peers, erros por tipo, latência média;
- os peers de saída, identificados por
IP:portacomo o socket os vê, até os 64 com mais tráfego, cada um com conexões, bytes e latência. É o que torna visível um deploy que trocou de dependência (parou de chamar o cache e passou a chamar o banco direto), que a contagem sozinha não mostra. Quem chama o processo vira só um número, sem endereço; - distribuições de memória e CPU;
- nome do processo, versão e ambiente;
- qual instância foi medida: o UID do pod, o id do container, ou o PID quando não há nada mais estável. Uma captura é sempre de um processo; em Kubernetes o seu workload tem várias réplicas, e sem esse identificador duas capturas de réplicas diferentes entrariam no mesmo conjunto como se fossem a mesma coisa observada duas vezes.
A assinatura não passa pela redação de PII da borda. Os peers saem com o IP do seu serviço de destino: endereço da sua infraestrutura, a mesma classe de dado que o learned-state guarda sobre os seus registros DNS (ver Redação de PII).
Símbolo e caminho de arquivo do seu código atravessam a fronteira. É a mesma classe de dado que o connector de código já lê quando você o conecta, e dizemos isso explicitamente porque não dá para redigir: apagar o nome da função destrói a única coisa que torna a assinatura útil. Se essa classe de dado não pode sair da sua fronteira, o caminho certo é não instalar o Profiler, em vez de instalá-lo e confiar numa máscara.
Nunca sai, em nenhuma configuração:
- conteúdo de requisição, resposta, log ou qualquer payload;
- texto em claro de conexões TLS: o coletor sabe capturá-lo, e a opção fica desligada e travada;
- endereço de quem chama o processo (os peers de entrada viram só uma contagem);
argve variáveis de ambiente do processo;- qualquer leitura de disco fora do
/procdo próprio alvo.
Onde a assinatura fica depois
Seção intitulada “Onde a assinatura fica depois”Ela é gravada no control-plane do WhyStack, isolada por organização como o resto do que guardamos: nenhum tenant lê a assinatura de outro, e apagar a organização apaga as assinaturas junto.
O Profiler não guarda nada. Ele mede, empurra e esquece. Não há banco no artefato privilegiado, e é por isso que reiniciá-lo não perde nem acumula nada.
Quem interpreta a assinatura é um serviço nosso que roda ao lado do control-plane. Ele recebe os documentos na requisição, computa a comparação e responde: não tem banco, não tem credencial sua, e não guarda o que recebeu.
A assinatura não alimenta nenhum acervo compartilhado entre clientes. O que o WhyStack promove entre tenants é anonimizado e categórico (papéis de serviço, tipos de métrica); assinatura comportamental carrega símbolo e caminho do seu código, e por isso ela fica onde foi gravada.
Como você escreve a lista de alvos
Seção intitulada “Como você escreve a lista de alvos”Um arquivo JSON, apontado por RUNNER_PROFILE_TARGETS_FILE, montado no container:
{ "targets": [ { "workload": "checkout-api" }, { "namespace": "producao", "workload": "pricing-engine" } ]}namespace é do Kubernetes e é opcional: em Docker não existe.
Três coisas que o Profiler recusa no boot, em vez de subir e capturar o que não devia:
- arquivo ausente: não há default que signifique “tudo o que estiver no nó”;
- lista vazia (
"targets": []): é um deployment que se acha configurado e não observa nada; "workload": "*": curinga é a omissão com outro nome.
O schema é estrito: qualquer chave que ele não conheça derruba o boot. Isso inclui um campo de comentário bem-intencionado. Se precisar explicar a lista, faça num arquivo ao lado.
Arquivo e não variável de ambiente de propósito: no seu repositório de infraestrutura ele é diffável,
entra numa revisão e alguém olha. Uma lista enterrada num values.yaml não é revisada por ninguém, e
esta é literalmente a lista de onde o container privilegiado pode encostar.
Quanto custa rodar
Seção intitulada “Quanto custa rodar”Duas coisas, e elas se comportam de formas muito diferentes.
O volume é o lado tranquilo. Cada captura produz ~30 KB: a assinatura é agregado, não evento, e o tamanho não cresce com a duração da janela.
O custo em CPU depende de uma escolha sua, e a escolha é o probe de alocação. Medimos, e a resposta honesta é uma tabela:
| Alocações por segundo do processo | Todos os probes | Sem o probe de heap |
|---|---|---|
| 11 mil | +14,8% | +0,1% |
| 114 mil | +109,9% | +0,2% |
| 1,2 milhão | +404,4% | −0,9% |
| 14,4 milhões | +3214% | +2,3% |
Tempo de CPU do processo observado por unidade de trabalho. 2 núcleos aarch64, kernel 7.0, piso de
ruído medido de ±4,9%. Dados e harness em bench/ no repositório do coletor.
Leia a segunda coluna primeiro: todo o resto é de graça, em qualquer taxa. Chamadas de sistema, contenção de lock, rede, CPU: nada disso aparece na medição. O probe de heap é o custo inteiro, e ele custa proporcionalmente ao quanto o seu processo aloca.
O probe de heap é uma compra deliberada
Seção intitulada “O probe de heap é uma compra deliberada”Ele é o eixo que carrega func e file:line, o que torna possível apontar a linha que mudou de
comportamento, e não só que o comportamento mudou. É o mais caro porque é o único que dispara uma vez
por alocação.
Capturar com ele desligado mantém chamadas de sistema, forma da contenção, rede e CPU: você continua sabendo o que mudou, e abre mão de onde. Para um serviço que aloca muito, essa é provavelmente a troca certa; para um que aloca pouco, ligá-lo custa pouco e paga bem.
A recomendação prática: ligue por serviço, não globalmente, e comece por um não-crítico para medir no seu ambiente antes de decidir.
Uma distinção que vale fazer aqui, porque as duas coisas se parecem: o custo é escolha sua, o privilégio não. Desligar o probe de heap num serviço economiza CPU dele; não muda o que o Profiler precisa para subir. As cinco permissões da tabela são exigidas de qualquer forma (inclusive de quem nunca for ligar o heap), porque é isso que mantém todas as suas capturas comparáveis entre si.
Como desligar, e como ele é ligado
Seção intitulada “Como desligar, e como ele é ligado”Ligar é uma decisão registrada, e ela não é nossa sozinha. Um Profiler só consegue um certificado da classe privilegiada se a sua organização tiver o Profiler habilitado no control-plane. Sem isso o enrollment é recusado, e a recusa é o padrão: nasce desligado. Ligar e desligar ficam no seu log de auditoria, com quem fez e quando.
A classe vive no certificado, decidida por nós contra essa política no momento do enrollment. Não é um campo que o processo declara. Um Agent não consegue se anunciar como Profiler nem por erro de configuração, e o Profiler que perde a permissão para de renovar.
Para desligar:
Pelo control-plane: desligue o Profiler nas configurações da organização. Isso não derruba o container na hora: o certificado dele vale por minutos e a renovação passa pelo mesmo gate, então ele para no fim do certificado atual. A tela diz o prazo.
Desinstalando: remova o deployment. A coleta para junto com ele, e o WhyStack continua inteiro: nenhuma outra ferramenta é afetada. As capturas que você já tem seguem legíveis, porque quem as guarda é o control-plane e não o Profiler. As ferramentas de profiling continuam na superfície do agente e passam a responder que aquele processo não tem captura.
Tirando o processo da lista de alvos: o Profiler recusa o que não está na lista e nomeia no log o alvo recusado. É o controle mais fino dos três: desliga um processo sem desligar os outros, e a lista é sua, na sua infra.
Não há caminho pelo qual nós liguemos o Profiler sozinhos. Ele é um artefato que você implanta, com uma lista de alvos que você escreve, sob uma permissão que você concede.