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