Monitoring: métricas com Prometheus node exporter

1. Introdução ao Monitoramento com Prometheus e Node Exporter

O ecossistema Prometheus é uma solução open-source de monitoramento que coleta métricas de sistemas e aplicações, armazena-as em um banco de dados time-series e permite a criação de alertas baseados em regras. O Node Exporter é um agente oficial que expõe métricas do sistema operacional (CPU, memória, disco, rede) via HTTP no formato texto do Prometheus.

Para aplicações escritas em Linguagem C, o monitoramento nativo é essencial quando se deseja expor métricas internas do processo — como número de requisições processadas, memória alocada, tempo de execução ou estado de conexões. Instrumentar manualmente sua aplicação C com métricas customizadas permite que você integre dados específicos do seu software ao pipeline do Prometheus, indo além das métricas genéricas de sistema.

2. Instalação e Configuração do Node Exporter

O Node Exporter pode ser baixado como binário pré-compilado da página de releases do projeto no GitHub ou compilado a partir do código fonte.

Instalação via binário (Linux x86_64):

wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xvf node_exporter-1.8.2.linux-amd64.tar.gz
sudo cp node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/

Para executar como serviço systemd, crie o arquivo /etc/systemd/system/node_exporter.service:

[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
ExecStart=/usr/local/bin/node_exporter \
    --collector.textfile.directory=/var/lib/node_exporter/textfile \
    --web.listen-address=:9100
Restart=always

[Install]
WantedBy=multi-user.target

Ative e inicie o serviço:

sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter

Verifique a coleta básica:

curl http://localhost:9100/metrics | head -20

3. Formatando Métricas no Padrão Prometheus

O formato text exposition do Prometheus segue uma estrutura clara: cada métrica possui seções HELP (descrição), TYPE (tipo) e linhas de valor. Os tipos principais são:
- counter: valor que só aumenta (ex: requisições totais)
- gauge: valor que pode subir e descer (ex: memória atual)
- histogram: distribuição de valores (ex: latência)
- summary: quantis calculados no cliente

Exemplo de função em C para escrever uma métrica gauge formatada:

#include <stdio.h>
#include <string.h>

void write_gauge_metric(char *buffer, size_t size,
                        const char *name, const char *help,
                        double value, const char *labels) {
    snprintf(buffer, size,
             "# HELP %s %s\n"
             "# TYPE %s gauge\n"
             "%s{%s} %.6f\n",
             name, help, name, name, labels, value);
}

// Uso:
char buf[512];
write_gauge_metric(buf, sizeof(buf),
                   "myapp_memory_rss_bytes",
                   "Resident Set Size in bytes",
                   12345678.0, "pid=\"1234\"");
printf("%s", buf);

Saída gerada:

# HELP myapp_memory_rss_bytes Resident Set Size in bytes
# TYPE myapp_memory_rss_bytes gauge
myapp_memory_rss_bytes{pid="1234"} 12345678.000000

4. Criando Métricas Customizadas em C

Para coletar métricas do próprio processo, podemos ler arquivos do sistema de arquivos /proc. Exemplo de leitura de RSS (memória residente) em bytes:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

long get_rss_bytes() {
    FILE *fp = fopen("/proc/self/status", "r");
    if (!fp) return -1;
    char line[256];
    long rss = -1;
    while (fgets(line, sizeof(line), fp)) {
        if (sscanf(line, "VmRSS: %ld kB", &rss) == 1) {
            rss *= 1024; // converte para bytes
            break;
        }
    }
    fclose(fp);
    return rss;
}

Exemplo de contador de requisições e gauge de conexões ativas:

#include <pthread.h>

static pthread_mutex_t metrics_lock = PTHREAD_MUTEX_INITIALIZER;
static long request_counter = 0;
static int active_connections = 0;

void increment_requests() {
    pthread_mutex_lock(&metrics_lock);
    request_counter++;
    pthread_mutex_unlock(&metrics_lock);
}

void set_active_connections(int n) {
    pthread_mutex_lock(&metrics_lock);
    active_connections = n;
    pthread_mutex_unlock(&metrics_lock);
}

void build_metrics(char *buffer, size_t size) {
    pthread_mutex_lock(&metrics_lock);
    long reqs = request_counter;
    int conns = active_connections;
    pthread_mutex_unlock(&metrics_lock);

    char tmp[1024];
    snprintf(buffer, size,
             "# HELP myapp_requests_total Total requests processed\n"
             "# TYPE myapp_requests_total counter\n"
             "myapp_requests_total %ld\n"
             "# HELP myapp_active_connections Current active connections\n"
             "# TYPE myapp_active_connections gauge\n"
             "myapp_active_connections %d\n",
             reqs, conns);
}

5. Expondo Métricas via Servidor HTTP Embutido

Para expor as métricas, implementamos um servidor HTTP mínimo que responde na rota /metrics. Exemplo simplificado:

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>

#define PORT 9090
#define BUFFER_SIZE 4096

void handle_client(int client_fd) {
    char request[1024];
    read(client_fd, request, sizeof(request) - 1);

    char response[BUFFER_SIZE];
    char metrics[2048];
    build_metrics(metrics, sizeof(metrics));

    snprintf(response, sizeof(response),
             "HTTP/1.1 200 OK\r\n"
             "Content-Type: text/plain; charset=utf-8\r\n"
             "Content-Length: %zu\r\n\r\n%s",
             strlen(metrics), metrics);

    write(client_fd, response, strlen(response));
    close(client_fd);
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT)
    };
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 5);

    while (1) {
        int client = accept(server_fd, NULL, NULL);
        handle_client(client);
    }
    close(server_fd);
    return 0;
}

