Vitor Bellini
Todos os posts

Monitorando um servidor do Forge com Netdata

4 min de leitura

Eu queria olhar CPU, memória e disco do meu servidor de produção sem abrir o Netdata Cloud, sem expor a porta 19999 e sem instalar mais nada pesado. O resultado foi um Netdata escutando só em localhost, um proxy no Nginx do Forge protegido por token e uma página HTML estática.

O servidor é pequeno: um Linode com 1 vCPU e 2 GB de RAM, rodando Ubuntu 24.04 e gerenciado pelo Forge.

A ideia é experimental, quando tiver mais tempo irei polir melhor e quem sabe transformar em app.

A arquitetura

Ninguém fala direto com o Netdata. Tudo passa pelo Nginx, que só deixa passar /api/ com o token certo.

flowchart LR
    B["Navegador"] -- "HTTPS + Bearer" --> CF["Cloudflare"]
    CF --> NG["Nginx do Forge"]
    NG -- "/api/* com token válido" --> ND["Netdata em 127.0.0.1:19999"]
    NG -- "página inicial /" --> HTML["index.html estático"]
    NG -- "qualquer outra rota" --> X["404"]

Três decisões sustentam o resto:

  • O Netdata não escuta na internet. Só o Nginx alcança a porta 19999.
  • O token é validado no Nginx, e o header é removido antes de chegar ao Netdata.
  • Só o prefixo /api/ e a página / existem. O resto é 404, inclusive o dashboard nativo do Netdata.

1. Instalando o Netdata

Usei o script oficial com pacotes nativos. Assim o Netdata vem do repositório apt deles e atualiza junto com o resto do sistema.

wget -O /tmp/netdata-kickstart.sh https://get.netdata.cloud/kickstart.sh
sh /tmp/netdata-kickstart.sh --stable-channel --native-only --disable-telemetry

Depois, duas mudanças. A primeira prende o servidor web do Netdata em localhost, em /etc/netdata/netdata.conf:

[web]
    bind to = 127.0.0.1:19999

A segunda desliga o Netdata Cloud, em /var/lib/netdata/cloud.d/cloud.conf:

[global]
    enabled = no

Reinicie e confirme que ele responde só localmente:

sudo systemctl restart netdata
curl -s http://127.0.0.1:19999/api/v1/info | head -c 200
ss -ltnp | grep 19999

O ss precisa mostrar 127.0.0.1:19999, e não 0.0.0.0:19999. Se mostrar 0.0.0.0, a porta está aberta para o mundo.

2. O proxy no Forge

No Forge, adicionei o domínio netdata.seudominio.com.br ao site que já existia, emiti o certificado Let's Encrypt pelo próprio painel e editei a configuração Nginx desse domínio. Na Cloudflare, o registro DNS ficou com o proxy ligado.

O token é só um valor aleatório longo:

openssl rand -hex 32

O bloco que vai dentro do server da porta 443:

root /home/forge/netdata-dash;

location ^~ /api/ {
    if ($http_authorization != "Bearer SEU_TOKEN") { return 401; }
    proxy_pass http://127.0.0.1:19999;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header Authorization "";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
}

location = / {
    try_files /index.html =404;
    add_header Cache-Control "no-cache";
}

location / {
    return 404;
}

O que cada parte faz:

  • ^~ /api/ pega tudo que começa com /api/ e impede que outra location com regex tome a frente.
  • O if compara o header inteiro com o token. if com return é o uso seguro dele no Nginx.
  • proxy_set_header Authorization "" tira o token antes de repassar. O Netdata nunca vê o segredo.
  • proxy_http_version 1.1 com Connection "" mantém conexões reaproveitadas com o Netdata, o que ajuda quando o painel faz várias chamadas por segundo.
  • location = / serve a página. location / fecha todo o resto.

Uma requisição passa por este caminho:

sequenceDiagram
    participant C as Navegador
    participant N as Nginx
    participant D as Netdata
    C->>N: GET /api/v1/data com Authorization Bearer
    alt token diferente ou ausente
        N-->>C: 401 com página HTML do Nginx
    else token certo
        N->>D: GET /api/v1/data sem Authorization
        D-->>N: JSON
        N-->>C: 200 JSON
    end

Um detalhe que custa tempo: o 401 volta em HTML, e não em JSON. Quem consome a API precisa checar o status antes de fazer o parse. Senão o erro vira um Unexpected token '<' que não diz nada.

