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