Atenção: este servidor é sequencial e não trata concorrência. Para produção, use threads ou um pool de conexões.

6. Integração com Node Exporter via Textfile Collector

O Node Exporter possui o collector textfile, que lê arquivos .prom de um diretório configurado. Isso permite que sua aplicação C escreva métricas periodicamente sem precisar rodar um servidor HTTP.

Exemplo de escrita segura com rename atômico:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

void write_metrics_file(const char *dir) {
    char tmp_path[512], final_path[512];
    snprintf(tmp_path, sizeof(tmp_path), "%s/metrics.tmp", dir);
    snprintf(final_path, sizeof(final_path), "%s/metrics.prom", dir);

    FILE *fp = fopen(tmp_path, "w");
    if (!fp) return;

    char metrics[2048];
    build_metrics(metrics, sizeof(metrics));
    fputs(metrics, fp);
    fclose(fp);

    rename(tmp_path, final_path); // atômico no mesmo filesystem
}

Chame essa função periodicamente usando setitimer():

#include <sys/time.h>

void timer_handler(int sig) {
    write_metrics_file("/var/lib/node_exporter/textfile");
}

int main() {
    signal(SIGALRM, timer_handler);
    struct itimerval timer = {
        .it_interval = { .tv_sec = 15, .tv_usec = 0 },
        .it_value    = { .tv_sec = 15, .tv_usec = 0 }
    };
    setitimer(ITIMER_REAL, &timer, NULL);
    // ... resto da aplicação
}

7. Testando e Validando as Métricas

Teste manual com curl:

curl http://localhost:9090/metrics

Valide o formato com promtool:

promtool check metrics < metrics.txt

Configure o Prometheus para fazer scrape da sua aplicação. Adicione ao prometheus.yml:

scrape_configs:
  - job_name: 'myapp_c'
    static_configs:
      - targets: ['localhost:9090']

No Grafana, crie um dashboard simples com consultas como myapp_requests_total e myapp_active_connections.

8. Boas Práticas e Segurança

  • Bind em localhost: configure o servidor para escutar apenas em 127.0.0.1 e use o Node Exporter como proxy reverso, se necessário.
  • Firewall: bloqueie portas de métricas com iptables ou ufw.
  • Evite informações sensíveis: não coloque senhas, tokens ou dados pessoais em labels.
  • Cache de métricas: evite ler /proc a cada requisição HTTP. Atualize as métricas em intervalo fixo (ex: a cada 15 segundos) e sirva o buffer cacheado.
  • Redução de syscalls: agrupe leituras de /proc em uma única chamada por ciclo.

Referências