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