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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 484 oiddescription_print(const struct oiddescription *od, FILE *fp) 485 { 486 487 fprintf(fp, "%s", od->description); 488 } 489 490 static void 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 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 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 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