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