kcollect - Final dbm support code
[dragonfly.git] / usr.bin / kcollect / kcollect.c
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 }