xref: /freebsd/usr.sbin/prometheus_sysctl_exporter/prometheus_sysctl_exporter.c (revision 2e3507c25e42292b45a5482e116d278f5515d04d)
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. */
46 static regex_t inc_regex;
47 static regex_t exc_regex;
48 
49 /*
50  * Cursor for iterating over all of the system's sysctl OIDs.
51  */
52 struct oid {
53 	int	id[CTL_MAXNAME];
54 	size_t	len;
55 };
56 
57 /* Initializes the cursor to point to start of the tree. */
58 static void
59 oid_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. */
67 static bool
68 oid_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. */
76 static bool
77 oid_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. */
85 static bool
86 oid_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  */
107 struct oidformat {
108 	unsigned int	kind;
109 	char		format[BUFSIZ];
110 };
111 
112 /* Returns whether the OID represents a temperature value. */
113 static bool
114 oidformat_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. */
121 static bool
122 oidformat_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. */
129 static bool
130 oid_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  */
150 struct 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. */
160 static double
161 oidvalue_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. */
177 static void
178 oidvalue_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. */
186 static void
187 oidvalue_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. */
195 static void
196 oidvalue_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. */
204 static void
205 oidvalue_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. */
235 static bool
236 oid_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  */
319 struct 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  */
328 static void
329 oidname_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. */
336 static void
337 oid_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. */
375 static void
376 oid_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. */
436 static bool
437 oidname_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  */
450 struct oiddescription {
451 	char description[BUFSIZ];
452 };
453 
454 /*
455  * Fetches the description of an OID.
456  */
457 static bool
458 oid_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. */
482 static void
483 oiddescription_print(const struct oiddescription *od, FILE *fp)
484 {
485 
486 	fprintf(fp, "%s", od->description);
487 }
488 
489 static void
490 oid_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. */
540 static bool
541 buf_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 
559 static void
560 usage(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 
569 int
570 main(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