Create a basic stats service API using C and FastCGI

Preface

Was thinking about making my own stats page to monitor basic information for all my servers using the Primary back-end and Secondary Workers, where the Primary server will serve the front-end assets while other Worker back-ends provides API exposing the server stats.

This post is about the Secondary Worker written in C using FastCGI, other parts of the system will not be discussed here.

The FastCGI process will be expecting a maximum request rate of 1 request per 10 seconds - the downstream will be responsible to cache the information via Redis.

Deployed primary server can be viewed at stats.jixun.uk.

Environment Setup

I currently have multiple VPS running on different OS: Alpine Linux, Ubuntu and CentOS 7.

They all have their own package manager and package name conventions, I have wrote a simple script to automate it (which needs to be available):

setup_apt()
{
    apt update
    apt install -y gcc make libfcgi libfcgi-dev spawn-fcgi
}

setup_apk()
{
    apk update
    apk add gcc musl-dev make fcgi fcgi-dev spawn-fcgi
}

setup_yum()
{
    yum -y install gcc make fcgi fcgi-devel spawn-fcgi
}

which apt && setup_apt
which apk && setup_apk
which yum && setup_yum

The Code

Talk is cheap, show me the code!

FastCGI related information is available on their archived home page, where you can also find a sample code to get started.

The C code emits contents via stdout and not much string variables to escape, the code did not use any third-party library to handle the JSON.

#include "fcgi_stdio.h"

#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <sys/statvfs.h>

#define JSON_KEY(str) "\"" str "\""
#define IS_DIGIT(c) (c >= '0' && c <= '9')

// Do not use shared_buff if multi-threaded.
char shared_buff[4096];

// CPU INFO

void print_cpu()
{
    printf(JSON_KEY("cpu") ":");
    FILE* f = fopen("/proc/loadavg", "r");
    fread(shared_buff, 1024, 1, f);
    fclose(f);

    char* p = shared_buff;
    while(*p != ' ') p++;
    *p = 0;

    printf("%s", shared_buff);
}

// RAM INFO

// Ram Keyword struct
struct kwd {
    const char* word;
    int len;
};

struct kwd ramKwds[] = {
    {"MemTotal", 8},
    {"MemFree",  7},
    {"Cached",   6},

    {"SwapTotal",  9},
    {"SwapFree",   8},
    {"SwapCached", 10},
};
#define RAM_KWD_N (sizeof(ramKwds)/sizeof(ramKwds[0]))

void print_ram()
{
    printf(JSON_KEY("ram") ":{");

    FILE* f = fopen("/proc/meminfo", "r");
    fread(shared_buff, 4096, 1, f);
    fclose(f);

    int wrote = 0;
    char* p = shared_buff;
    while(*p)
    {
        char* line = p;
        while(*p && *p != '\n') p++;
        if (!*p) break;
        *p++ = 0;

        for (int i = 0; i < RAM_KWD_N; i++) {
            if (memcmp(ramKwds[i].word, line, ramKwds[i].len) == 0) {
                if (wrote) putchar(',');

                printf("\"%s\":", ramKwds[i].word);

                // skip past ':' char
                line += ramKwds[i].len + 1;

                // skip whitespace
                while(*line == ' ') {
                    line++;
                }
                while(IS_DIGIT(*line)) {
                    putchar(*line++);
                }

                wrote = 1;
            }
        }
    }
    putchar('}');
}

char* storages[] = {
    "/"
};
#define STORAGE_COUNT (sizeof(storages)/sizeof(storages[0]))

void print_storage()
{
    printf(JSON_KEY("drives") ":[");

    struct statvfs buf;
    int wrote = 0;
    for (int i = 0; i < STORAGE_COUNT; i++) {
        // see: https://github.com/php/php-src/blob/001d43444944df0bef4e33a1876ba2818c188e11/ext/standard/filestat.c#L251
        if (statvfs(storages[i], &buf)) continue;

        uint64_t block_size  = buf.f_frsize ? buf.f_frsize : buf.f_bsize;
        uint64_t bytes_free  = block_size * buf.f_bavail;
        uint64_t bytes_total = block_size * buf.f_blocks;

        if (wrote) putchar(',');

        printf("{"
            JSON_KEY("mnt")   ":\"%s\","
            JSON_KEY("free")  ":%" PRIu64 ","
            JSON_KEY("total") ":%" PRIu64 "}"
            , storages[i], bytes_free, bytes_total
        );

        wrote = 1;
    }
    putchar(']');
}

int main(void) {
    while (FCGI_Accept() >= 0) {
        printf("Content-type: application/json\r\n");
        printf("\r\n");
        putchar('{');
        print_cpu();
        putchar(',');
        print_ram();
        putchar(',');
        print_storage();
        putchar('}');
    }

    return 0;
}

编译时带上 -lfcgi 即可。我使用的编译语句如下:

gcc main.c -Wall -Wpedantic -std=c11 -lfcgi -O2 -o stats-worker.cgi

To run it...

Since nginx does not spawn fastcgi process on-demand, spawn-fcgi is required to listen on socket.

Use -n flag to not fork the process, so it can be supervised with systemd or openrc easily.

# run as root and let spawn-fcgi drop privilege later.
spawn-fcgi -n -u www-data -g www-data -f /srv/slave.cgi -s /run/slave.sock -P /run/slave.pid

This article is also available in: 中文 (简体)

Jixun

Jixun

Just some random guy talking about some random stuff.