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