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