root/usr.sbin/prometheus_sysctl_exporter/prometheus_sysctl_exporter.c
/*-
 * Copyright (c) 2016-2017 Nuxi, https://nuxi.nl/
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/param.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/sysctl.h>

#include <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <math.h>
#include <regex.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <zlib.h>

/* Regular expressions for filtering output. */
static regex_t inc_regex;
static regex_t exc_regex;

/*
 * Cursor for iterating over all of the system's sysctl OIDs.
 */
struct oid {
        int     id[CTL_MAXNAME];
        size_t  len;
};

/* Initializes the cursor to point to start of the tree. */
static void
oid_get_root(struct oid *o)
{

        o->id[0] = CTL_KERN;
        o->len = 1;
}

/* Obtains the OID for a sysctl by name. */
static bool
oid_get_by_name(struct oid *o, const char *name)
{

        o->len = nitems(o->id);
        return (sysctlnametomib(name, o->id, &o->len) == 0);
}

/* Returns whether an OID is placed below another OID. */
static bool
oid_is_beneath(struct oid *oa, struct oid *ob)
{

        return (oa->len >= ob->len &&
            memcmp(oa->id, ob->id, ob->len * sizeof(oa->id[0])) == 0);
}

/* Advances the cursor to the next OID. */
static bool
oid_get_next(const struct oid *cur, struct oid *next)
{
        int lookup[CTL_MAXNAME + 2];
        size_t nextsize;

        lookup[0] = CTL_SYSCTL;
        lookup[1] = CTL_SYSCTL_NEXT;
        memcpy(lookup + 2, cur->id, cur->len * sizeof(lookup[0]));
        nextsize = sizeof(next->id);
        if (sysctl(lookup, 2 + cur->len, &next->id, &nextsize, 0, 0) != 0) {
                if (errno == ENOENT)
                        return (false);
                err(1, "sysctl(next)");
        }
        next->len = nextsize / sizeof(next->id[0]);
        return (true);
}

/*
 * OID formatting metadata.
 */
struct oidformat {
        unsigned int    kind;
        char            format[BUFSIZ];
};

/* Returns whether the OID represents a temperature value. */
static bool
oidformat_is_temperature(const struct oidformat *of)
{

        return (of->format[0] == 'I' && of->format[1] == 'K');
}

/* Returns whether the OID represents a timeval structure. */
static bool
oidformat_is_timeval(const struct oidformat *of)
{

        return (strcmp(of->format, "S,timeval") == 0);
}

/* Fetches the formatting metadata for an OID. */
static bool
oid_get_format(const struct oid *o, struct oidformat *of)
{
        int lookup[CTL_MAXNAME + 2];
        size_t oflen;

        lookup[0] = CTL_SYSCTL;
        lookup[1] = CTL_SYSCTL_OIDFMT;
        memcpy(lookup + 2, o->id, o->len * sizeof(lookup[0]));
        oflen = sizeof(*of);
        if (sysctl(lookup, 2 + o->len, of, &oflen, 0, 0) != 0) {
                if (errno == ENOENT)
                        return (false);
                err(1, "sysctl(oidfmt)");
        }
        return (true);
}

/*
 * Container for holding the value of an OID.
 */
struct oidvalue {
        enum { SIGNED, UNSIGNED, FLOAT } type;
        union {
                intmax_t        s;
                uintmax_t       u;
                double          f;
        } value;
};

/* Extracts the value of an OID, converting it to a floating-point number. */
static double
oidvalue_get_float(const struct oidvalue *ov)
{

        switch (ov->type) {
        case SIGNED:
                return (ov->value.s);
        case UNSIGNED:
                return (ov->value.u);
        case FLOAT:
                return (ov->value.f);
        default:
                assert(0 && "Unknown value type");
        }
}

/* Sets the value of an OID as a signed integer. */
static void
oidvalue_set_signed(struct oidvalue *ov, intmax_t s)
{

        ov->type = SIGNED;
        ov->value.s = s;
}

