File Coverage

blib/lib/Log/ProgramInfo.pm
Criterion Covered Total %
statement 219 240 91.2
branch 58 108 53.7
condition 17 27 62.9
subroutine 22 24 91.6
pod 0 9 0.0
total 316 408 77.4


line stmt bran cond sub pod time code
1             package Log::ProgramInfo;
2              
3             =head1 NAME
4              
5             Log::ProgramInfo - log global info from a perl programs.
6              
7             =head1 VERSION
8              
9             Version 0.1.13
10              
11             =cut
12              
13             our $VERSION = '0.1.13';
14              
15 1     1   18103 use feature qw(say);
  1         2  
  1         91  
16 1     1   530 use Data::Dumper;
  1         6742  
  1         74  
17 1     1   494 use FindBin;
  1         754  
  1         36  
18 1     1   489 use Time::HiRes qw(time);
  1         1077  
  1         4  
19 1     1   874 use DateTime;
  1         76892  
  1         37  
20 1     1   7 use DateTime::Duration;
  1         2  
  1         19  
21 1     1   4 use Carp qw(carp croak cluck);
  1         1  
  1         83  
22 1     1   4 use Fcntl qw(:flock);
  1         1  
  1         181  
23 1     1   7 use Config;
  1         2  
  1         57  
24 1     1   470 use Digest::SHA;
  1         2018  
  1         840  
