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