1 /* 2 * Copyright (c) 2017 The DragonFly Project. All rights reserved. 3 * 4 * This code is derived from software contributed to The DragonFly Project 5 * by Matthew Dillon <dillon@backplane.com> 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the 16 * distribution. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 26 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 28 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 * SUCH DAMAGE. 30 */ 31 32 #include "kcollect.h" 33 34 #include <ndbm.h> 35 #include <fcntl.h> 36 #include <errno.h> 37 38 #define SLEEP_INTERVAL 60 /* minimum is KCOLLECT_INTERVAL */ 39 40 #define DISPLAY_TIME_ONLY "%H:%M:%S" 41 #define DISPLAY_FULL_DATE "%F %H:%M:%S" 42 #define HDR_FMT "HEADER0" 43 #define HDR_TITLE "HEADER1" 44 45 static void format_output(uintmax_t value,char fmt,uintmax_t scale, char* ret); 46 static void dump_text(kcollect_t *ary, size_t count, 47 size_t total_count, const char* display_fmt); 48 static void dump_dbm(kcollect_t *ary, size_t count, const char *datafile); 49 static int str2unix(const char* str, const char* fmt); 50 static int rec_comparator(const void *c1, const void *c2); 51 static void load_dbm(const char *datafile, 52 kcollect_t **ret_ary, size_t *counter); 53 static void dump_fields(kcollect_t *ary); 54 static void adjust_fields(kcollect_t *ent, const char *fields); 55 static void restore_headers(kcollect_t *ary, const char *datafile); 56 57 FILE *OutFP; 58 int UseGMT; 59 int OutputWidth = 1024; 60 int OutputHeight = 1024; 61 int SmoothOpt; 62 int LoadedFromDB = 0; 63 64 int 65 main(int ac, char **av) 66 { 67 kcollect_t *ary; 68 size_t bytes = 0; 69 size_t count; 70 size_t total_count; 71 const char *datafile = NULL; 72 const char *fields = NULL; 73 int cmd = 't'; 74 int ch; 75 int keepalive = 0; 76 int last_ticks; 77 int loops = 0; 78 int maxtime = 0; 79 80 kcollect_t *dbmAry = NULL; 81 const char *dbmFile = NULL; 82 int fromFile = 0; 83 84 OutFP = stdout; 85 86 sysctlbyname("kern.collect_data", NULL, &bytes, NULL, 0); 87 if (bytes == 0) { 88 fprintf(stderr, "kern.collect_data not available\n"); 89 exit(1); 90 } 91 92 while ((ch = getopt(ac, av, "o:b:d:r:flsgt:xw:GW:H:")) != -1) { 93 char *suffix; 94 95 switch(ch) { 96 case 'o': 97 fields = optarg; 98 break; 99 case 'b': 100 datafile = optarg; 101 cmd = 'b'; 102 break; 103 case 'd': 104 dbmFile = optarg; 105 fromFile = 1; 106 break; 107 case 'r': 108 datafile = optarg; 109 cmd = 'r'; 110 break; 111 case 'f': 112 keepalive = 1; 113 break; 114 case 'l': 115 cmd = 'l'; 116 break; 117 case 's': 118 SmoothOpt = 1; 119 break; 120 case 'w': 121 datafile = optarg; 122 cmd = 'w'; 123 break; 124 case 'g': 125 cmd = 'g'; 126 break; 127 case 'x': 128 cmd = 'x'; 129 break; 130 case 't': 131 maxtime = strtol(optarg, &suffix, 0); 132 switch(*suffix) { 133 case 'd': 134 maxtime *= 24; 135 /* fall through */ 136 case 'h': 137 maxtime *= 60; 138 /* fall through */ 139 case 'm': 140 maxtime *= 60; 141 break; 142 case 0: 143 break; 144 default: 145 fprintf(stderr, 146 "Illegal suffix in -t option\n"); 147 exit(1); 148 } 149 break; 150 case 'G': 151 UseGMT = 1; 152 break; 153 case 'W': 154 OutputWidth = strtol(optarg, NULL, 0); 155 break; 156 case 'H': 157 OutputHeight = strtol(optarg, NULL, 0); 158 break; 159 default: 160 fprintf(stderr, "Unknown option %c\n", ch); 161 exit(1); 162 /* NOT REACHED */ 163 } 164 } 165 if (cmd != 'x' && ac != optind) { 166 fprintf(stderr, "Unknown argument %s\n", av[optind]); 167 exit(1); 168 /* NOT REACHED */ 169 } 170 171 total_count = 0; 172 last_ticks = 0; 173 174 if (cmd == 'x' || cmd == 'w') 175 start_gnuplot(ac - optind, av + optind, datafile); 176 177 do { 178 /* 179 * Snarf as much data as we can. If we are looping, 180 * snarf less (no point snarfing stuff we already have). 181 */ 182 bytes = 0; 183 sysctlbyname("kern.collect_data", NULL, &bytes, NULL, 0); 184 if (cmd == 'l') 185 bytes = sizeof(kcollect_t) * 2; 186 187 if (loops) { 188 size_t loop_bytes; 189 190 loop_bytes = sizeof(kcollect_t) * 191 (4 + SLEEP_INTERVAL / KCOLLECT_INTERVAL); 192 if (bytes > loop_bytes) 193 bytes = loop_bytes; 194 } 195 196 ary = malloc(bytes); 197 sysctlbyname("kern.collect_data", ary, &bytes, NULL, 0); 198 count = bytes / sizeof(kcollect_t); 199 200 /* 201 * If we got specified a file to load from: replace the data 202 * array and counter 203 */ 204 if (fromFile) { 205 load_dbm(dbmFile, &dbmAry, &count); 206 free(ary); 207 ary = dbmAry; 208 209 } 210 if (fields) 211 adjust_fields(&ary[1], fields); 212 213 214 /* 215 * Delete duplicate entries when looping 216 */ 217 if (loops) { 218 while (count > 2) { 219 if ((int)(ary[count-1].ticks - last_ticks) > 0) 220 break; 221 --count; 222 } 223 } 224 225 /* 226 * Delete any entries beyond the time limit 227 */ 228 if (maxtime) { 229 maxtime *= ary[0].hz; 230 while (count > 2) { 231 if ((int)(ary[0].ticks - ary[count-1].ticks) < 232 maxtime) { 233 break; 234 } 235 --count; 236 } 237 } 238 239 switch(cmd) { 240 case 't': 241 if (count > 2) { 242 dump_text(ary, count, total_count, 243 (fromFile ? DISPLAY_FULL_DATE : 244 DISPLAY_TIME_ONLY)); 245 } 246 break; 247 case 'b': 248 if (count > 2) 249 dump_dbm(ary, count, datafile); 250 break; 251 case 'r': 252 if (count >= 2) 253 restore_headers(ary, datafile); 254 break; 255 case 'l': 256 dump_fields(ary); 257 exit(0); 258 break; /* NOT REACHED */ 259 case 'g': 260 if (count > 2) 261 dump_gnuplot(ary, count); 262 break; 263 case 'w': 264 if (count >= 2) 265 dump_gnuplot(ary, count); 266 break; 267 case 'x': 268 if (count > 2) 269 dump_gnuplot(ary, count); 270 break; 271 } 272 if (keepalive && !fromFile) { 273 fflush(OutFP); 274 fflush(stdout); 275 switch(cmd) { 276 case 't': 277 sleep(1); 278 break; 279 case 'x': 280 case 'g': 281 case 'w': 282 sleep(60); 283 break; 284 default: 285 sleep(10); 286 break; 287 } 288 } 289 last_ticks = ary[2].ticks; 290 if (count >= 2) 291 total_count += count - 2; 292 293 /* 294 * Loop for incremental aquisition. When outputting to 295 * gunplot, we have to send the whole data-set again so 296 * do not increment loops in that case. 297 */ 298 if (cmd != 'g' && cmd != 'x' && cmd != 'w') 299 ++loops; 300 301 free(ary); 302 } while (keepalive); 303 304 if (cmd == 'x') 305 pclose(OutFP); 306 } 307 308 static 309 void 310 format_output(uintmax_t value,char fmt,uintmax_t scale, char* ret) 311 { 312 char buf[9]; 313 314 switch(fmt) { 315 case '2': 316 /* 317 * fractional x100 318 */ 319 sprintf(ret, "%5ju.%02ju", 320 value / 100, value % 100); 321 break; 322 case 'p': 323 /* 324 * Percentage fractional x100 (100% = 10000) 325 */ 326 sprintf(ret,"%4ju.%02ju%%", 327 value / 100, value % 100); 328 break; 329 case 'm': 330 /* 331 * Megabytes 332 */ 333 humanize_number(buf, sizeof(buf), value, "", 334 2, 335 HN_FRACTIONAL | 336 HN_NOSPACE); 337 sprintf(ret,"%8.8s", buf); 338 break; 339 case 'c': 340 /* 341 * Raw count over period (this is not total) 342 */ 343 humanize_number(buf, sizeof(buf), value, "", 344 HN_AUTOSCALE, 345 HN_FRACTIONAL | 346 HN_NOSPACE | 347 HN_DIVISOR_1000); 348 sprintf(ret,"%8.8s", buf); 349 break; 350 case 'b': 351 /* 352 * Total bytes (this is a total), output 353 * in megabytes. 354 */ 355 if (scale > 100000000) { 356 humanize_number(buf, sizeof(buf), 357 value, "", 358 3, 359 HN_FRACTIONAL | 360 HN_NOSPACE); 361 } else { 362 humanize_number(buf, sizeof(buf), 363 value, "", 364 2, 365 HN_FRACTIONAL | 366 HN_NOSPACE); 367 } 368 sprintf(ret,"%8.8s", buf); 369 break; 370 default: 371 sprintf(ret,"%s"," "); 372 break; 373 } 374 } 375 376 static 377 void 378 dump_text(kcollect_t *ary, size_t count, size_t total_count, 379 const char* display_fmt) 380 { 381 int j; 382 int i; 383 uintmax_t scale; 384 uintmax_t value; 385 char fmt; 386 char sbuf[20]; 387 struct tm *tmv; 388 time_t t; 389 390 for (i = count - 1; i >= 2; --i) { 391 if ((total_count & 15) == 0) { 392 if (!strcmp(display_fmt, DISPLAY_FULL_DATE)) { 393 printf("%20s", "timestamp "); 394 } else { 395 printf("%8.8s", "time"); 396 } 397 for (j = 0; j < KCOLLECT_ENTRIES; ++j) { 398 if (ary[1].data[j]) { 399 printf(" %8.8s", 400 (char *)&ary[1].data[j]); 401 } 402 } 403 printf("\n"); 404 } 405 406 /* 407 * Timestamp 408 */ 409 t = ary[i].realtime.tv_sec; 410 if (UseGMT) 411 tmv = gmtime(&t); 412 else 413 tmv = localtime(&t); 414 strftime(sbuf, sizeof(sbuf), display_fmt, tmv); 415 printf("%8s", sbuf); 416 417 for (j = 0; j < KCOLLECT_ENTRIES; ++j) { 418 if (ary[1].data[j] == 0) 419 continue; 420 421 /* 422 * NOTE: kernel does not have to provide the scale 423 * (that is, the highest likely value), nor 424 * does it make sense in all cases. 425 * 426 * Example scale - kernel provides total amount 427 * of memory available for memory related 428 * statistics in the scale field. 429 */ 430 value = ary[i].data[j]; 431 scale = KCOLLECT_GETSCALE(ary[0].data[j]); 432 fmt = KCOLLECT_GETFMT(ary[0].data[j]); 433 434 printf(" "); 435 436 format_output(value, fmt, scale, sbuf); 437 printf("%s",sbuf); 438 } 439 440 printf("\n"); 441 ++total_count; 442 } 443 } 444 445 /* restores the DBM database header records to current machine */ 446 static 447 void 448 restore_headers(kcollect_t *ary, const char *datafile) 449 { 450 DBM *db; 451 char hdr_fmt[] = HDR_FMT; 452 char hdr_title[] = HDR_TITLE; 453 datum key, value; 454 455 db = dbm_open(datafile, (O_RDWR), 456 (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)); 457 458 if (db == NULL) { 459 switch (errno) { 460 case EACCES: 461 fprintf(stderr, 462 "[ERR] database file \"%s\" is read-only, " 463 "check permissions. (%i)\n", 464 datafile, errno); 465 break; 466 default: 467 fprintf(stderr, 468 "[ERR] opening our database file \"%s\" " 469 "produced an error. (%i)\n", 470 datafile, errno); 471 } 472 exit(EXIT_FAILURE); 473 } else { 474 key.dptr = hdr_fmt; 475 key.dsize = sizeof(HDR_FMT); 476 value.dptr = &ary[0].data; 477 value.dsize = sizeof(uint64_t) * KCOLLECT_ENTRIES; 478 if (dbm_store(db,key,value,DBM_REPLACE) == -1) { 479 fprintf(stderr, 480 "[ERR] error storing the value in " 481 "the database file \"%s\" (%i)\n", 482 datafile, errno); 483 dbm_close(db); 484 exit(EXIT_FAILURE); 485 } 486 487 key.dptr = hdr_title; 488 key.dsize = sizeof(HDR_FMT); 489 value.dptr = &ary[1].data; 490 value.dsize = sizeof(uint64_t) * KCOLLECT_ENTRIES; 491 if (dbm_store(db,key,value,DBM_REPLACE) == -1) { 492 fprintf(stderr, 493 "[ERR] error storing the value in " 494 "the database file \"%s\" (%i)\n", 495 datafile, errno); 496 dbm_close(db); 497 exit(EXIT_FAILURE); 498 } 499 } 500 dbm_close(db); 501 } 502 503 504 /* 505 * Store the array of kcollect_t records in a dbm db database, 506 * path passed in datafile 507 */ 508 static 509 void 510 dump_dbm(kcollect_t *ary, size_t count, const char *datafile) 511 { 512 DBM * db; 513 514 db = dbm_open(datafile, (O_RDWR | O_CREAT), 515 (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)); 516 if (db == NULL) { 517 switch (errno) { 518 case EACCES: 519 fprintf(stderr, 520 "[ERR] database file \"%s\" is read-only, " 521 "check permissions. (%i)\n", 522 datafile, errno); 523 break; 524 default: 525 fprintf(stderr, 526 "[ERR] opening our database file \"%s\" " 527 "produced an error. (%i)\n", 528 datafile, errno); 529 } 530 exit(EXIT_FAILURE); 531 } else { 532 struct tm *tmv; 533 char buf[20]; 534 datum key; 535 datum value; 536 time_t t; 537 uint i; 538 char hdr_fmt[] = HDR_FMT; 539 char hdr_title[] = HDR_TITLE; 540 541 for (i = 0; i < (count); ++i) { 542 /* first 2 INFO records are special and get 0|1 */ 543 544 if (i < 2) { 545 if (i == 0) 546 key.dptr = hdr_fmt; 547 else 548 key.dptr = hdr_title; 549 key.dsize = sizeof(HDR_FMT); 550 } else { 551 t = ary[i].realtime.tv_sec; 552 if (LoadedFromDB) 553 tmv = localtime(&t); 554 else 555 tmv = gmtime(&t); 556 strftime(buf, sizeof(buf), 557 DISPLAY_FULL_DATE, tmv); 558 key.dptr = buf; 559 key.dsize = sizeof(buf); 560 } 561 value.dptr = ary[i].data; 562 value.dsize = sizeof(uint64_t) * KCOLLECT_ENTRIES; 563 if (dbm_store(db, key, value, DBM_INSERT) == -1) { 564 fprintf(stderr, 565 "[ERR] error storing the value in " 566 "the database file \"%s\" (%i)\n", 567 datafile, errno); 568 dbm_close(db); 569 exit(EXIT_FAILURE); 570 } 571 572 } 573 dbm_close(db); 574 } 575 } 576 577 /* 578 * Transform a string (str) matching a format string (fmt) into a unix 579 * timestamp and return it used by load_dbm() 580 */ 581 static 582 int 583 str2unix(const char* str, const char* fmt){ 584 struct tm tm; 585 time_t ts; 586 587 /* 588 * Reset all the fields because strptime only sets what it 589 * finds, which may lead to undefined members 590 */ 591 memset(&tm, 0, sizeof(struct tm)); 592 strptime(str, fmt, &tm); 593 ts = timegm(&tm); 594 595 return (int)ts; 596 } 597 598 /* 599 * Sorts the ckollect_t records by time, to put youngest first, 600 * so desc by timestamp used by load_dbm() 601 */ 602 static 603 int 604 rec_comparator(const void *c1, const void *c2) 605 { 606 const kcollect_t *k1 = (const kcollect_t*)c1; 607 const kcollect_t *k2 = (const kcollect_t*)c2; 608 609 if (k1->realtime.tv_sec < k2->realtime.tv_sec) 610 return -1; 611 if (k1->realtime.tv_sec > k2->realtime.tv_sec) 612 return 1; 613 return 0; 614 } 615 616 /* 617 * Loads the ckollect records from a dbm DB database specified in datafile. 618 * returns the resulting array in ret_ary and the array counter in counter 619 */ 620 static 621 void 622 load_dbm(const char* datafile, kcollect_t **ret_ary, 623 size_t *counter) 624 { 625 DBM * db = dbm_open(datafile,(O_RDONLY),(S_IRUSR|S_IRGRP)); 626 datum key; 627 datum value; 628 size_t recCounter = 0; 629 int headersFound = 0; 630 631 if (db == NULL) { 632 fprintf(stderr, 633 "[ERR] opening our database \"%s\" produced " 634 "an error! (%i)\n", 635 datafile, errno); 636 exit(EXIT_FAILURE); 637 } else { 638 /* counting loop */ 639 for (key = dbm_firstkey(db); key.dptr; key = dbm_nextkey(db)) { 640 value = dbm_fetch(db, key); 641 if (value.dptr != NULL) 642 recCounter++; 643 } 644 645 /* with the count allocate enough memory */ 646 if (*ret_ary) 647 free(*ret_ary); 648 *ret_ary = malloc(sizeof(kcollect_t) * recCounter); 649 bzero(*ret_ary, sizeof(kcollect_t) * recCounter); 650 if (*ret_ary == NULL) { 651 fprintf(stderr, 652 "[ERR] failed to allocate enough memory to " 653 "hold the database! Aborting.\n"); 654 dbm_close(db); 655 exit(EXIT_FAILURE); 656 } else { 657 uint c; 658 uint sc; 659 /* 660 * Actual data retrieval but only of recCounter 661 * records 662 */ 663 c = 2; 664 key = dbm_firstkey(db); 665 while (key.dptr && c < recCounter) { 666 value = dbm_fetch(db, key); 667 if (value.dptr != NULL) { 668 if (!strcmp(key.dptr, HDR_FMT)) { 669 sc = 0; 670 headersFound |= 1; 671 } 672 else if (!strcmp(key.dptr, HDR_TITLE)) { 673 sc = 1; 674 headersFound |= 2; 675 } 676 else { 677 sc = c; 678 c++; 679 } 680 681 memcpy((*ret_ary)[sc].data, 682 value.dptr, 683 sizeof(uint64_t) * KCOLLECT_ENTRIES); 684 (*ret_ary)[sc].realtime.tv_sec = 685 str2unix(key.dptr, 686 DISPLAY_FULL_DATE); 687 } 688 key = dbm_nextkey(db); 689 } 690 } 691 } 692 693 /* 694 * Set the counter, 695 * and sort the non-header records. 696 */ 697 *counter = recCounter; 698 qsort(&(*ret_ary)[2], recCounter - 2, sizeof(kcollect_t), rec_comparator); 699 dbm_close(db); 700 701 if (headersFound != 3) { 702 fprintf(stderr, "We could not retrieve all necessary headers, might be the database file is corrupted? (%i)\n", headersFound); 703 exit(EXIT_FAILURE); 704 } 705 LoadedFromDB = 1; 706 } 707 708 static void 709 dump_fields(kcollect_t *ary) 710 { 711 int j; 712 713 for (j = 0; j < KCOLLECT_ENTRIES; ++j) { 714 if (ary[1].data[j] == 0) 715 continue; 716 printf("%8.8s %c\n", 717 (char *)&ary[1].data[j], 718 KCOLLECT_GETFMT(ary[0].data[j])); 719 } 720 } 721 722 static void 723 adjust_fields(kcollect_t *ent, const char *fields) 724 { 725 char *copy = strdup(fields); 726 char *word; 727 int selected[KCOLLECT_ENTRIES]; 728 int j; 729 730 bzero(selected, sizeof(selected)); 731 732 word = strtok(copy, ", \t"); 733 while (word) { 734 for (j = 0; j < KCOLLECT_ENTRIES; ++j) { 735 if (strncmp(word, (char *)&ent->data[j], 8) == 0) { 736 selected[j] = 1; 737 break; 738 } 739 } 740 word = strtok(NULL, ", \t"); 741 } 742 free(copy); 743 for (j = 0; j < KCOLLECT_ENTRIES; ++j) { 744 if (!selected[j]) 745 ent->data[j] = 0; 746 } 747 } 748