1/*-
2 * Copyright (c) 2016-2017 Nuxi, https://nuxi.nl/
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23 * SUCH DAMAGE.
24 */
25
26#include <sys/param.h>
27#include <sys/resource.h>
28#include <sys/socket.h>
29#include <sys/sysctl.h>
30
31#include <assert.h>
32#include <ctype.h>
33#include <err.h>
34#include <errno.h>
35#include <math.h>
36#include <regex.h>
37#include <stdbool.h>
38#include <stdint.h>
39#include <stdio.h>
40#include <stdlib.h>
41#include <string.h>
42#include <unistd.h>
43#include <zlib.h>
44
45/* Regular expressions for filtering output. */
46static regex_t inc_regex;
47static regex_t exc_regex;
48
49/*
50 * Cursor for iterating over all of the system's sysctl OIDs.
51 */
52struct oid {
53	int	id[CTL_MAXNAME];
54	size_t	len;
55};
56
57/* Initializes the cursor to point to start of the tree. */
58static void
59oid_get_root(struct oid *o)
60{
61
62	o->id[0] = CTL_KERN;
63	o->len = 1;
64}
65
66/* Obtains the OID for a sysctl by name. */
67static bool
68oid_get_by_name(struct oid *o, const char *name)
69{
70
71	o->len = nitems(o->id);
72	return (sysctlnametomib(name, o->id, &o->len) == 0);
73}
74
75/* Returns whether an OID is placed below another OID. */
76static bool
77oid_is_beneath(struct oid *oa, struct oid *ob)
78{
79
80	return (oa->len >= ob->len &&
81	    memcmp(oa->id, ob->id, ob->len * sizeof(oa->id[0])) == 0);
82}
83
84/* Advances the cursor to the next OID. */
85static bool
86oid_get_next(const struct oid *cur, struct oid *next)
87{
88	int lookup[CTL_MAXNAME + 2];
89	size_t nextsize;
90
91	lookup[0] = CTL_SYSCTL;
92	lookup[1] = CTL_SYSCTL_NEXT;
93	memcpy(lookup + 2, cur->id, cur->len * sizeof(lookup[0]));
94	nextsize = sizeof(next->id);
95	if (sysctl(lookup, 2 + cur->len, &next->id, &nextsize, 0, 0) != 0) {
96		if (errno == ENOENT)
97			return (false);
98		err(1, "sysctl(next)");
99	}
100	next->len = nextsize / sizeof(next->id[0]);
101	return (true);
102}
103
104/*
105 * OID formatting metadata.
106 */
107struct oidformat {
108	unsigned int	kind;
109	char		format[BUFSIZ];
110};
111
112/* Returns whether the OID represents a temperature value. */
113static bool
114oidformat_is_temperature(const struct oidformat *of)
115{
116
117	return (of->format[0] == 'I' && of->format[1] == 'K');
118}
119
120/* Returns whether the OID represents a timeval structure. */
121static bool
122oidformat_is_timeval(const struct oidformat *of)
123{
124
125	return (strcmp(of->format, "S,timeval") == 0);
126}
127
128/* Fetches the formatting metadata for an OID. */
129static bool
130oid_get_format(const struct oid *o, struct oidformat *of)
131{
132	int lookup[CTL_MAXNAME + 2];
133	size_t oflen;
134
135	lookup[0] = CTL_SYSCTL;
136	lookup[1] = CTL_SYSCTL_OIDFMT;
137	memcpy(lookup + 2, o->id, o->len * sizeof(lookup[0]));
138	oflen = sizeof(*of);
139	if (sysctl(lookup, 2 + o->len, of, &oflen, 0, 0) != 0) {
140		if (errno == ENOENT)
141			return (false);
142		err(1, "sysctl(oidfmt)");
143	}
144	return (true);
145}
146
147/*
148 * Container for holding the value of an OID.
149 */
150struct oidvalue {
151	enum { SIGNED, UNSIGNED, FLOAT } type;
152	union {
153		intmax_t	s;
154		uintmax_t	u;
155		double		f;
156	} value;
157};
158
159/* Extracts the value of an OID, converting it to a floating-point number. */
160static double
161oidvalue_get_float(const struct oidvalue *ov)
162{
163
164	switch (ov->type) {
165	case SIGNED:
166		return (ov->value.s);
167	case UNSIGNED:
168		return (ov->value.u);
169	case FLOAT:
170		return (ov->value.f);
171	default:
172		assert(0 && "Unknown value type");
173	}
174}
175
176/* Sets the value of an OID as a signed integer. */
177static void
178oidvalue_set_signed(struct oidvalue *ov, intmax_t s)
179{
180
181	ov->type = SIGNED;
182	ov->value.s = s;
183}
184
185/* Sets the value of an OID as an unsigned integer. */
186static void
187oidvalue_set_unsigned(struct oidvalue *ov, uintmax_t u)
188{
189
190	ov->type = UNSIGNED;
191	ov->value.u = u;
192}
193
194/* Sets the value of an OID as a floating-point number. */
195static void
196oidvalue_set_float(struct oidvalue *ov, double f)
197{
198
199	ov->type = FLOAT;
200	ov->value.f = f;
201}
202
203/* Prints the value of an OID to a file stream. */
204static void
205oidvalue_print(const struct oidvalue *ov, FILE *fp)
206{
207
208	switch (ov->type) {
209	case SIGNED:
210		fprintf(fp, "%jd", ov->value.s);
211		break;
212	case UNSIGNED:
213		fprintf(fp, "%ju", ov->value.u);
214		break;
215	case FLOAT:
216		switch (fpclassify(ov->value.f)) {
217		case FP_INFINITE:
218			if (signbit(ov->value.f))
219				fprintf(fp, "-Inf");
220			else
221				fprintf(fp, "+Inf");
222			break;
223		case FP_NAN:
224			fprintf(fp, "Nan");
225			break;
226		default:
227			fprintf(fp, "%.6f", ov->value.f);
228			break;
229		}
230		break;
231	}
232}
233
234/* Fetches the value of an OID. */
235static bool
236oid_get_value(const struct oid *o, const struct oidformat *of,
237    struct oidvalue *ov)
238{
239
240	switch (of->kind & CTLTYPE) {
241#define	GET_VALUE(ctltype, type) \
242	case (ctltype): {						\
243		type value;						\
244		size_t valuesize;					\
245									\
246		valuesize = sizeof(value);				\
247		if (sysctl(o->id, o->len, &value, &valuesize, 0, 0) != 0) \
248			return (false);					\
249		if ((type)-1 > 0)					\
250			oidvalue_set_unsigned(ov, value);		\
251		else							\
252			oidvalue_set_signed(ov, value);			\
253		break;							\
254	}
255	GET_VALUE(CTLTYPE_INT, int);
256	GET_VALUE(CTLTYPE_UINT, unsigned int);
257	GET_VALUE(CTLTYPE_LONG, long);
258	GET_VALUE(CTLTYPE_ULONG, unsigned long);
259	GET_VALUE(CTLTYPE_S8, int8_t);
260	GET_VALUE(CTLTYPE_U8, uint8_t);
261	GET_VALUE(CTLTYPE_S16, int16_t);
262	GET_VALUE(CTLTYPE_U16, uint16_t);
263	GET_VALUE(CTLTYPE_S32, int32_t);
264	GET_VALUE(CTLTYPE_U32, uint32_t);
265	GET_VALUE(CTLTYPE_S64, int64_t);
266	GET_VALUE(CTLTYPE_U64, uint64_t);
267#undef GET_VALUE
268	case CTLTYPE_OPAQUE:
269		if (oidformat_is_timeval(of)) {
270			struct timeval tv;
271			size_t tvsize;
272
273			tvsize = sizeof(tv);
274			if (sysctl(o->id, o->len, &tv, &tvsize, 0, 0) != 0)
275				return (false);
276			oidvalue_set_float(ov,
277			    (double)tv.tv_sec + (double)tv.tv_usec / 1000000);
278			return (true);
279		} else if (strcmp(of->format, "S,loadavg") == 0) {
280			struct loadavg la;
281			size_t lasize;
282
283			/*
284			 * Only return the one minute load average, as
285			 * the others can be inferred using avg_over_time().
286			 */
287			lasize = sizeof(la);
288			if (sysctl(o->id, o->len, &la, &lasize, 0, 0) != 0)
289				return (false);
290			oidvalue_set_float(ov,
291			    (double)la.ldavg[0] / (double)la.fscale);
292			return (true);
293		}
294		return (false);
295	default:
296		return (false);
297	}
298
299	/* Convert temperatures from decikelvin to degrees Celsius. */
300	if (oidformat_is_temperature(of)) {
301		double v;
302		int e;
303
304		v = oidvalue_get_float(ov);
305		if (v < 0) {
306			oidvalue_set_float(ov, NAN);
307		} else {
308			e = of->format[2] >= '0' && of->format[2] <= '9' ?
309			    of->format[2] - '0' : 1;
310			oidvalue_set_float(ov, v / pow(10, e) - 273.15);
311		}
312	}
313	return (true);
314}
315
316/*
317 * The full name of an OID, stored as a series of components.
318 */
319struct oidname {
320	struct oid	oid;
321	char		names[BUFSIZ];
322	char		labels[BUFSIZ];
323};
324
325/*
326 * Initializes the OID name object with an empty value.
327 */
328static void
329oidname_init(struct oidname *on)
330{
331
332	on->oid.len = 0;
333}
334
335/* Fetches the name and labels of an OID, reusing the previous results. */
336static void
337oid_get_name(const struct oid *o, struct oidname *on)
338{
339	int lookup[CTL_MAXNAME + 2];
340	char *c, *label;
341	size_t i, len;
342
343	/* Fetch the name and split it up in separate components. */
344	lookup[0] = CTL_SYSCTL;
345	lookup[1] = CTL_SYSCTL_NAME;
346	memcpy(lookup + 2, o->id, o->len * sizeof(lookup[0]));
347	len = sizeof(on->names);
348	if (sysctl(lookup, 2 + o->len, on->names, &len, 0, 0) != 0)
349		err(1, "sysctl(name)");
350	for (c = strchr(on->names, '.'); c != NULL; c = strchr(c + 1, '.'))
351		*c = '\0';
352
353	/* No need to fetch labels for components that we already have. */
354	label = on->labels;
355	for (i = 0; i < o->len && i < on->oid.len && o->id[i] == on->oid.id[i];
356	    ++i)
357		label += strlen(label) + 1;
358
359	/* Fetch the remaining labels. */
360	lookup[1] = 6;
361	for (; i < o->len; ++i) {
362		len = on->labels + sizeof(on->labels) - label;
363		if (sysctl(lookup, 2 + i + 1, label, &len, 0, 0) == 0) {
364			label += len;
365		} else if (errno == ENOENT) {
366			*label++ = '\0';
367		} else {
368			err(1, "sysctl(oidlabel)");
369		}
370	}
371	on->oid = *o;
372}
373
374/* Populates the name and labels of an OID to a buffer. */
375static void
376oid_get_metric(const struct oidname *on, const struct oidformat *of,
377    char *metric, size_t mlen)
378{
379	const char *name, *label;
380	size_t i;
381	char separator, buf[BUFSIZ];
382
383	/* Print the name of the metric. */
384	snprintf(metric, mlen, "%s", "sysctl");
385	name = on->names;
386	label = on->labels;
387	for (i = 0; i < on->oid.len; ++i) {
388		if (*label == '\0') {
389			strlcat(metric, "_", mlen);
390			while (*name != '\0') {
391				/* Map unsupported characters to underscores. */
392				snprintf(buf, sizeof(buf), "%c",
393				    isalnum(*name) ? *name : '_');
394				strlcat(metric, buf, mlen);
395				++name;
396			}
397		}
398		name += strlen(name) + 1;
399		label += strlen(label) + 1;
400	}
401	if (oidformat_is_temperature(of))
402		strlcat(metric, "_celsius", mlen);
403	else if (oidformat_is_timeval(of))
404		strlcat(metric, "_seconds", mlen);
405
406	/* Print the labels of the metric. */
407	name = on->names;
408	label = on->labels;
409	separator = '{';
410	for (i = 0; i < on->oid.len; ++i) {
411		if (*label != '\0') {
412			assert(label[strspn(label,
413			    "abcdefghijklmnopqrstuvwxyz"
414			    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
415			    "0123456789_")] == '\0');
416			snprintf(buf, sizeof(buf), "%c%s=\"", separator, label);
417			strlcat(metric, buf, mlen);
418			while (*name != '\0') {
419				/* Escape backslashes and double quotes. */
420				if (*name == '\\' || *name == '"')
421					strlcat(metric, "\\", mlen);
422				snprintf(buf, sizeof(buf), "%c", *name++);
423				strlcat(metric, buf, mlen);
424			}
425			strlcat(metric, "\"", mlen);
426			separator = ',';
427		}
428		name += strlen(name) + 1;
429		label += strlen(label) + 1;
430	}
431	if (separator != '{')
432		strlcat(metric, "}", mlen);
433}
434
435/* Returns whether the OID name has any labels associated to it. */
436static bool
437oidname_has_labels(const struct oidname *on)
438{
439	size_t i;
440
441	for (i = 0; i < on->oid.len; ++i)
442		if (on->labels[i] != 0)
443			return (true);
444	return (false);
445}
446
447/*
448 * The description of an OID.
449 */
450struct oiddescription {
451	char description[BUFSIZ];
452};
453
454/*
455 * Fetches the description of an OID.
456 */
457static bool
458oid_get_description(const struct oid *o, struct oiddescription *od)
459{
460	int lookup[CTL_MAXNAME + 2];
461	char *newline;
462	size_t odlen;
463
464	lookup[0] = CTL_SYSCTL;
465	lookup[1] = CTL_SYSCTL_OIDDESCR;
466	memcpy(lookup + 2, o->id, o->len * sizeof(lookup[0]));
467	odlen = sizeof(od->description);
468	if (sysctl(lookup, 2 + o->len, &od->description, &odlen, 0, 0) != 0) {
469		if (errno == ENOENT)
470			return (false);
471		err(1, "sysctl(oiddescr)");
472	}
473
474	newline = strchr(od->description, '\n');
475	if (newline != NULL)
476		*newline = '\0';
477
478	return (*od->description != '\0');
479}
480
481/* Prints the description of an OID to a file stream. */
482static void
483oiddescription_print(const struct oiddescription *od, FILE *fp)
484{
485
486	fprintf(fp, "%s", od->description);
487}
488
489static void
490oid_print(const struct oid *o, struct oidname *on, bool print_description,
491    bool exclude, bool include, FILE *fp)
492{
493	struct oidformat of;
494	struct oidvalue ov;
495	struct oiddescription od;
496	char metric[BUFSIZ];
497	bool has_desc;
498
499	if (!oid_get_format(o, &of) || !oid_get_value(o, &of, &ov))
500		return;
501	oid_get_name(o, on);
502
503	oid_get_metric(on, &of, metric, sizeof(metric));
504
505	if (exclude && regexec(&exc_regex, metric, 0, NULL, 0) == 0)
506		return;
507
508	if (include && regexec(&inc_regex, metric, 0, NULL, 0) != 0)
509		return;
510
511	has_desc = oid_get_description(o, &od);
512	/*
513	 * Skip metrics with "(LEGACY)" in the name.  It's used by several
514	 * redundant ZFS sysctls whose names alias with the non-legacy versions.
515	 */
516	if (has_desc && strnstr(od.description, "(LEGACY)", BUFSIZ) != NULL)
517		return;
518	/*
519	 * Print the line with the description. Prometheus expects a
520	 * single unique description for every metric, which cannot be
521	 * guaranteed by sysctl if labels are present. Omit the
522	 * description if labels are present.
523	 */
524	if (print_description && !oidname_has_labels(on) && has_desc) {
525		fprintf(fp, "# HELP ");
526		fprintf(fp, "%s", metric);
527		fputc(' ', fp);
528		oiddescription_print(&od, fp);
529		fputc('\n', fp);
530	}
531
532	/* Print the line with the value. */
533	fprintf(fp, "%s", metric);
534	fputc(' ', fp);
535	oidvalue_print(&ov, fp);
536	fputc('\n', fp);
537}
538
539/* Gzip compresses a buffer of memory. */
540static bool
541buf_gzip(const char *in, size_t inlen, char *out, size_t *outlen)
542{
543	z_stream stream = {
544	    .next_in	= __DECONST(unsigned char *, in),
545	    .avail_in	= inlen,
546	    .next_out	= (unsigned char *)out,
547	    .avail_out	= *outlen,
548	};
549
550	if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
551	    MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK ||
552	    deflate(&stream, Z_FINISH) != Z_STREAM_END) {
553		return (false);
554	}
555	*outlen = stream.total_out;
556	return (deflateEnd(&stream) == Z_OK);
557}
558
559static void
560usage(void)
561{
562
563	fprintf(stderr, "%s",
564	    "usage: prometheus_sysctl_exporter [-dgh] [-e pattern] [-i pattern]\n"
565	    "\t[prefix ...]\n");
566	exit(1);
567}
568
569int
570main(int argc, char *argv[])
571{
572	struct oidname on;
573	char *http_buf;
574	FILE *fp;
575	size_t http_buflen;
576	int ch, error;
577	bool exclude, include, gzip_mode, http_mode, print_descriptions;
578	char errbuf[BUFSIZ];
579
580	/* Parse command line flags. */
581	include = exclude = gzip_mode = http_mode = print_descriptions = false;
582	while ((ch = getopt(argc, argv, "de:ghi:")) != -1) {
583		switch (ch) {
584		case 'd':
585			print_descriptions = true;
586			break;
587		case 'e':
588			error = regcomp(&exc_regex, optarg, REG_EXTENDED);
589			if (error != 0) {
590				regerror(error, &exc_regex, errbuf, sizeof(errbuf));
591				errx(1, "bad regular expression '%s': %s",
592				    optarg, errbuf);
593			}
594			exclude = true;
595			break;
596		case 'g':
597			gzip_mode = true;
598			break;
599		case 'h':
600			http_mode = true;
601			break;
602		case 'i':
603			error = regcomp(&inc_regex, optarg, REG_EXTENDED);
604			if (error != 0) {
605				regerror(error, &inc_regex, errbuf, sizeof(errbuf));
606				errx(1, "bad regular expression '%s': %s",
607				    optarg, errbuf);
608			}
609			include = true;
610			break;
611		default:
612			usage();
613		}
614	}
615	argc -= optind;
616	argv += optind;
617
618	/* HTTP output: cache metrics in buffer. */
619	if (http_mode) {
620		fp = open_memstream(&http_buf, &http_buflen);
621		if (fp == NULL)
622			err(1, "open_memstream");
623	} else {
624		fp = stdout;
625	}
626
627	oidname_init(&on);
628	if (argc == 0) {
629		struct oid o;
630
631		/* Print all OIDs. */
632		oid_get_root(&o);
633		do {
634			oid_print(&o, &on, print_descriptions, exclude, include, fp);
635		} while (oid_get_next(&o, &o));
636	} else {
637		int i;
638
639		/* Print only trees provided as arguments. */
640		for (i = 0; i < argc; ++i) {
641			struct oid o, root;
642
643			if (!oid_get_by_name(&root, argv[i])) {
644				/*
645				 * Ignore trees provided as arguments that
646				 * can't be found.  They might belong, for
647				 * example, to kernel modules not currently
648				 * loaded.
649				 */
650				continue;
651			}
652			o = root;
653			do {
654				oid_print(&o, &on, print_descriptions, exclude, include, fp);
655			} while (oid_get_next(&o, &o) &&
656			    oid_is_beneath(&o, &root));
657		}
658	}
659
660	if (http_mode) {
661		const char *content_encoding = "";
662
663		if (ferror(fp) || fclose(fp) != 0)
664			err(1, "Cannot generate output");
665
666		/* Gzip compress the output. */
667		if (gzip_mode) {
668			char *buf;
669			size_t buflen;
670
671			buflen = http_buflen;
672			buf = malloc(buflen);
673			if (buf == NULL)
674				err(1, "Cannot allocate compression buffer");
675			if (buf_gzip(http_buf, http_buflen, buf, &buflen)) {
676				content_encoding = "Content-Encoding: gzip\r\n";
677				free(http_buf);
678				http_buf = buf;
679				http_buflen = buflen;
680			} else {
681				free(buf);
682			}
683		}
684
685		/* Print HTTP header and metrics. */
686		dprintf(STDOUT_FILENO,
687		    "HTTP/1.1 200 OK\r\n"
688		    "Connection: close\r\n"
689		    "%s"
690		    "Content-Length: %zu\r\n"
691		    "Content-Type: text/plain; version=0.0.4\r\n"
692		    "\r\n",
693		    content_encoding, http_buflen);
694		write(STDOUT_FILENO, http_buf, http_buflen);
695		free(http_buf);
696
697		/* Drain output. */
698		if (shutdown(STDIN_FILENO, SHUT_WR) == 0) {
699			char buf[1024];
700
701			while (read(STDIN_FILENO, buf, sizeof(buf)) > 0) {
702			}
703		}
704	}
705	return (0);
706}
707