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
oid_get_root(struct oid * o)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
oid_get_by_name(struct oid * o,const char * name)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
oid_is_beneath(struct oid * oa,struct oid * ob)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
oid_get_next(const struct oid * cur,struct oid * next)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
oidformat_is_temperature(const struct oidformat * of)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
oidformat_is_timeval(const struct oidformat * of)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
oid_get_format(const struct oid * o,struct oidformat * of)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
oidvalue_get_float(const struct oidvalue * ov)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
oidvalue_set_signed(struct oidvalue * ov,intmax_t s)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
oidvalue_set_unsigned(struct oidvalue * ov,uintmax_t u)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
oidvalue_set_float(struct oidvalue * ov,double f)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
oidvalue_print(const struct oidvalue * ov,FILE * fp)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
oid_get_value(const struct oid * o,const struct oidformat * of,struct oidvalue * ov)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
oidname_init(struct oidname * on)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
oid_get_name(const struct oid * o,struct oidname * on)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
oid_get_metric(const struct oidname * on,const struct oidformat * of,char * metric,size_t mlen)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
oidname_has_labels(const struct oidname * on)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
oid_get_description(const struct oid * o,struct oiddescription * od)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
oiddescription_print(const struct oiddescription * od,FILE * fp)483 oiddescription_print(const struct oiddescription *od, FILE *fp)
484 {
485
486 fprintf(fp, "%s", od->description);
487 }
488
489 static void
oid_print(const struct oid * o,struct oidname * on,bool print_description,bool exclude,bool include,FILE * fp)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
buf_gzip(const char * in,size_t inlen,char * out,size_t * outlen)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
usage(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
main(int argc,char * argv[])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