rsalz@bbn.com (Rich Salz) (02/11/88)
Submitted-by: Roy Smith <phri!roy> Posting-number: Volume 13, Issue 33 Archive-name: printacct [ This package displays the contents of the /usr/adm/acct files in human-readable format, so you can use things like awk and perl to print "better" accounting records. It will probably need work to run with the SystemV accounting stuff. --r$ ] # This is a shell archive. Remove anything before this line, # then unpack it by saving it in a file and typing "sh file". # Contents: README Makefile pracct.1 pracct.c yesterday.c accounting echo x - README sed 's/^@//' > "README" <<'@//E*O*F README//' Mon Jan 25 23:15:18 EST 1988 This is a little program I whipped up to help me do user cpu usage accounting. On the surface, /etc/sa (or /usr/etc/sa if you're on a Sun) does cpu usage accounting, but it does so much damn processing that I kept finding myself being frustrated because I couldn't get exactly the statistics I wanted. Very un-unix-like, sa is. Doesn't let the user get at the actual data to do what he wants with it. My answer was to just convert the fields in /usr/adm/acct to ascii and hack together awk scripts to pull out whatever bits of information I happened to want that month. Maybe not very efficient, but easy. Contained herein is the program to print the /usr/adm/acct file, a man page, and a sample shell script that we run out of crontab at 15 minutes past midnight every day. We run this on a network of Sun workstations, all of which remote mount a common /usr/local. Since root permissions don't apply accross NFS mount points, I made /usr/local/acct owned by user "acct", and su to that when it comes time to move the daily summary file there. You can't just make the whole script suid acct because it has to remove /usr/adm/acct (aka /private/usr/adm/acct), which needs root permission. Isn't NFS fun? One last note. I like to keep each day's stats in a file named for that day. Since we run the stats for day X a few minutes into day X+1, I can't just use date to generate file names, because the names would be off by one day. I whipped up yesterday.c to solve this problem. A nicer solution (but decidedly feeping creatureism) would be to expand the Sys5 date to accept a (signed) offset which is added to the current date before going through the variable output formatting code. This was developed on SunOS-3.[02] systems, and hasn't been tested on any other systems. I believe the format of /usr/adm/acct is BSD specific, so it probably won't run on Sys5 machines unmodified. This code is public domain. Do with it what you want, including selling it for profit if you can find somebody stupid enough to pay money for it. All I ask is that you retain this notice with the code, and keep my name on it. Roy Smith, System Administrator Public Health Research Institute 455 First Avenue, New York, NY 10016 UUCP: {allegra,philabs,seismo!cmcl2}!phri!roy @//E*O*F README// chmod u=rw,g=r,o=r README echo x - Makefile sed 's/^@//' > "Makefile" <<'@//E*O*F Makefile//' # # Makefile for pracct # # $Header: Makefile,v 1.3 86/12/09 17:41:42 roy Exp $ # Copyright 1986 Roy Smith # # $Log: Makefile,v $ # Revision 1.3 86/12/09 17:41:42 roy # Added rules for yesterday. Deleted rules for lint. # # Revision 1.2 86/11/29 23:30:18 roy # Added rule to co source file if needed. # Added rule for "make lint" # # Revision 1.1 86/11/29 23:12:19 roy # Initial revision # # CFLAGS = -O programs: pracct yesterday pracct: pracct.o cc $(CFLAGS) -o pracct pracct.o pracct.o: pracct.c cc $(CFLAGS) -c pracct.c pracct.c: RCS/pracct.c,v co pracct.c yesterday: yesterday.o cc $(CFLAGS) -o yesterday yesterday.o yesterday.o: yesterday.c cc $(CFLAGS) -c yesterday.c yesterday.c: RCS/yesterday.c,v co yesterday.c @//E*O*F Makefile// chmod u=rw,g=r,o=r Makefile echo x - pracct.1 sed 's/^@//' > "pracct.1" <<'@//E*O*F pracct.1//' @.TH DATE 1 "25 Jan 1988" @.SH NAME pracct \- print accounting information in human readable form @.SH SYNOPSIS @.B pracct [-v] [-a file] [-f [cvVsSxXeEbBuUgGmitTfF]] @.SH DESCRIPTION @.I Pracct prints out the system accounting file in human-readable form with a minimum of processing. This is sort of a RISC version of @.I /etc/sa. As the man page says, @.I sa has a near google of options; unfortunately, it is rare that it has exactly the right one. With @.I pracct, you can simply get an ascii dump of the fields of interest and use @.I awk (for example) to calculate whatever statistics you desire. The options are many: @.TP @.B -v Verbose output; each field is tagged with a (hopefully) descriptive label. @.TP @.BI -a " file" Alternate accounting file, used instead of default @.I /usr/adm/acct. @.TP @.BI -f " flags" Fields to print. Fields are printed in the order specified. Default is @.B UcXB. Fields are separated with spaces, and an attempt is made to ensure that no spaces appear within fields (i.e. -f F will give `F----' instead of `F\0\0\0\0'). This may be defeated, however, by command, user, group, or tty names containing spaces within them. Also, @.B -B gives a date in ctime format, which has embedded spaces. The fields are: @.TP @.B c Command name @.TP @.B v Numeric user CPU time (seconds) @.TP @.B V Symbolic user time (DD+HH:MM:SS) @.TP @.B s Numeric system time (seconds) @.TP @.B S Symbolic system time (DD+HH:MM:SS) @.TP @.B x User+system time (seconds) @.TP @.B X Symbolic user+system time (DD+HH:MM:SS) @.TP @.B e Numeric elapsed time (seconds) @.TP @.B E Symbolic elapsed time (DD+HH:MM:SS) @.TP @.B b Numeric beginning time (seconds) @.TP @.B B Symbolic beginning time (ctime format) @.TP @.B u Numeric user I.D. @.TP @.B U Login name @.TP @.B g Numeric group I.D. @.TP @.B G Group name @.TP @.B m Average memory usage @.TP @.B i Disk I/O blocks @.TP @.B t Major/minor device for control tty @.TP @.B T Name of control tty (/dev/tty??); currently unimplemented @.TP @.B f Accounting flags in octal @.TP @.B F Flags as FSCDX @.SH "SEE ALSO" sa(8), acct(2) @.SH AUTHOR Roy Smith <phri!roy> @//E*O*F pracct.1// chmod u=rw,g=r,o=r pracct.1 echo x - pracct.c sed 's/^@//' > "pracct.c" <<'@//E*O*F pracct.c//' /* * Pracct.c -- print out the system accounting file with a minimum of * processing. This is sort of a RISC version of /etc/sa. As the man page * says, sa has a near google of options; unfortunately, it is rare that * it has exactly the right one. With pracct, you can simply get an ascii * dump of the fields of interest and let awk do the the rest. */ # ifndef LINT static char rcsid[] = "$Header: pracct.c,v 1.7 88/01/25 22:57:16 roy Exp $"; # endif /* * $Log: pracct.c,v $ * Revision 1.7 88/01/25 22:57:16 roy * Removed copyright notice; code placed in public domain. * * Revision 1.6 87/11/17 15:10:55 roy * Added uid/gid name caches. * * Revision 1.5 86/12/02 17:31:43 roy * Fixed but with printing ac_comm -- if command name is full * length of ac_comm[], it isn't null terminated; we now are * careful not to run past this. Changed symbolic flag format * to print '-' instead of ' ' for missing flags to keep the number * of fields on an output line constant. * * Revision 1.4 86/12/02 14:34:52 roy * Added support for -v (verbose) and -a * (alternate accounting file) options. * * Revision 1.3 86/12/02 00:33:21 roy * Added command-line option processing using getopt. * * Revision 1.2 86/11/29 23:29:16 roy * Fixed rcsid string (capitalized $ H e a d e r : $). * Added type casts to keep lint happy. * * Revision 1.1 86/11/29 23:07:16 roy * Initial revision * */ # include <sys/types.h> # include <sys/acct.h> # include <sys/file.h> # include <sys/time.h> # include <pwd.h> # include <grp.h> # include <stdio.h> # define ACCT_FILE "/usr/adm/acct" /* raw accounting data file */ # define DFLT_FLDS "UcXB" /* default fields to print */ # define FIELD_SEP ' ' /* how to delimit fields on output */ # define GETOPT_FMT "va:f:" /* permissible command-line flags */ # define USAGE_MSG "pracct [-v] [-a file] [-f [cvVsSxXeEbBuUgGmitTfF]]" /* * Symbolic names and flags for fields in acct(5) structure */ # define COMM 'c' /* command name */ # define UTIME 'v' /* numeric user CPU time (seconds) */ # define SYM_UTIME 'V' /* symbolic user time (DD+HH:MM:SS) */ # define STIME 's' /* numeric system time (seconds) */ # define SYM_STIME 'S' /* symbolic system time (DD+HH:MM:SS) */ # define SUMTIME 'x' /* user+system time (seconds) */ # define SYM_SUMTIME 'X' /* symbolic user+system time (DD+HH:MM:SS) */ # define ETIME 'e' /* numeric elapsed time (seconds) */ # define SYM_ETIME 'E' /* symbolic elapsed time (DD+HH:MM:SS) */ # define BTIME 'b' /* numeric beginning time (seconds) */ # define SYM_BTIME 'B' /* symbolic beginning time (ctime format) */ # define UID 'u' /* numeric user I.D. */ # define SYM_UID 'U' /* login name */ # define GID 'g' /* numeric group I.D. */ # define SYM_GID 'G' /* group name */ # define MEM 'm' /* average memory usage */ # define IO 'i' /* disk I/O blocks */ # define TTY 't' /* major/minor device for control tty */ # define SYM_TTY 'T' /* name of control tty /dev/tty?? */ # define FLAG 'f' /* flags in octal */ # define SYM_FLAG 'F' /* flags as FSCDX */ /* * Name cache stuff. */ # define NC_NUID 256 /* size of uid cache; power of 2 */ # define NC_UIDMASK 0xff /* log base 2 (NC_NUID) - 1 */ # define NC_NGID 64 /* size of gid cache; power of 2 */ # define NC_GIDMASK 0x3f /* log base 2 (NC_NGID) - 1 */ struct nc { int nc_n; /* numeric uid or gid */ char *nc_name; /* user or group name; NULL if unused */ } unc[NC_NUID], gnc[NC_NGID]; main (argc, argv) int argc; char *argv[]; { struct acct ac; int fd, acsize, first, nbytes, verbose, n, maxcom; long utime, stime, etime, sumtime, btime; char *fields, *f, *ct, *ctime(), *dhms(), *uid2name(), *gid2name(); char c, *afile; extern int optind; extern char *optarg; fields = DFLT_FLDS; verbose = 0; afile = ACCT_FILE; /* * figure out the maximum number of characters * stored for each command name. */ maxcom = sizeof (ac.ac_comm) / sizeof (*ac.ac_comm); while ((c = getopt (argc, argv, GETOPT_FMT)) != EOF) { switch (c) { case 'f': fields = optarg; break; case 'v': verbose = 1; break; case 'a': afile = optarg; break; case '?': fprintf (stderr, "pracct: illegal option (-%c)\n", c); fprintf (stderr, "pracct: usage: %s\n", USAGE_MSG); exit (1); default: fprintf (stderr, "bad return from getopt (%o)!\n", c); abort (); } } /* * Open accounting file for reading. */ if ((fd = open (afile, O_RDONLY, 0)) < 0) { perror ("pracct: can't open accounting file"); exit (1); } /* * Flush name cache. */ for (n = 0; n < NC_NUID; n++) unc[n].nc_name = NULL; for (n = 0; n < NC_NGID; n++) gnc[n].nc_name = NULL; /* * Read the accounting file, one accounting-structure-worth * at a time, until EOF or error. */ acsize = sizeof (ac) / sizeof (char); while ((nbytes = read (fd, (char *) &ac, acsize)) == acsize) { /* * Convert from snazzy compressed floating point format to * plain old integers. It's easier to just do all the * conversions than to check to see which ones we actually * need. We often want sumtime, so we rarely lose big. * This may get changed later if it proves to be important. */ utime = expand (ac.ac_utime); stime = expand (ac.ac_stime); sumtime = utime + stime; etime = expand (ac.ac_etime); /* * Step though the desired fields in the order specified. * Insert a field delimiter in front of every field except * for the first one. Warning: fields may have internal * blanks, i.e. those in ctime format. */ first = 1; for (f = fields; *f != NULL; f++) { if (first) first = 0; else putchar (FIELD_SEP); switch (*f) { case COMM: /* * We have to be careful not to print past * the end of the string because the command * name is only null terminated if it is * less than maxcom characters long. */ printf ("%-*.*s", maxcom, maxcom, ac.ac_comm); continue; case UTIME: printf ("%8ld", utime); if (verbose) printf ("user"); continue; case SYM_UTIME: printf ("%11s", dhms (utime)); if (verbose) printf ("user"); continue; case STIME: printf ("%8ld", stime); if (verbose) printf ("sys"); continue; case SYM_STIME: printf ("%11s", dhms (stime)); if (verbose) printf ("sys"); continue; case SUMTIME: printf ("%8ld", sumtime); if (verbose) printf ("u+s"); continue; case SYM_SUMTIME: printf ("%11s", dhms (sumtime)); if (verbose) printf ("u+s"); continue; case ETIME: printf ("%8ld", etime); if (verbose) printf ("real"); continue; case SYM_ETIME: printf ("%11s", dhms (etime)); if (verbose) printf ("real"); continue; case BTIME: printf ("%10ld", (long) ac.ac_btime); if (verbose) printf ("begin"); continue; case SYM_BTIME: /* * Beginning time in ctime format. Ctime * does us a favor and sticks a newline at * the end of the string, which we then have * to be careful not to print. */ btime = ac.ac_btime; ct = ctime (&btime); while (*ct != '\n' && *ct != NULL) putchar (*(ct++)); if (verbose) printf (" begin"); continue; case UID: printf ("%5d", ac.ac_uid); if (verbose) printf ("uid"); continue; case SYM_UID: printf ("%-8s", uid2name (ac.ac_uid)); continue; case GID: printf ("%5d", ac.ac_gid); if (verbose) printf ("gid"); continue; case SYM_GID: printf ("%-8s", gid2name (ac.ac_gid)); continue; case MEM: printf ("%5d", ac.ac_mem); if (verbose) printf ("mem"); continue; case IO: printf ("%10ld", (long) expand (ac.ac_io)); if (verbose) printf ("io"); continue; case TTY: if (verbose) { printf ("%3d", major (ac.ac_tty)); printf ("/%3d", minor (ac.ac_tty)); } else { printf ("%3d", major (ac.ac_tty)); printf (" %3d", minor (ac.ac_tty)); } continue; case SYM_TTY: printf ("no SYM_TTY yet"); continue; case FLAG: printf ("%03o", ac.ac_flag); continue; case SYM_FLAG: putchar (ac.ac_flag & AFORK ? 'F' : '-'); putchar (ac.ac_flag & ASU ? 'S' : '-'); putchar (ac.ac_flag & ACOMPAT ? 'C' : '-'); putchar (ac.ac_flag & ACORE ? 'D' : '-'); putchar (ac.ac_flag & AXSIG ? 'X' : '-'); continue; } } /* * After all the required fields, end the line. */ putchar ('\n'); } if (nbytes > 0) { fprintf (stderr, "pracct: short read on accounting file\n"); exit (1); } if (nbytes < 0) { perror ("pracct: read error on accounting file"); exit (1); } exit (0); } /* * Dhms -- convert time in seconds to DD+HH:MM:SS format. If * time is negative or greater than 99+23:59:59, return all ?'s. * Result is returned as a fixed-width, null-terminated, static * string which is overwritten on each call. */ char *dhms (time) register long time; /* , no see :-) */ { int sec, min, hour, day; static char buf[(sizeof ("DD+HH:MM:SS") / sizeof (char)) + 1]; sec = time % 60; time = time / 60; min = time % 60; time = time / 60; hour = time % 24; time = time / 24; day = time; if (time < 0 || day > 99) sprintf (buf, "??+??:??:??"); else sprintf (buf, "%02d+%02d:%02d:%02d", day, hour, min, sec); return (buf); } /* * Expand -- convert time from compressed floating point format to normal * integer. This routine is based on one in the 4.2BSD /etc/sa.c source * file. Warning: this depends on the format of t being exactly as in * 4.2BSD -- 3 bit power-of-8 exponent in high-order bits and 13 bit * mantissa in low-order bits. */ time_t expand (t) comp_t t; { register time_t xtime; register exp; /* * Get mantissa and exponent. */ xtime = t & 017777; exp = (t >> 13) & 07; /* * Compute time = mantissa * 8^exp. */ while (exp != 0) { exp--; xtime <<= 3; } return(xtime); } /* * Uid2name -- convert a numeric user i.d. to a login name by looking it * up in /etc/passwd. The login name is returned as a null-terminated, * static string which is overwritten on each call. A name cache speeds * this up many fold (without it, over two-thirds of the user cpu time can * be spent here). */ char *uid2name (uid) register int uid; { struct passwd *getpwuid(), *pw; register struct nc *entry; char *malloc(); register char *name, *temp; register int l; entry = &unc[uid & NC_UIDMASK]; if (entry->nc_name == NULL || entry->nc_n != uid) { entry->nc_n = uid; if (entry->nc_name != NULL) free (entry->nc_name); if ((pw = getpwuid (uid)) == NULL) name = "???"; else name = pw->pw_name; l = 0; for (temp = name; *temp != NULL; temp++) l++; if ((entry->nc_name = malloc (l+1)) == NULL) { fprintf (stderr, "out of memory in uid2name\n"); exit (1); } temp = entry->nc_name; while ((*(temp++) = *(name++)) != NULL) ; } return (entry->nc_name); } /* * Gid2name -- convert a numeric group i.d. to a group name by * looking it up in /etc/group. Similar to uid2name, q.v. */ char *gid2name (gid) register int gid; { struct group *getgrgid(), *gr; register struct nc *entry; char *malloc(); register char *name, *temp; register int l; entry = &gnc[gid & NC_GIDMASK]; if (entry->nc_name == NULL || entry->nc_n != gid) { entry->nc_n = gid; if (entry->nc_name != NULL) free (entry->nc_name); if ((gr = getgrgid (gid)) == NULL) name = "???"; else name = gr->gr_name; l = 0; for (temp = name; *temp != NULL; temp++) l++; if ((entry->nc_name = malloc (l+1)) == NULL) { fprintf (stderr, "out of memory in gid2name\n"); exit (1); } temp = entry->nc_name; while ((*(temp++) = *(name++)) != NULL) ; } return (entry->nc_name); } @//E*O*F pracct.c// chmod u=r,g=r,o=r pracct.c echo x - yesterday.c sed 's/^@//' > "yesterday.c" <<'@//E*O*F yesterday.c//' /* * Yesterday -- print out yesterday's date. */ # ifndef LINT static char rcsid[] = "$Header: yesterday.c,v 1.3 88/01/25 22:57:34 roy Exp $"; # endif /* * $Log: yesterday.c,v $ * Revision 1.3 88/01/25 22:57:34 roy * Removed copyright notice; code placed in public domain. * * Revision 1.2 86/12/09 17:40:44 roy * Fixed typo (prerror->perror) and added missing arguments to printf. * */ # include <sys/time.h> # define SEC_PER_DAY (60*60*24) /* Let's see them change this one! */ char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; char *days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; main () { register struct tm *timebuf; struct timeval t; struct timezone tz; long yesterday; int status; status = 0; if ((status = gettimeofday (&t, &tz)) < 0) { perror ("yesterday: gettimeofday failed"); yesterday = 0; } else { yesterday = t.tv_sec - SEC_PER_DAY; } timebuf = localtime (&yesterday); printf ("%s %s %2d 19%02d\n", days[timebuf->tm_wday], months[timebuf->tm_mon], timebuf->tm_mday, timebuf->tm_year); exit (status); } @//E*O*F yesterday.c// chmod u=r,g=r,o=r yesterday.c echo x - accounting sed 's/^@//' > "accounting" <<'@//E*O*F accounting//' #!/bin/sh # # Generate per-user cpu time stats. Stats are kept in $acctdir/hostname/date. # Note that since we do this a few minutes after midnight, date is yesterday, # not today. If $acctdir/hostname doesn't exist, we create it. # # # Change these to put things in different places # pracct=/usr/local/etc/pracct yesterday=/usr/local/etc/yesterday acctdir=/usr/local/acct # # Make a copy of the accounting file, then do the normal daily summary # and truncate the accounting file. Accounting is temporarily turned # off while we snarf a copy of the file and do the statistics to keep # things in sync. # /usr/etc/accton cp /usr/adm/acct /tmp/acct1.$$ /usr/etc/sa -s > /dev/null rm /usr/adm/acct touch /usr/adm/acct /usr/etc/accton /usr/adm/acct # # Generate the per-user stats. # $pracct -a /tmp/acct1.$$ -f Ux | awk ' {time[$1] = time[$1] + $2} END {for (name in time) print name, time[name]} ' > /tmp/acct2.$$ rm -f /tmp/acct1.$$ # # Make sure a directory exists for this machine. If not, try to create it. # Move daily summary temp file to accounting directory. This must be done # as user "acct" so we can write on the common accounting directory. # su acct << EOF_SU_ACCT if test ! -d $acctdir/`hostname` then mkdir $acctdir/`hostname` fi cp /tmp/acct2.$$ $acctdir/`hostname`/`$yesterday | tr ' ' '-'` EOF_SU_ACCT rm -f /tmp/acct2.$$ @//E*O*F accounting// chmod u=rwx,g=rx,o=rx accounting echo Inspecting for damage in transit... temp=/tmp/shar$$; dtemp=/tmp/.shar$$ trap "rm -f $temp $dtemp; exit" 0 1 2 3 15 cat > $temp <<\!!! 43 405 2316 README 40 114 755 Makefile 135 366 2061 pracct.1 499 1907 11883 pracct.c 59 170 1153 yesterday.c 52 215 1364 accounting 828 3177 19532 total !!! wc README Makefile pracct.1 pracct.c yesterday.c accounting | sed 's=[^ ]*/==' | diff -b $temp - >$dtemp if [ -s $dtemp ] then echo "Ouch [diff of wc output]:" ; cat $dtemp else echo "No problems found." fi exit 0 -- Roy Smith, {allegra,cmcl2,philabs}!phri!roy System Administrator, Public Health Research Institute 455 First Avenue, New York, NY 10016 -- For comp.sources.unix stuff, mail to sources@uunet.uu.net.