xref: /dragonfly/usr.bin/kcollect/kcollect.c (revision 1310e0bb)
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