/* Sets the value of an OID as an unsigned integer. */
static void
oidvalue_set_unsigned(struct oidvalue *ov, uintmax_t u)
{

        ov->type = UNSIGNED;
        ov->value.u = u;
}

/* Sets the value of an OID as a floating-point number. */
static void
oidvalue_set_float(struct oidvalue *ov, double f)
{

        ov->type = FLOAT;
        ov->value.f = f;
}

/* Prints the value of an OID to a file stream. */
static void
oidvalue_print(const struct oidvalue *ov, FILE *fp)
{

        switch (ov->type) {
        case SIGNED:
                fprintf(fp, "%jd", ov->value.s);
                break;
        case UNSIGNED:
                fprintf(fp, "%ju", ov->value.u);
                break;
        case FLOAT:
                switch (fpclassify(ov->value.f)) {
                case FP_INFINITE:
                        if (signbit(ov->value.f))
                                fprintf(fp, "-Inf");
                        else
                                fprintf(fp, "+Inf");
                        break;
                case FP_NAN:
                        fprintf(fp, "Nan");
                        break;
                default:
                        fprintf(fp, "%.6f", ov->value.f);
                        break;
                }
                break;
        }
}

/* Fetches the value of an OID. */
static bool
oid_get_value(const struct oid *o, const struct oidformat *of,
    struct oidvalue *ov)
{

        switch (of->kind & CTLTYPE) {
#define GET_VALUE(ctltype, type) \
        case (ctltype): {                                               \
                type value;                                             \
                size_t valuesize;                                       \
                                                                        \
                valuesize = sizeof(value);                              \
                if (sysctl(o->id, o->len, &value, &valuesize, 0, 0) != 0) \
                        return (false);                                 \
                if ((type)-1 > 0)                                       \
                        oidvalue_set_unsigned(ov, value);               \
                else                                                    \
                        oidvalue_set_signed(ov, value);                 \
                break;                                                  \
        }
        GET_VALUE(CTLTYPE_INT, int);
        GET_VALUE(CTLTYPE_UINT, unsigned int);
        GET_VALUE(CTLTYPE_LONG, long);
        GET_VALUE(CTLTYPE_ULONG, unsigned long);
        GET_VALUE(CTLTYPE_S8, int8_t);
        GET_VALUE(CTLTYPE_U8, uint8_t);
        GET_VALUE(CTLTYPE_S16, int16_t);
        GET_VALUE(CTLTYPE_U16, uint16_t);
        GET_VALUE(CTLTYPE_S32, int32_t);
        GET_VALUE(CTLTYPE_U32, uint32_t);
        GET_VALUE(CTLTYPE_S64, int64_t);
        GET_VALUE(CTLTYPE_U64, uint64_t);
#undef GET_VALUE
        case CTLTYPE_OPAQUE:
                if (oidformat_is_timeval(of)) {
                        struct timeval tv;
                        size_t tvsize;

                        tvsize = sizeof(tv);
                        if (sysctl(o->id, o->len, &tv, &tvsize, 0, 0) != 0)
                                return (false);
                        oidvalue_set_float(ov,
                            (double)tv.tv_sec + (double)tv.tv_usec / 1000000);
                        return (true);
                } else if (strcmp(of->format, "S,loadavg") == 0) {
                        struct loadavg la;
                        size_t lasize;

                        /*
                         * Only return the one minute load average, as
                         * the others can be inferred using avg_over_time().
                         */
                        lasize = sizeof(la);
                        if (sysctl(o->id, o->len, &la, &lasize, 0, 0) != 0)
                                return (false);
                        oidvalue_set_float(ov,
                            (double)la.ldavg[0] / (double)la.fscale);
                        return (true);
                }
                return (false);
        default:
                return (false);
        }

