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