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