        /* Convert temperatures from decikelvin to degrees Celsius. */
        if (oidformat_is_temperature(of)) {
                double v;
                int e;

                v = oidvalue_get_float(ov);
                if (v < 0) {
                        oidvalue_set_float(ov, NAN);
                } else {
                        e = of->format[2] >= '0' && of->format[2] <= '9' ?
                            of->format[2] - '0' : 1;
                        oidvalue_set_float(ov, v / pow(10, e) - 273.15);
                }
        }
        return (true);
}

/*
 * The full name of an OID, stored as a series of components.
 */
struct oidname {
        struct oid      oid;
        char            names[BUFSIZ];
        char            labels[BUFSIZ];
};

/*
 * Initializes the OID name object with an empty value.
 */
static void
oidname_init(struct oidname *on)
{

        on->oid.len = 0;
}

/* Fetches the name and labels of an OID, reusing the previous results. */
static void
oid_get_name(const struct oid *o, struct oidname *on)
{
        int lookup[CTL_MAXNAME + 2];
        char *c, *label;
        size_t i, len;

        /* Fetch the name and split it up in separate components. */
        lookup[0] = CTL_SYSCTL;
        lookup[1] = CTL_SYSCTL_NAME;
        memcpy(lookup + 2, o->id, o->len * sizeof(lookup[0]));
        len = sizeof(on->names);
        if (sysctl(lookup, 2 + o->len, on->names, &len, 0, 0) != 0)
                err(1, "sysctl(name)");
        for (c = strchr(on->names, '.'); c != NULL; c = strchr(c + 1, '.'))
                *c = '\0';

        /* No need to fetch labels for components that we already have. */
        label = on->labels;
        for (i = 0; i < o->len && i < on->oid.len && o->id[i] == on->oid.id[i];
            ++i)
                label += strlen(label) + 1;

        /* Fetch the remaining labels. */
        lookup[1] = 6;
        for (; i < o->len; ++i) {
                len = on->labels + sizeof(on->labels) - label;
                if (sysctl(lookup, 2 + i + 1, label, &len, 0, 0) == 0) {
                        label += len;
                } else if (errno == ENOENT) {
                        *label++ = '\0';
                } else {
                        err(1, "sysctl(oidlabel)");
                }
        }
        on->oid = *o;
}

/* Populates the name and labels of an OID to a buffer. */
static void
oid_get_metric(const struct oidname *on, const struct oidformat *of,
    char *metric, size_t mlen)
{
        const char *name, *label;
        size_t i;
        char separator, buf[BUFSIZ];

        /* Print the name of the metric. */
        snprintf(metric, mlen, "%s", "sysctl");
        name = on->names;
        label = on->labels;
        for (i = 0; i < on->oid.len; ++i) {
                if (*label == '\0') {
                        strlcat(metric, "_", mlen);
                        while (*name != '\0') {
                                /* Map unsupported characters to underscores. */
                                snprintf(buf, sizeof(buf), "%c",
                                    isalnum(*name) ? *name : '_');
                                strlcat(metric, buf, mlen);
                                ++name;
                        }
                }
                name += strlen(name) + 1;
                label += strlen(label) + 1;
        }
        if (oidformat_is_temperature(of))
                strlcat(metric, "_celsius", mlen);
        else if (oidformat_is_timeval(of))
                strlcat(metric, "_seconds", mlen);

        /* Print the labels of the metric. */
        name = on->names;
        label = on->labels;
        separator = '{';
        for (i = 0; i < on->oid.len; ++i) {
                if (*label != '\0') {
                        assert(label[strspn(label,
                            "abcdefghijklmnopqrstuvwxyz"
                            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                            "0123456789_")] == '\0');
                        snprintf(buf, sizeof(buf), "%c%s=\"", separator, label);
                        strlcat(metric, buf, mlen);
                        while (*name != '\0') {
                                /* Escape backslashes and double quotes. */
                                if (*name == '\\' || *name == '"')
                                        strlcat(metric, "\\", mlen);
                                snprintf(buf, sizeof(buf), "%c", *name++);
                                strlcat(metric, buf, mlen);
                        }
                        strlcat(metric, "\"", mlen);
                        separator = ',';
                }
                name += strlen(name) + 1;
                label += strlen(label) + 1;
        }
        if (separator != '{')
                strlcat(metric, "}", mlen);
}

