Index: etc/inetd.conf =================================================================== --- etc/inetd.conf +++ etc/inetd.conf @@ -117,3 +117,7 @@ #netbios-ssn stream tcp nowait root /usr/local/sbin/smbd smbd #netbios-ns dgram udp wait root /usr/local/sbin/nmbd nmbd #swat stream tcp nowait/400 root /usr/local/sbin/swat swat +# +# Example entry for the Prometheus sysctl metrics exporter +# +#prom-sysctl stream tcp nowait nobody /usr/sbin/prometheus_sysctl_exporter prometheus_sysctl_exporter -dh Index: etc/services =================================================================== --- etc/services +++ etc/services @@ -2450,6 +2450,7 @@ aurora 9084/tcp #IBM AURORA Performance Visualizer aurora 9084/udp #IBM AURORA Performance Visualizer jetdirect 9100/tcp #HP JetDirect card +prom-sysctl 9124/tcp #prometheus_sysctl_exporter(8) git 9418/tcp #git pack transfer service git 9418/udp #git pack transfer service man 9535/tcp Index: sbin/sysctl/sysctl.8 =================================================================== --- sbin/sysctl/sysctl.8 +++ sbin/sysctl/sysctl.8 @@ -28,7 +28,7 @@ .\" From: @(#)sysctl.8 8.1 (Berkeley) 6/6/93 .\" $FreeBSD$ .\" -.Dd December 10, 2015 +.Dd December 14, 2016 .Dt SYSCTL 8 .Os .Sh NAME @@ -306,7 +306,8 @@ .Xr sysctl 3 , .Xr loader.conf 5 , .Xr sysctl.conf 5 , -.Xr loader 8 +.Xr loader 8 , +.Xr prometheus_sysctl_exporter 8 .Sh HISTORY A .Nm Index: usr.sbin/Makefile =================================================================== --- usr.sbin/Makefile +++ usr.sbin/Makefile @@ -62,6 +62,7 @@ periodic \ powerd \ procctl \ + prometheus_sysctl_exporter \ pstat \ pw \ pwd_mkdb \ Index: usr.sbin/prometheus_sysctl_exporter/Makefile =================================================================== --- /dev/null +++ usr.sbin/prometheus_sysctl_exporter/Makefile @@ -0,0 +1,8 @@ +# $FreeBSD$ + +PROG= prometheus_sysctl_exporter +MAN= prometheus_sysctl_exporter.8 + +LIBADD= m + +.include Index: usr.sbin/prometheus_sysctl_exporter/prometheus_sysctl_exporter.8 =================================================================== --- /dev/null +++ usr.sbin/prometheus_sysctl_exporter/prometheus_sysctl_exporter.8 @@ -0,0 +1,104 @@ +.\" Copyright (c) 2016 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. +.\" +.\" $FreeBSD$ +.Dd December 14, 2016 +.Dt PROMETHEUS_SYSCTL_EXPORTER 8 +.Os +.Sh NAME +.Nm prometheus_sysctl_exporter +.Nd print kernel state as Prometheus metrics +.Sh SYNOPSIS +.Nm prometheus_sysctl_exporter +.Op Fl dh +.Op Ar prefix ... +.Sh DESCRIPTION +Prometheus is a monitoring system that gathers metrics from its targets +by fetching them through HTTP GET requests. +Metrics are identified by a name and an optional set of labels. +Sample values are required to be numerical. +.Pp +The +.Nm +utility prints the values of sysctl nodes to standard output, +formatted such that they can be scraped by Prometheus directly. +By default, +it prints metrics for all numerically representable nodes in the sysctl +namespace. +It is also possible to limit output to a smaller number of metrics by +specifying one or more prefixes as arguments. +.Pp +Metrics printed by this utility are named +.Ql sysctl_ , +followed by the name of the sysctl node having its +.Ql .\& +separators replaced by +.Ql _ . +Components on which it is desirable to aggregate (e.g., +names of devices) are omitted from the metric's name, +but are appended as labels instead. +.Pp +There are two different methods for exporting the output of +.Nm +to Prometheus. +The first method is to periodically invoke this utility through +.Xr cron 8 +and store its output in a textfile. +The metrics in this textfile can then be served over HTTP using the +Prometheus node exporter's textfile collector. +The second method is to run this utility through +.Xr inetd 8 . +TCP port 9124 has been allocated for this purpose. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl d +Print descriptions of metrics when available. +.It Fl h +Precede the output with a HTTP response header. +This flag is required when running this utility through +.Xr inetd 8 . +.El +.Sh SEE ALSO +.Xr cron 8 , +.Xr inetd 8 , +.Xr sysctl 8 , +.Xr SYSCTL_ADD_NODE_WITH_LABEL 9 +.Pp +Prometheus project: +.Pa https://prometheus.io/ . +.Pp +Prometheus exposition formats: +.Pa https://prometheus.io/docs/instrumenting/exposition_formats/ . +.Pp +Prometheus node exporter: +.Pa https://github.com/prometheus/node_exporter . +.Pp +Prometheus default port allocations: +.Pa https://github.com/prometheus/prometheus/wiki/Default-port-allocations . +.Sh HISTORY +.Nm +first appeared in +.Fx 12.0 . +.Sh AUTHORS +.An Nuxi : Pa https://nuxi.nl/ . Index: usr.sbin/prometheus_sysctl_exporter/prometheus_sysctl_exporter.c =================================================================== --- /dev/null +++ usr.sbin/prometheus_sysctl_exporter/prometheus_sysctl_exporter.c @@ -0,0 +1,588 @@ +/*- + * Copyright (c) 2016 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 +__FBSDID("$FreeBSD$"); + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * 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] = 1; + o->len = 1; +} + +/* Obtains the OID for a sysctl by name. */ +static void +oid_get_by_name(struct oid *o, const char *name) +{ + + o->len = sizeof(o->id) / sizeof(o->id[0]); + if (sysctlnametomib(name, o->id, &o->len) != 0) + err(1, "sysctl(%s)", name); +} + +/* 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] = 0; + lookup[1] = 2; + 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'); +} + +/* 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] = 0; + lookup[1] = 4; + 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, "%.2f", 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 + default: + return (false); + } + + /* Convert temperatures from decikelvin to degrees Celcius. */ + 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 { + int id[CTL_MAXNAME]; + char name_buf[BUFSIZ]; + size_t name_end[CTL_MAXNAME + 1]; + char label_buf[BUFSIZ]; + size_t label_end[CTL_MAXNAME]; +}; + +/* + * Initializes the OID name object with an empty value. + */ +static void +oidname_init(struct oidname *on) +{ + + on->name_end[0] = 0; +} + +/* Fetches the name of an OID, partiall reusing the previous results. */ +static void +oid_get_name(const struct oid *o, struct oidname *on) +{ + size_t i, name_start, label_start; + + /* No need to fetch components that we already have. */ + i = name_start = label_start = 0; + while (i < o->len && on->name_end[i] != 0 && on->id[i] == o->id[i]) { + name_start = on->name_end[i]; + if (on->label_end[i] != 0) + label_start = on->label_end[i]; + ++i; + } + + /* Fetch the remainder. */ + for (; i < o->len; ++i) { + int lookup[CTL_MAXNAME + 2]; + char buf[BUFSIZ]; + size_t len; + + /* Look up the name of the component. */ + lookup[0] = 0; + lookup[1] = 1; + memcpy(lookup + 2, o->id, (i + 1) * sizeof(lookup[0])); + len = sizeof(buf); + if (sysctl(lookup, 2 + i + 1, buf, &len, 0, 0) == 0) { + char *name; + + name = strrchr(buf, '.'); + if (name == NULL) { + name = buf; + } else { + ++name; + len -= name - buf; + } + + assert(name_start + len <= sizeof(on->name_buf)); + memcpy(on->name_buf + name_start, name, len); + on->name_end[i] = name_start += len; + } else { + err(1, "sysctl(name)"); + } + + /* Look up the optional label, allowing aggregation. */ + lookup[1] = 6; + len = sizeof(buf); + if (sysctl(lookup, 2 + i + 1, buf, &len, 0, 0) == 0) { + assert(label_start + len <= sizeof(on->label_buf)); + memcpy(on->label_buf + label_start, buf, len); + on->label_end[i] = label_start += len; + } else if (errno == ENOENT) { + on->label_end[i] = 0; + } else { + err(1, "sysctl(oidlabel)"); + } + } + on->name_end[o->len] = 0; +} + +/* Prints the name and labels of an OID to a file stream. */ +static void +oidname_print(const struct oidname *on, const struct oidformat *of, + FILE *fp) +{ + const char *name, *label; + size_t i; + char separator; + + /* Print the name of the metric. */ + fprintf(fp, "sysctl"); + name = on->name_buf; + for (i = 0; on->name_end[i] != 0; ++i) { + if (on->label_end[i] == 0) { + assert(name[strspn(name, + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789_")] == '\0'); + fprintf(fp, "_%s", name); + } + name = on->name_buf + on->name_end[i]; + } + if (oidformat_is_temperature(of)) + fprintf(fp, "_celcius"); + + /* Print the labels of the metric. */ + label = on->label_buf; + name = on->name_buf; + separator = '{'; + for (i = 0; on->name_end[i] != 0; ++i) { + if (on->label_end[i] != 0) { + assert(name[strspn(name, + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789_-")] == '\0'); + assert(label[strspn(label, + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789_")] == '\0'); + fprintf(fp, "%c%s=\"%s\"", separator, label, name); + separator = ','; + label = on->label_buf + on->label_end[i]; + } + name = on->name_buf + on->name_end[i]; + } + if (separator != '{') + fputc('}', fp); +} + +/* 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; on->name_end[i] != 0; ++i) + if (on->label_end[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] = 0; + lookup[1] = 5; + 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, + FILE *fp) +{ + struct oidformat of; + struct oidvalue ov; + struct oiddescription od; + + if (!oid_get_format(o, &of) || !oid_get_value(o, &of, &ov)) + return; + oid_get_name(o, on); + + /* + * 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) && + oid_get_description(o, &od)) { + fprintf(fp, "# HELP "); + oidname_print(on, &of, fp); + fputc(' ', fp); + oiddescription_print(&od, fp); + fputc('\n', fp); + } + + /* Print the line with the value. */ + oidname_print(on, &of, fp); + fputc(' ', fp); + oidvalue_print(&ov, fp); + fputc('\n', fp); +} + +static void +usage(void) +{ + + fprintf(stderr, + "usage: prometheus_sysctl_exporter [-dh] [prefix ...]\n"); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + struct oidname on; + char *http_buf; + FILE *fp; + size_t http_buflen; + int ch; + bool http_mode, print_descriptions; + + /* Parse command line flags. */ + http_mode = print_descriptions = false; + while ((ch = getopt(argc, argv, "dh")) != -1) { + switch (ch) { + case 'd': + print_descriptions = true; + break; + case 'h': + http_mode = 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, 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; + + oid_get_by_name(&root, argv[i]); + o = root; + do { + oid_print(&o, &on, print_descriptions, fp); + } while (oid_get_next(&o, &o) && + oid_is_beneath(&o, &root)); + } + } + + if (http_mode) { + if (ferror(fp) || fclose(fp) != 0) + err(1, "Cannot generate output"); + + /* Print HTTP header and metrics. */ + dprintf(STDOUT_FILENO, + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Length: %zu\r\n" + "Content-Type: text/plain; version=0.0.4\r\n" + "\r\n", + http_buflen); + write(STDOUT_FILENO, http_buf, http_buflen); + + /* Drain output. */ + if (shutdown(STDIN_FILENO, SHUT_WR) == 0) { + char buf[1024]; + + while (read(STDIN_FILENO, buf, sizeof(buf)) > 0) { + } + } + } + return (0); +}