Observabilidade
O harness-sphere é o watcher da stack. Este capítulo diz o que ele observa, o que ele mostra para você, como rodá-lo com e sem um backend de métricas, e as duas regras sobre ele que um operador não pode quebrar sem querer.
O que ele é, e o que ele não é
Antes deste serviço existir, a stack não emitia nada — não “pouco”, mas nada: nenhum endpoint de métricas, nenhum coletor, nenhum dashboard. A primeira pergunta que qualquer um faz durante um incidente, a máquina estava sob pressão quando aquilo ficou lento?, não tinha resposta.
O harness-sphere é um único binário Rust que transforma o que encontra em métricas OpenTelemetry padrão e as envia para o backend que você apontar. Ele é exclusivo desta stack, pela primeira linha do seu próprio README: coletores sem fonte aqui foram apagados em vez de deixados configuráveis.
Três coisas que ele deliberadamente nunca faz, e cada uma vale saber antes de você sair procurando o recurso:
- Ele não segura um socket do Docker. Veja a seção de regras abaixo.
- Ele não reporta custo de token. O picoclaw não escreve contagens de token em disco e não expõe endpoint de métricas; o único caminho que já existiu raspava um endpoint que esta stack não roda, e ele foi apagado. Isso não é trabalho adiado.
- Ele não lê conteúdo de transcrição. Corpos de mensagem são dados do membro. Ele conta mensagens, sessões e chamadas de ferramenta;
contentnem chega a ser desserializado.
O que ele observa
A stack é modelada como exatamente seis camadas: o host, o próprio watcher, e as quatro coisas que a máquina roda. Duas delas — Host e Watcher — são Critical, ou seja, o watcher sai com código diferente de zero em vez de rodar cego sem elas. Todo o resto é Optional: se estiver faltando ou se comportando mal, ele sai de lado em silêncio e nunca derruba o processo.
O host. Utilização de CPU, uso e utilização de memória, e swap. Esses são os números da máquina real, não os do container, e isso foi medido e não presumido — o Linux não coloca /proc em namespace, então o sysinfo dentro do container lê o host. Um container limitado a -m 512m ainda reportava os 33 GB do host. É exatamente por isso que o serviço não monta /proc nem /sys.
Ele mesmo. A sua própria CPU, memória residente e memória virtual. Um watcher cuja memória cresce sem limite é um watcher prestes a virar o incidente.
Três endpoints de serviço, por sonda TCP. O crab/harness-sphere/config.zombie-crab.toml lista os três pelo nome de serviço do compose, cada um marcado com a camada a que pertence: mycelium-gateway:8080 (gateway), crab-shell-proxy:8080 (proxy) e chat-webapp:3000 (webapp). Note o terceiro: o repositório se chama crab-exoskeleton-webapp, mas chat-webapp é o serviço do compose e portanto o único nome que resolve na zombie_net.
Um alvo que está fora do ar lê um 0 honesto em vez de sumir. É também por isso que o serviço não declara nenhum depends_on — colocar o watcher atrás da saúde daquilo que ele observa suprimiria justamente o sinal que ele existe para produzir.
Atividade de IA por membro, a partir da árvore de workspaces. Ele faz um glob de tenants/*/subscriptions/*/agents/*/users/* sob o mount somente-leitura /data e roda um coletor de sessão por tupla (tenant, subscription, agent, user), cada um carimbado com aquela tupla. A partir das transcrições JSONL em disco ele deriva contagens de mensagens por papel, contagens de conversas, contagens de chamadas de ferramenta, e execuções de tarefas agendadas contadas à parte. Quatro distorções são corrigidas, cada uma com um teste por trás: conversas de projeto vivem em um workspace-<id>/sessions irmão, sessions/durable/ espelha os arquivos vivos um a um e dobraria exatamente todos os números, cada execução de cron escreve o seu próprio arquivo de sessão, e uma transcrição que encolheu é relida do zero em vez de tratada como um delta negativo.
Os intervalos estão no mesmo arquivo de configuração: descoberta a cada 30 s, sessões a cada 60 s, material de aprendizado a cada 300 s, host a cada 10 s, ele mesmo a cada 30 s.
Estes são Gauges, não Counters. O coletor reporta o total absoluto que encontra em disco, derivado de novo a cada coleta, então passá-lo pelo
add()de um Counter contaria tudo em dobro a cada tick. A consequência a esperar é que um valor pode legitimamente cair quando transcrições são rotacionadas para fora.
O que ele ainda não consegue ver
O watcher lê a árvore de tenants em disco. Ele ainda não consome o inventário vivo GET /v1/instances do crab-shell-proxy, então ele consegue dizer que o workspace de um membro existe e quanta atividade há nele, mas não se o container daquele membro está rodando agora. Liveness por instância e CPU e memória por container dependem os dois desse inventário. O harness-sphere nunca calcula o hash do nome do container por conta própria, porque isso duplicaria uma preimagem que pertence ao proxy e divergiria em silêncio no dia em que o prefixo ou o hash mudar.
Como rodar
O watcher já está no arquivo compose base, então ele sobe junto com a stack. De fábrica o seu exportador é stdout: ele imprime os seus sinais no log do próprio container. Esse é um padrão deliberado — ele prova o pipeline inteiro sem precisar levantar backend nenhum.
docker compose logs -f harness-sphere
Para de fato olhar números ao longo do tempo, suba o overlay opcional, que acrescenta um OpenTelemetry Collector, Prometheus e Grafana:
docker compose -f docker-compose.yaml -f docker-compose.observability.yaml up -d
O Grafana fica então em http://localhost:3001 (GRAFANA_PORT), com acesso anônimo de admin e sem formulário de login — é um backend local em uma porta de loopback, e um prompt de login na frente dos seus próprios gráficos de CPU não tem modelo de ameaça por trás. Se algum dia você expuser isso para fora do localhost, aquelas três linhas GF_AUTH_* são a primeira coisa a remover. O Prometheus é publicado na 9090 para você rodar uma consulta PromQL crua quando um dashboard discordar do que você espera.
Dois dashboards são provisionados a partir de deploy/observability/grafana/dashboards/, junto com o datasource do Prometheus, então a stack já é útil no primeiro boot, sem nenhum clique: o zombie-crab — stack, organizado por camada, responde “a stack está saudável?”, e o zombie-crab — learning, organizado por membro, responde “onde está cada instância?”.
Consulte os nomes com underscore. A tradução de OTLP para Prometheus troca pontos por underscores e acrescenta um sufixo de unidade, então o que o watcher emite como
system.memory.usageé coletado comosystem_memory_usage_bytes. Declare os instrumentos com os nomes com ponto; consulte com os traduzidos.
Por que existe um collector no meio
O exportador OTLP do harness-sphere é construído com .with_tonic(), então ele fala OTLP sobre gRPC, enquanto o receptor OTLP do próprio Prometheus só fala HTTP. Os dois não podem ser ligados diretamente. O collector recebe gRPC na 4317 e reexpõe tudo em formato de exposição do Prometheus na 8889, que é o único alvo que o deploy/observability/prometheus.yml coleta.
O receptor HTTP dele na 4318 está ali por causa de um segundo produtor: o crab-ganglion-harness fala OTLP sobre HTTP com codificação JSON, o que ele consegue fazer só com a biblioteca padrão do Go, e o arquivo compose passa GANGLION_OTLP_ENDPOINT (padrão http://otel-collector:4318) pelo proxy para dentro de cada container de agente. Deixá-lo vazio desliga a exportação dentro do harness, em vez de fazê-lo logar uma requisição falha a cada turno.
O pipeline é só de métricas. O harness-sphere sobe com
sources=3 receivers=0— sem traces, sem logs — que é também por que a stack SigNoz vendorizada emcrab/harness-sphere/deploy/signoz/não é o que este overlay usa: seis serviços, incluindo ClickHouse e Zookeeper, dimensionados para um sinal que ainda não existe.
Dois ajustes que vão te morder
A cadeia de -f não é opcional em nenhum comando. O Compose aplica um overlay só quando você o nomeia. Rode docker compose up -d, ou restart, ou up -d harness-sphere sem os dois arquivos, e todo serviço que o overlay sobrescreve cai silenciosamente de volta para o arquivo base — o exportador volta para stdout e o bind-mount da configuração some. Nada avisa disso. Todo container continua saudável, o watcher continua coletando, os logs dele parecem movimentados, e o Grafana simplesmente fica vazio. Coloque isto no .env da raiz do repositório e um docker compose up -d pelado passa a estar correto:
COMPOSE_FILE=docker-compose.yaml:docker-compose.observability.yaml
Defina HARNESS_SPHERE_HOST_NAME com o nome da máquina real. O watcher lê o seu hostname para preencher o atributo de recurso host.name, que vira um label em toda série. O hostname de um container por padrão é o id dele, que muda a cada recriação — então todo redeploy criaria uma série temporal novinha e cada painel mostraria a mesma métrica uma vez por geração de container. O padrão do compose (zombie-crab-host) é estável em vez de exato, que é a metade importante: um label errado mas constante agrupa certo, um label certo mas mutável nunca agrupa.
As duas regras
Estas são as regras do .claude/CLAUDE.md, e elas estão declaradas aqui porque um operador que não entende por quê vai quebrar uma delas tentando fazer alguma coisa funcionar.
Ele nunca recebe um socket do Docker
O crab-shell-proxy já monta /var/run/docker.sock e roda como root. Ele é o plano de controle confiável da stack, e é o componente mais privilegiado que existe: um socket do Docker é start, stop e exec em qualquer container, e um caminho até root no host.
Um segundo serviço montando o socket dobraria o raio de destruição do pior comprometimento possível da stack, sem ganho nenhum — porque a única coisa para a qual o watcher precisaria de um socket, mapear um container em execução de volta para o seu tenant, é atendida pelo GET /v1/instances somente-leitura do proxy.
O harness-sphere de fato roda como root neste arquivo compose (user: "0:0"), o que sobrescreve o USER 10001:10001 que o Dockerfile dele define, e essa decisão é defendida no lugar em vez de presumida: o crab-shell-proxy cria a árvore de tenants como root:root 0700, e a barreira está no topo da árvore, então um watcher que não é root não consegue nem atravessar para dentro dela. As alternativas eram piores — um chmod 0755 deixaria todo caminho de workspace enumerável por qualquer uid local, e os nomes dos diretórios são UUIDs de conta.
O que mantém isso estreito são três restrições que sustentam o peso juntas, e uma mudança futura não pode afrouxá-las uma de cada vez:
- o bind de
/datacontinua:ro— ele pode ler a árvore, nunca escrever nela; - nenhum socket do Docker, jamais;
- nenhuma porta publicada, então não há superfície de entrada nenhuma. Todo coletor puxa.
CRAB_TELEMETRY_TOKEN não é um token de agente
O CRAB_TELEMETRY_TOKEN autoriza GET /v1/instances no crab-shell-proxy, e nada mais. Ele é uma credencial separada e somente-leitura de propósito, e o bearer token de um agente nunca pode ser usado no lugar dele.
A razão é o que um token de agente vale de fato aqui. O header de perfil do Mycelium é decodificado e nunca verificado pelo proxy (identity.SDKResolver.Resolve), então a checagem do bearer do agente é a única coisa que impede um chamador que alcançou o proxy diretamente na zombie_net de afirmar qualquer id de conta que quiser. Esse token libera conversar como qualquer membro de qualquer tenant. Entregá-lo a um componente de monitoramento deixaria esse componente mandar mensagens como outras pessoas. O handleInstances deliberadamente não passa pela resolução normal de agente pelo mesmo motivo, e compara o token em tempo constante.
Deixá-lo sem valor é suportado e é o padrão seguro. A rota então não é registrada de jeito nenhum — um 404, não um 401. Isso não é descuido: este endpoint revela toda a topologia de tenants, subscriptions e usuários do deploy, então um deploy que não optou por isso não ganha superfície nova. O watcher continua reportando o host, ele mesmo e as três sondas de serviço; ele só não consegue atribuir instâncias por tenant.
Gere um novo com openssl rand -hex 32, como dizem tanto o deploy/standalone/.env.example quanto o deploy/prod/.env.example.
Para onde ir agora
Solução de problemas cobre a falha do Grafana vazio e outras em forma de sintoma. harness-sphere descreve o componente em si, e Deploy cobre os modos de compose sobre os quais este overlay assenta.