/* Returns whether the OID name has any labels associated to it. */
static bool
oidname_has_labels(const struct oidname *on)
{
        size_t i;

        for (i = 0; i < on->oid.len; ++i)
                if (on->labels[i] != 0)
                        return (true);
        return (false);
}

/*
 * The description of an OID.
 */
struct oiddescription {
        char description[BUFSIZ];
};

/*
 * Fetches the description of an OID.
 */
static bool
oid_get_description(const struct oid *o, struct oiddescription *od)
{
        int lookup[CTL_MAXNAME + 2];
        char *newline;
        size_t odlen;

        lookup[0] = CTL_SYSCTL;
        lookup[1] = CTL_SYSCTL_OIDDESCR;
        memcpy(lookup + 2, o->id, o->len * sizeof(lookup[0]));
        odlen = sizeof(od->description);
        if (sysctl(lookup, 2 + o->len, &od->description, &odlen, 0, 0) != 0) {
                if (errno == ENOENT)
                        return (false);
                err(1, "sysctl(oiddescr)");
        }

        newline = strchr(od->description, '\n');
        if (newline != NULL)
                *newline = '\0';

        return (*od->description != '\0');
}

/* Prints the description of an OID to a file stream. */
static void
oiddescription_print(const struct oiddescription *od, FILE *fp)
{

        fprintf(fp, "%s", od->description);
}

static void
oid_print(const struct oid *o, struct oidname *on, bool print_description,
    bool exclude, bool include, FILE *fp)
{
        struct oidformat of;
        struct oidvalue ov;
        struct oiddescription od;
        char metric[BUFSIZ];
        bool has_desc;

        if (!oid_get_format(o, &of) || !oid_get_value(o, &of, &ov))
                return;
        oid_get_name(o, on);

        oid_get_metric(on, &of, metric, sizeof(metric));

        if (exclude && regexec(&exc_regex, metric, 0, NULL, 0) == 0)
                return;

        if (include && regexec(&inc_regex, metric, 0, NULL, 0) != 0)
                return;

        has_desc = oid_get_description(o, &od);
        /*
         * Skip metrics with "(LEGACY)" in the name.  It's used by several
         * redundant ZFS sysctls whose names alias with the non-legacy versions.
         */
        if (has_desc && strnstr(od.description, "(LEGACY)", BUFSIZ) != NULL)
                return;
        /*
         * Print the line with the description. Prometheus expects a
         * single unique description for every metric, which cannot be
         * guaranteed by sysctl if labels are present. Omit the
         * description if labels are present.
         */
        if (print_description && !oidname_has_labels(on) && has_desc) {
                fprintf(fp, "# HELP ");
                fprintf(fp, "%s", metric);
                fputc(' ', fp);
                oiddescription_print(&od, fp);
                fputc('\n', fp);
        }

        /* Print the line with the value. */
        fprintf(fp, "%s", metric);
        fputc(' ', fp);
        oidvalue_print(&ov, fp);
        fputc('\n', fp);
}

/* Gzip compresses a buffer of memory. */
static bool
buf_gzip(const char *in, size_t inlen, char *out, size_t *outlen)
{
        z_stream stream = {
            .next_in    = __DECONST(unsigned char *, in),
            .avail_in   = inlen,
            .next_out   = (unsigned char *)out,
            .avail_out  = *outlen,
        };

        if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
            MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK ||
            deflate(&stream, Z_FINISH) != Z_STREAM_END) {
                return (false);
        }
        *outlen = stream.total_out;
        return (deflateEnd(&stream) == Z_OK);
}

static void
usage(void)
{

        fprintf(stderr, "%s",
            "usage: prometheus_sysctl_exporter [-dgh] [-e pattern] [-i pattern]\n"
            "\t[prefix ...]\n");
        exit(1);
}