25              
26              
27             =head1 SYNOPSIS
28              
29             use Log::ProgramInfo qw(
30             [ -logname LOGNAME ]
31             [ -logdir LOGDIR ]
32             [ -logext LOGEXT ]
33             [ -logdate none|date|time|datetime ]
34             [ -stdout ]
35             [ -suppress ]
36             [ -log $log4perl_log ]
37             );
38              
39             # main program does lots of stuff...
40             exit 0;
41              
42             After the program has run, this module will automatically
43             log information about this run into a log file and/or to a
44             log object. It will list such things as:
45             - program
46             - name
47             - version
48             - command line arguments
49             - version of perl
50             - modules loaded
51             - source code location
52             - Version
53             - run time
54              
55             If a -log parameter is provided, it should be a log object that provides
56             an info method (i.e. a Log4Perl object is likely). The info will be sent
57             sent to this log in addition to writing it to a log file. This logging
58             is not affected by the -suppress attribute - use that if you don't want a
59             file log written too.
60              
61             Warning, the log parser will have to be modified if you need to parse this
62             info out of a log4perl log - there is extra text in the lines (and any
63             other logging from the program) which will need to be pruned.
64              
65             The log is appended to the file whose name is determined by:
66             LOGDIR/LOGDATE-LOGNAME.LOGEXT
67             where
68             LOGDIR defaults to . (the current directory when the program terminates)
69             LOGDATE defaults to the date that the program was started
70             LOGNAME defaults to the basename of the program
71             LOGEXT defaults to ".programinfo"
72              
73             The -ARG specifiers in the "import" list can be used to over-ride these defaults. Specifying:
74              
75             -logname LOGNAME will use LOGNAME instead of the program name
76             -logdir LOGDIR will use LOGDIR instead of the current directory
77             - if it is a relative path, it will be based on the
78             current directory at termination of execution
79             -logext EXT will add .EXT to the log filename
80             -logext .EXT will add .EXT to the log filename
81             -logext "" will add no extension to the log filename
82             -logdate STRING
83             will specify the LOGDATE portion of the filename. The STRING can be:
84             none LOGNAME (and no dash)
85             date YYYYMMDD-LOGNAME (this is the default)
86             time HHMMSS-LOGNAME
87             datetime YYYYMMDDHHMMSS-LOGNAME
88              
89             -stdout will cause the log to be sent to stdout instead of a file
90             -suppress will suppress logging (unless environment variable
91             LOGPROGRAMINFO_SUPPRESS is explcitly set to 0 or null)
92              
93             Normally, neither -suppress nor -stdout will be set in the
94             use statement, and the environment variables can then be
95             used to disable the logging completely or to send it to
96             stdout instead of to the selected file.
97              
98             For some programs, however, it may be desired to not normally
99             provide any logging. Specifying -suppress will accomplish
100             this. In such a case, setting the environment variable
101             LOGPROGRAMINFO_SUPPRESS=0 will over-ride that choice, causing
102             the log to be written (as specified by the other options
103             and environment variables).
104              
105             Environment variables can over-ride these parameters:
106             LOGPROGRAMINFO_SUPPRESS=x boolean suppresses all logging if true
107             LOGPROGRAMINFO_STDOUT=x boolean sets -stdout
108             LOGPROGRAMINFO_DIR=DIR string sets the target directory name
109             LOGPROGRAMINFO_NAME=NAME string sets the target filename LOGNAME
110             LOGPROGRAMINFO_EXT=EXT string sets the target extension
111             LOGPROGRAMINFO_DATE=DATE string sets the target filename LOGDATE selector
112              
113             (there is no environment variable for setting the log attribute, that
114             can only be done within the program)
115              
116             Adding extra loggable information:
117             If you want to add your own classes of loggable info, there are a
118             few restrictions.
119              
120             You define a logging extension routine using:
121              
122             Log::ProgramInfo::add_extra_logger( \&my_logger );
123              
124             Your logger routine should be defined as:
125              
126             sub my_logger {
127             my $write_entry = shift;
128             $write_entry->( $key1, $value );
129             $write_entry->( $key1, $key2, $value );
130             }
131              
132             The $write_entry function passed to my_logger must be called with
133             2 or 3 arguments. The leading arguments are major (and minor if
134             desired) keys, the final one is the value for the key(s).
135              
136             Try to keep the first key to 7 characters, and the second to 8 to
137             keep the log readable by humans. (It will be parseable even if you
138             use longer keys.)
139              
140             Help improve the world! If you are writing additional classes of
141             info loggers, please consider whether they are truly unique to your
142             own environment. If there is a chance that they would be useful to
143             other environments, please be encouraged to send your logger to be
144             included into Log::ProgramInfo as either a standard default logger
145             or as an available optional logger.
146              
147             Parsing the log file:
148             The log file is designed to be easily parsed.
149              
150             A log always starts with a line beginning with 8 hash marks in column
151             one (########) plus some identifying info.
152              
153             The value lines are of the form:
154              
155             key : value
156             key1 : key2 : value
157              
158             The first key is extended to at least 7 characters with blanks, the
159             second key (if any) is extended to at least 8 characters. There is
160             always a separator of (space(colon)(space) between a key and the
161             following field. (A key can be provided with leading spaces for making
162             the log more readable by humans - the readlog function in the test suite
163             will remove such spaces.)
164              
165             Two subroutines are available to do this parsing for you:
166              
167             my $firstonly = 0;
168             @logs = readlog( $filepath [, $acceptsub] [, $firstonly] );
169             @logs = parselog( $filehandle [, $acceptsub] [, $firstonly] );
170              
171             $logs = readlog( $filepath [, $acceptsub ], 1 );
172             $logs = parselog( $filehandle [, $acceptsub ] ,1 );
173              
174             The first parameter to each is either a string pathname (for readlog)
175             or an already opened readable file handle (for parselog).
176              
177             If a subroutine reference arg $acceptsub is provided, each log that is
178             read will be passed to that sub reference. If the acceptsub returns
179             true the log is retained, otherwise it is discarded. If a trailing
180             (non-sub-ref) value is provided, it selects whether only the first
181             (acceptable) log found will be returned as a single hash reference, or
182             whether all of the (accepted) logs in the file will be returned as a
183             list of hash references.`
184              
185             The hash reference for each accepted log contains the key/value (or
186             key1 => { key2/value pairs }) from that log.
187              
188             Whenever a key (or key1/key2 pair) is seen multiple times, the value
189             is an array ref instead of a scalar. This only happens with the
190             MODULE pairs (MODULE/NAME, MODULE/LOC, MODULE/VERSION), and the INC
191             key. (User-provided keys are not currently permitted to use the same
192             key set multiple times.)
193              
194             =cut
195              
196             # preserve command line info
197             my @args = @ARGV;
198             my $progbase;
199             my $starttime;
200              
201             my %option;
202              
203             my %valid_dates;
204              
205             my %_omap;
206             my %env_options;
207              
208             my $kill_caught;
209              
210             my ($uid, $gid);
211             my %cache;
212             my %modkeys = ( NAME => 1, VERSION => 1, LOC => 1, SUM => 1 );
213              
214             sub readlog {
215 4     4 0 10718735 my $file = shift;
216 4 50       183 open my $fh, '<', $file or die "Cannot open $file: $!";
217 4         32 return parselog( $fh, @_ );
218             }
219              
220             sub parselog {
221 4     4 0 21 my $fh = shift;
222 4         7 my $accept;
223 4 50       16 $accept = shift if ref($_[0]) eq 'CODE';
224 4         9 my $firstonly = shift;
225 4         6 my @logs;
226 4         19 while (my $log = parse1log( $fh )) {
227 4 50 33     13 next if $accept && ! $accept->($log);
228 4 50       9 return $log if $firstonly;
229 4         22 push @logs, $log;
230             }
231 4         143 return @logs;
232             }
233              
234             sub parse1log {
235 8     8 0 12 my $fh = shift;
236 8         12 my $log;
237 8         149 while (my $line = <$fh>) {
238 2475 100       3779 return $log if $line =~ /^$/;
239 2471 100       2939 next if $line =~ /^########/;
240 2467         1663 chomp $line;
241 2467   100     2760 $log ||= {};
242 2467         3823 my @keys = split ': ', $line;
243 2467         9376 s/^\s*// for @keys;
244 2467         14658 s/\s*$// for @keys;
245 2467 50       3239 die "Unexpected syntax in log line: $line\n" unless scalar(@keys) >= 2;
246 2467         2031 my $val = pop @keys;
247 2467         1730 my $key = shift @keys;
248 2467 100       2642 if (scalar(@keys) == 0) {
249 93 100       98 if ($key eq 'INC') {
250 28   100     84 my $list = $log->{$key} ||= [];
251 28         85 push @$list, $val;
252             }
253             else {
254 65 50       92 die "repeated key: {$key} : line {$line}" if exists $log->{$key};
255 65         233 $log->{$key} = $val;
256             }
257             }
258             else {
259 2374         1484 my $key2 = shift @keys;
260 2374 50       2811 die "invalid nested key: {" . join( '}{', $key, $key2, @keys, $val ) . "}"
261             if scalar(@keys);
262 2374 100       2323 if ($key eq 'MODULE') {
263 880 50       1285 die "Unknown MODULE key ($key2)" unless $modkeys{$key2};
264 880   100     1413 my $list = $log->{$key}{$key2} ||= [];
265 880         2866 push @$list, $val;
266             }
267             else {
268 1494 50       2095 die "repeated key: {$key} {$key2}" if exists $log->{$key}{$key2};
269 1494         3919 $log->{$key}{$key2} = $val;
270             }
271             }
272             }
273 4         13 return $log;
274             }
275              
276             my @extra_loggers = ();
277              
278             sub add_extra_logger {
279 1     1 0 2428 for my $logger (@_) {
280 1 50       15 croak "arg to extra_loggers is not a code ref: " . Dumper($logger)
281             unless ref $logger eq 'CODE';
282 0         0 push @extra_loggers, $logger;
283             }
284             }
285              
286             sub groupmap {
287 2     2 0 9 my $list = shift;
288 2         2 my @res;
289             my %unique;
290 2   66     4 push @res, ($cache{$_} //= getgrgid $_) for grep { ! $unique{$_}++ } split ' ', $list;
  2         102  
291 2         4 my $g1 = shift @res;
292 2         10 return join( '+', $g1, join( ',', @res ) );
293             }
294              
295             BEGIN {
296 1     1   2 $progbase = $FindBin::Script;
297 1         9 $starttime = DateTime->from_epoch(epoch => time);
298 1         317 $valid_dates{$_} = 1 for qw( date time datetime none );
299 1         524 $uid = getpwuid $<;
300 1         23 my $euid = getpwuid $>;
301 1         3 $gid = groupmap $(;
302 1         1 my $egid = groupmap $);
303 1 50       6 $uid = "$euid($uid)" if $uid ne $euid;
304 1 50       2 $gid = "$egid // $gid" if $egid ne $gid;
305              
306 1         6 %option = (
307             suppress => 0,
308             stdout => 0,
309             logdir => ".",
310             logdate => "date",
311             logname => $progbase,
312             logext => ".programinfo",
313             log => undef,
314             );
315              
316 1         5 %_omap = (
317             LOGPROGRAMINFO_SUPPRESS => 'suppress',
318             LOGPROGRAMINFO_STDOUT => 'stdout',
319             LOGPROGRAMINFO_DIR => 'logdir',
320             LOGPROGRAMINFO_DATE => 'logdate',
321             LOGPROGRAMINFO_NAME => 'logname',
322             LOGPROGRAMINFO_EXT => 'logext',
323             );
324              
325 1         4 while( my($k,$v) = each %_omap ) {
326 6 50       16 $env_options{$v} = $ENV{$k} if exists $ENV{$k};
327             }
328 1         17 $SIG{HUP} = \&catch_sig;
329 1         3 $SIG{INT} = \&catch_sig;
330 1         4 $SIG{PIPE} = \&catch_sig;
331 1         3 $SIG{TERM} = \&catch_sig;
332 1         3 $SIG{USR1} = \&catch_sig;
333 1         1658 $SIG{USR2} = \&catch_sig;
334             }
335              
336             sub import {
337 1     1   12 my $mod = shift;
338              
339 1         3 while (scalar(@_)) {
340 1 50       8 if ($_[0] =~ /^-(logname|logdir|logext|logdate)$/) {
    0          
341 1         2 my $key = $1;
342 1 50       3 croak "Option to Log::ProgramInfo requires a value: $_[0]" if scalar(@_) == 1;
343 1         2 shift;
344 1         1 my $val = shift;
345 1         3 $option{$key} = $val;
346             }
347             elsif ($_[0] =~ /^-(stdout|suppress)$/) {
348 0         0 my $key = $1;
349 0         0 shift;
350 0         0 $option{$key} = 1;
351             }
352             else {
353 0         0 last;
354             }
355             }
356              
357 1 50 33     4 croak "Unknown option to Log::ProgramInfo: $_[0]" if (@_ and $_[0] =~ /^-/);
358 1 50       2 croak "Import arguments not supported from Log::ProgramInfo: " . join( ',', @_ ) if @_;
359             croak "Unknown logdate option: $option{logdate}"
360 1 50       3 unless exists $valid_dates{ $option{logdate} };
361              
362 1 50       11 say STDERR "resolved option hash: ", Dumper(\%option) if $ENV{DUMP_LOG_IMPORTS};
363             }
364              
365             END {
366 1     1   2412 my $exit_status = $?;
367 1         3 local $?; # protect program exit code from END actions
368 1         7 finish_log($exit_status);
369             }
370              
371             sub catch_sig {
372 0     0 0 0 my $signame = shift;
373 0         0 local $?; # protect program exit code from END actions
374 0         0 finish_log("Killed with signal: $signame");
375             }
376              
377             my $logfh;
378             my $log;
379              
380             sub log_entry {
381 824     824 0 637 my $msg;
382 824         1164 my @vals = @_;
383 824         936 for (@vals) {
384 2445 50       2788 $_ = 'NO_VALUE_FOUND' unless length($_);
385 2445         1759 s/\t//g;
386 2445         2191 s/\n//g;
387             }
388 824 100       1242 if (@vals == 2 ) {
    50          
389 27         62 $msg = sprintf "%-7s : %s", @vals;
390             } elsif (@vals == 3 ) {
391 797         1607 $msg = sprintf "%-7s : %-8s : %s", @vals;
392             } else {
393 0         0 my $msg = "log_entry needs 2 or 3 arguments, got "
394             . scalar(@vals);
395 0 0       0 $msg .= ': (' . join( '), (', @vals ) . ')' if @vals;
396 0         0 cluck $msg;
397             }
398 824         866 _log_entry( $msg );
399             }
400              
401             sub _log_entry {
402 826     826   641 my $msg = shift;
403 826 50       1633 say $logfh $msg if $logfh;
404 826 50       2378 $log->info( $msg ) if $log;
405             }
406              
407             sub finish_log {
408 1 50   1 0 5 return if $kill_caught++; # only write log once - first kill, or termination
409 1         2 my $exit_status = shift;
410             # pull ENV var over-rides
411 1         7 while (my ($k, $v) = each %env_options) {
412 0         0 $option{$k} = $v;
413             }
414 1 50 33     7 if (!$option{suppress} || $option{log}) {
415 1         2 my $endtime;
416 1         3 $log = $option{log};
417 1 50       3 unless ($option{suppress}) {
418 1         19 $endtime = DateTime->from_epoch(epoch => time);
419              
420 1 50       438 if ($option{stdout}) {
421 0         0 open $logfh, ">>&STDOUT";
422             }
423             else {
424 1         6 my $dopt = $option{logdate};
425 1 0       20 my $date =
    50          
    50          
426             ( "none" eq $dopt ) ? ''
427             : ( "date" eq $dopt ) ? $starttime->ymd('')
428             : ( "time" eq $dopt ) ? $starttime->hms('')
429              
430             # : ("datetime" eq $dopt) # validated, so must be 'datetime '
431             : ( $starttime->ymd('') . $starttime->hms('') );
432 1 50       18 $date .= '-' if $date;
433 1 50       7 $option{logext} = ".$option{logext}" if $option{logext} =~ m(^[^.]);
434 1         5 my $log_path = "$option{logdir}/$date$option{logname}$option{logext}";
435 1 50       148 open( $logfh, ">>", $log_path )
436             or carp "cannot open log file $log_path: $!";
437 1         96 say STDERR "Appending log info to $log_path";
438 1         3 my $lock_cnt = 0;
439 1         1 while (1) {
440 1 50       13 flock $logfh, LOCK_EX and last;
441 0 0       0 croak "$0 [$$]: flock failed on $log_path: $!" if $lock_cnt > 30;
442 0 0       0 say STDERR "Waiting for lock on $log_path" unless $lock_cnt++;
443 0         0 print STDERR ".";
444 0         0 sleep(2);
445             }
446 1 50       3 say "" if $lock_cnt;
447 1         6 seek $logfh, 2, 0; # make sure we're still at the end now that it is locked
448             }
449              
450             }
451 1         9 _log_entry( join( ' ', "########", $uid, '(', $gid, ') :', $progbase, @args ) );
452              
453 1         3 my $mod = show_modules();
454 1         69 for my $key ( sort keys %$mod ) {
455 106         77 my ( $ver, $loc ) = @{ $mod->{$key} };
  106         284  
456 106         131 log_entry( MODULE => NAME => $key );
457 106         111 log_entry( MODULE => VERSION => $ver );
458 106         115 log_entry( MODULE => LOC => $loc );
459 106 50       2930 if (open my $fd, '<', $loc) {
460 106         406 my $sum = Digest::SHA->new(256);
461 106         920 $sum->addfile( $fd );
462 106         56914 log_entry( MODULE => SUM => $sum->hexdigest );
463             }
464             }
465 1         29 for my $inc (@INC) {
466 11         13 log_entry( INC => $inc );
467             }
468              
469 5         9298 log_entry( UNAME => $_->[1], do { my $out = qx( uname $_->[0] ); chomp $out; $out } )
  5         43  
  5         68  
470 1         12 for (
471             [ -s => "System" ],
472             [ -n => "Name" ],
473             [ -r => "OSRel" ],
474             [ -v => "OSVer" ],
475             [ -m => "Machine" ]
476             );
477 1         14 my $numproc = 0;
478 1         3 my $procid = 'PROC0';
479 1 50       58 if (open my $cpuinfo, '<', '/proc/cpuinfo') {
480 1         303 for (<$cpuinfo>) {
481 416         311 chomp;
482 416 100       726 next if /^\s*$/;
483 400         1062 my ($k, $v) = split /\s*:\s*/, $_, 2;
484 400 100       550 if ($k eq 'processor') {
485 16         19 $procid = "PROC$numproc";
486 16         22 ++$numproc;
487             }
488             else {
489 384 100       887 log_entry( $procid, $k, $v ) if $v =~ /\S/;
490             }
491             }
492 1         37 log_entry( PROCs, $numproc );
493             }
494 1         5 log_entry( PERL => $^X );
495 1         64 log_entry( PERLVer => $] );
496 1 50       94 if (open my $fd, '<', $^X) {
497 1         29 my $sum = Digest::SHA->new(256);
498 1         26 $sum->addfile( $fd );
499 1         17007 log_entry( PERLSUM => $sum->hexdigest );
500             }
501 1         15 log_entry( libc => $Config{libc} );
502 1 50       58 if (open my $fd, '<', $Config{libc}) {
503 0         0 my $sum = Digest::SHA->new(256);
504 0         0 $sum->addfile( $fd );
505 0         0 log_entry( libcSUM => $sum->hexdigest );
506             }
507 1         6 log_entry( User => $uid );
508 1         4 log_entry( Group => $gid );
509 1         4 my $progdir = $FindBin::Bin;
510 1         3 log_entry( ProgDir => $progdir );
511 1         5 log_entry( Program => $progbase );
512 1   50     14 log_entry( Version => ( $::VERSION // "(No VERSION)" ) );
513 1 50       68 if (open my $fd, '<', "$progdir/$progbase") {
514 1         13 my $sum = Digest::SHA->new(256);
515 1         18 $sum->addfile( $fd );
516 1         82 log_entry( ProgSUM => $sum->hexdigest );
517             }
518 1         8 log_entry( Args => scalar(@args) );
519 1         2 my $acnt = 0;
520 1         3 log_entry( " arg" => sprintf("%8d", ++$acnt), $args[$acnt-1] ) for @args;
521 1         12 log_entry( Start => $starttime->datetime() . "." . sprintf( "%03d", $starttime->millisecond ) );
522 1         6 log_entry( End => $endtime->datetime() . "." . sprintf( "%03d", $endtime->millisecond ) );
523 1         9 my $dur = $endtime->subtract_datetime_absolute($starttime);
524 1         229 log_entry( Elapsed => $dur->delta_seconds . "." .
525             sprintf( "%03d", $dur->delta_nanoseconds/1_000_000) );
526 1         6 log_entry( EndStat => $exit_status );
527              
528 1     0   2 $_->(sub { log_entry( @_ ) }) for @extra_loggers;
  0         0  
529              
530 1         3 _log_entry ""; # blank line to separate any appended later log
531              
532 1 50       348 close $logfh if $logfh;
533             }
534             }
535              
536             # Print version and loading path information for modules
537             sub show_modules {
538 1     1 0 2 my $module_infos = {};
539              
540             # %INC looks like this:
541             # {
542             # ...
543             # "Data/Dump.pm"
544             # => "/whatever/perl/lib/site_perl/5.18.1/Data/Dump.pm",
545             # ...
546             # }
547             # So let's convert it to this:
548             # {
549             # ...
550             # "Data::Dump"
551             # => [ "1.4.2",
552             # "/whatever/perl/lib/site_perl/5.18.1/Data/Dump.pm",
553             # ],
554             # ...
555             # }
556 1         122 foreach my $module_inc_name ( keys(%INC) ) {
557 106         89 my $real_name = $module_inc_name;
558 106         168 $real_name =~ s|/|::|g;
559 106         177 $real_name =~ s|\.pm$||;
560              
561 106         1179 my $version = eval { $real_name->VERSION }
562 106   66     82 // eval { ${"${real_name}::VERSION"} }
  25   100     18  
  25         117  
563             // 'unknown';
564             # stringify, in case it is a weird format
565             # - I don't think the 'invalid' alternative can be hit, but safer to have it in
566 106   50     127 $version = eval { $version . '' } // 'invalid';
  106         191  
567              
568 106         324 $module_infos->{$real_name} = [ $version, $INC{$module_inc_name} ];
569             }
570              
571 1         8 return $module_infos;
572             }
573              
574             1;