Trocar o token

Gere um novo com openssl rand -hex 32, troque a linha do if no Forge e cole o token novo no painel. Como o valor está em um só lugar no servidor, a rotação leva um minuto.

3. O que consultar na API

Três endpoints resolvem quase tudo:

Endpoint Para quê
/api/v1/info versão, sistema, núcleos, RAM total. Chame uma vez.
/api/v1/data?chart=... série temporal de um chart. É o endpoint principal.
/api/v1/alarms?active alertas em WARNING ou CRITICAL.

O valor atual de um chart é a média dos últimos 5 segundos em um ponto:

/api/v1/data?chart=system.cpu&after=-5&points=1&group=average&format=json&options=abs

As contas que uso:

CPU usada   = soma de todas as dimensões de system.cpu
RAM usada % = system.ram.used / soma de system.ram * 100
Disco %     = used / (avail + used + reserved for root) * 100
Load %      = system.load.load1 / núcleos * 100

As armadilhas que encontrei:

  • Números como texto. No /info, cores_total e ram_total vêm como string.
  • Valores negativos. Em rede e disco, "enviado" e "escrita" vêm negativos. options=abs resolve.
  • Ordem invertida. O ponto mais novo vem primeiro. Use options=flip ou ordene no cliente.
  • Nomes que mudam entre versões. No Netdata v2, system.io usa as dimensões reads e writes, e não mais in e out. Meu gráfico de disco ficou vazio até eu perceber.
  • Uma dimensão chamada time. O chart de retenção do banco devolve ["time", "space", "time"]. Um labels.indexOf("time") acha a coluna do horário, e não a dimensão. Procure a partir da coluna 1.

5. A página HTML

O painel é um único index.html, sem build e sem dependências além de uma fonte do Google Fonts. Ele pede o token uma vez, guarda no localStorage do navegador e faz as chamadas com fetch.

Algumas escolhas deixam ele leve:

  • As chamadas de cada ciclo saem em paralelo, com Promise.all. Cada /data leva dezenas de milissegundos.
  • O polling pausa quando a aba fica oculta.
  • Charts que não existem no servidor não derrubam a página. Eles aparecem como indisponíveis, e o resto continua.
  • O topo responde a pergunta que importa ("está saudável", "sob pressão", "precisa de atenção") antes de qualquer gráfico.

O deploy é uma cópia de arquivo:

scp index.html servidor:/home/forge/netdata-dash/index.html

Guardar o token no localStorage é uma troca consciente. Para um painel pessoal, acessado só por mim, é suficiente. Para um time, eu colocaria o domínio atrás do Cloudflare Access e tiraria o token do navegador.

6. Quanto custa monitorar

O Netdata mede a si mesmo pelo plugin de apps, nos charts app.netdata_*. No meu servidor, com 1 vCPU e 2 GB:

Recurso Uso do Netdata
CPU cerca de 1,2% a 1,5% do vCPU
Memória cerca de 112 MiB de RSS
Escrita em disco cerca de 2 KiB/s
API cerca de 2,4 requisições por segundo, respondendo em 1,4 ms

O histórico fica num banco próprio, em três camadas. Cada uma tem um limite de disco e de tempo, e vale o que vier primeiro:

Camada Resolução Limite padrão
tier 0 1 ponto por segundo 1 GiB ou 14 dias
tier 1 1 ponto por minuto 1 GiB ou 3 meses
tier 2 1 ponto por hora 1 GiB ou 2 anos

Em dois dias, o tier 0 ocupou uns 225 MiB. Nesse ritmo, o limite de 1 GiB segura cerca de 10 dias de histórico por segundo. Quando enche, o Netdata apaga sozinho o mais antigo. O disco nunca lota por causa dele, e o histórico por minuto e por hora continua.

Resumo

  • Netdata em 127.0.0.1, com Cloud e telemetria desligados.
  • Nginx do Forge como única porta de entrada, com token no header e o header removido antes do Netdata.
  • Tudo fora de /api/ e / responde 404.
  • Certificado e Nginx só pelo painel do Forge.
  • O painel checa o status antes do parse, tratam null como "sem valor" e não confiam em nomes de dimensão entre versões.

O custo total ficou em pouco mais de 1% de CPU e uns 100 MiB de memória. Em troca, uma página me diz em um segundo se o servidor está bem.

1 curtida