int
main(int argc, char *argv[])
{
        struct oidname on;
        char *http_buf;
        FILE *fp;
        size_t http_buflen;
        int ch, error;
        bool exclude, include, gzip_mode, http_mode, print_descriptions;
        char errbuf[BUFSIZ];

        /* Parse command line flags. */
        include = exclude = gzip_mode = http_mode = print_descriptions = false;
        while ((ch = getopt(argc, argv, "de:ghi:")) != -1) {
                switch (ch) {
                case 'd':
                        print_descriptions = true;
                        break;
                case 'e':
                        error = regcomp(&exc_regex, optarg, REG_EXTENDED);
                        if (error != 0) {
                                regerror(error, &exc_regex, errbuf, sizeof(errbuf));
                                errx(1, "bad regular expression '%s': %s",
                                    optarg, errbuf);
                        }
                        exclude = true;
                        break;
                case 'g':
                        gzip_mode = true;
                        break;
                case 'h':
                        http_mode = true;
                        break;
                case 'i':
                        error = regcomp(&inc_regex, optarg, REG_EXTENDED);
                        if (error != 0) {
                                regerror(error, &inc_regex, errbuf, sizeof(errbuf));
                                errx(1, "bad regular expression '%s': %s",
                                    optarg, errbuf);
                        }
                        include = true;
                        break;
                default:
                        usage();
                }
        }
        argc -= optind;
        argv += optind;

        /* HTTP output: cache metrics in buffer. */
        if (http_mode) {
                fp = open_memstream(&http_buf, &http_buflen);
                if (fp == NULL)
                        err(1, "open_memstream");
        } else {
                fp = stdout;
        }

        oidname_init(&on);
        if (argc == 0) {
                struct oid o;

                /* Print all OIDs. */
                oid_get_root(&o);
                do {
                        oid_print(&o, &on, print_descriptions, exclude, include, fp);
                } while (oid_get_next(&o, &o));
        } else {
                int i;

                /* Print only trees provided as arguments. */
                for (i = 0; i < argc; ++i) {
                        struct oid o, root;

                        if (!oid_get_by_name(&root, argv[i])) {
                                /*
                                 * Ignore trees provided as arguments that
                                 * can't be found.  They might belong, for
                                 * example, to kernel modules not currently
                                 * loaded.
                                 */
                                continue;
                        }
                        o = root;
                        do {
                                oid_print(&o, &on, print_descriptions, exclude, include, fp);
                        } while (oid_get_next(&o, &o) &&
                            oid_is_beneath(&o, &root));
                }
        }

        if (http_mode) {
                const char *content_encoding = "";

                if (ferror(fp) || fclose(fp) != 0)
                        err(1, "Cannot generate output");

                /* Gzip compress the output. */
                if (gzip_mode) {
                        char *buf;
                        size_t buflen;

                        buflen = http_buflen;
                        buf = malloc(buflen);
                        if (buf == NULL)
                                err(1, "Cannot allocate compression buffer");
                        if (buf_gzip(http_buf, http_buflen, buf, &buflen)) {
                                content_encoding = "Content-Encoding: gzip\r\n";
                                free(http_buf);
                                http_buf = buf;
                                http_buflen = buflen;
                        } else {
                                free(buf);
                        }
                }

                /* Print HTTP header and metrics. */
                dprintf(STDOUT_FILENO,
                    "HTTP/1.1 200 OK\r\n"
                    "Connection: close\r\n"
                    "%s"
                    "Content-Length: %zu\r\n"
                    "Content-Type: text/plain; version=0.0.4\r\n"
                    "\r\n",
                    content_encoding, http_buflen);
                write(STDOUT_FILENO, http_buf, http_buflen);
                free(http_buf);

                /* Drain output. */
                if (shutdown(STDIN_FILENO, SHUT_WR) == 0) {
                        char buf[1024];

                        while (read(STDIN_FILENO, buf, sizeof(buf)) > 0) {
                        }
                }
        }
        return (0);
}