File Coverage

blib/lib/Log/ProgramInfo.pm
Criterion Covered Total %
statement 216 237 91.1
branch 57 106 53.7
condition 18 29 62.0
subroutine 22 24 91.6
pod 0 9 0.0
total 313 405 77.2


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.12
10              
11             =cut
12              
13             our $VERSION = '0.1.12';
14              
15 1     1   28803 use feature qw(say);
  1         3  
  1         123  
16 1     1   850 use Data::Dumper;
  1         10735  
  1         82  
17 1     1   684 use FindBin;
  1         1268  
  1         59  
18 1     1   655 use Time::HiRes qw(time);
  1         1537  
  1         5  
19 1     1   1321 use DateTime;
  1         89146  
  1         33  
20 1     1   6 use DateTime::Duration;
  1         2  
  1         17  
21 1     1   4 use Carp qw(carp croak cluck);
  1         1  
  1         60  
22 1     1   4 use Fcntl qw(:flock);
  1         1  
  1         108  
23 1     1   5 use Config;
  1         0  
  1         30  
24 1     1   471 use Digest::SHA;
  1         2024  
  1         915  
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 10810908 my $file = shift;
216 4 50       163 open my $fh, '<', $file or die "Cannot open $file: $!";
217 4         33 return parselog( $fh, @_ );
218             }
219              
220             sub parselog {
221 4     4 0 9 my $fh = shift;
222 4         7 my $accept;
223 4 50       14 $accept = shift if ref($_[0]) eq 'CODE';
224 4         7 my $firstonly = shift;
225 4         5 my @logs;
226 4         18 while (my $log = parse1log( $fh )) {
227 4 50 33     16 next if $accept && ! $accept->($log);
228 4 50       9 return $log if $firstonly;
229 4         19 push @logs, $log;
230             }
231 4         153 return @logs;
232             }
233              
234             sub parse1log {
235 8     8 0 17 my $fh = shift;
236 8         10 my $log;
237 8         116 while (my $line = <$fh>) {
238 2475 100       3922 return $log if $line =~ /^$/;
239 2471 100       2838 next if $line =~ /^########/;
240 2467         1690 chomp $line;
241 2467   100     2587 $log ||= {};
242 2467         3488 my @keys = split ': ', $line;
243 2467         9413 s/^\s*// for @keys;
244 2467         14623 s/\s*$// for @keys;
245 2467 50       3234 die "Unexpected syntax in log line: $line\n" unless scalar(@keys) >= 2;
246 2467         1970 my $val = pop @keys;
247 2467         3021 my $key = shift @keys;
248 2467 100       2533 if (scalar(@keys) == 0) {
249 93 100       110 if ($key eq 'INC') {
250 28   100     60 my $list = $log->{$key} ||= [];
251 28         68 push @$list, $val;
252             }
253             else {
254 65 50       105 die "repeated key: {$key} : line {$line}" if exists $log->{$key};
255 65         243 $log->{$key} = $val;
256             }
257             }
258             else {
259 2374         1546 my $key2 = shift @keys;
260 2374 50       2691 die "invalid nested key: {" . join( '}{', $key, $key2, @keys, $val ) . "}"
261             if scalar(@keys);
262 2374 100       2324 if ($key eq 'MODULE') {
263 880 50       1200 die "Unknown MODULE key ($key2)" unless $modkeys{$key2};
264 880   100     1277 my $list = $log->{$key}{$key2} ||= [];
265 880         2385 push @$list, $val;
266             }
267             else {
268 1494 50       2102 die "repeated key: {$key} {$key2}" if exists $log->{$key}{$key2};
269 1494         4072 $log->{$key}{$key2} = $val;
270             }
271             }
272             }
273 4         10 return $log;
274             }
275              
276             my @extra_loggers = ();
277              
278             sub add_extra_logger {
279 1     1 0 2599 for my $logger (@_) {
280 1 50       20 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 7 my $list = shift;
288 2         2 my @res;
289             my %unique;
290 2   66     5 push @res, ($cache{$_} //= getgrgid $_) for grep { ! $unique{$_}++ } split ' ', $list;
  2         98  
291 2         4 my $g1 = shift @res;
292 2         11 return join( '+', $g1, join( ',', @res ) );
293             }
294              
295             BEGIN {
296 1     1   3 $progbase = $FindBin::Script;
297 1         8 $starttime = DateTime->from_epoch(epoch => time);
298 1         334 $valid_dates{$_} = 1 for qw( date time datetime none );
299 1         479 $uid = getpwuid $<;
300 1         23 my $euid = getpwuid $>;
301 1         4 $gid = groupmap $(;
302 1         2 my $egid = groupmap $);
303 1 50       4 $uid = "$euid($uid)" if $uid ne $euid;
304 1 50       6 $gid = "$egid // $gid" if $egid ne $gid;
305              
306 1         8 %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         6 %_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         5 while( my($k,$v) = each %_omap ) {
326 6 50       21 $env_options{$v} = $ENV{$k} if exists $ENV{$k};
327             }
328 1         18 $SIG{HUP} = \&catch_sig;
329 1         5 $SIG{INT} = \&catch_sig;
330 1         6 $SIG{PIPE} = \&catch_sig;
331 1         4 $SIG{TERM} = \&catch_sig;
332 1         5 $SIG{USR1} = \&catch_sig;
333 1         1583 $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         3 my $key = $1;
342 1 50       2 croak "Option to Log::ProgramInfo requires a value: $_[0]" if scalar(@_) == 1;
343 1         1 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   2269 my $exit_status = $?;
367 1         4 local $?; # protect program exit code from END actions
368 1         6 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 558 my $msg;
382 824   50     714 @_ = map { $_ // 'NO_VALUE_FOUND' } @_;
  2445         3929  
383 824 100       1235 if (@_ == 2 ) {
    50          
384 27         56 $msg = sprintf "%-7s : %s", @_;
385             } elsif (@_ == 3 ) {
386 797         1396 $msg = sprintf "%-7s : %-8s : %s", @_;
387             } else {
388 0         0 my $msg = "log_entry needs 2 or 3 arguments, got "
389             . scalar(@_);
390 0 0       0 $msg .= ': (' . join( '), (', @_ ) . ')' if @_;
391 0         0 cluck $msg;
392             }
393 824         814 _log_entry( $msg );
394             }
395              
396             sub _log_entry {
397 826     826   600 my $msg = shift;
398 826 50       1458 say $logfh $msg if $logfh;
399 826 50       2448 $log->info( $msg ) if $log;
400             }
401              
402             sub finish_log {
403 1 50   1 0 6 return if $kill_caught++; # only write log once - first kill, or termination
404 1         3 my $exit_status = shift;
405             # pull ENV var over-rides
406 1         6 while (my ($k, $v) = each %env_options) {
407 0         0 $option{$k} = $v;
408             }
409 1 50 33     6 if (!$option{suppress} || $option{log}) {
410 1         1 my $endtime;
411 1         2 $log = $option{log};
412 1 50       4 unless ($option{suppress}) {
413 1         20 $endtime = DateTime->from_epoch(epoch => time);
414              
415 1 50       457 if ($option{stdout}) {
416 0         0 open $logfh, ">>&STDOUT";
417             }
418             else {
419 1         3 my $dopt = $option{logdate};
420 1 0       19 my $date =
    50          
    50          
421             ( "none" eq $dopt ) ? ''
422             : ( "date" eq $dopt ) ? $starttime->ymd('')
423             : ( "time" eq $dopt ) ? $starttime->hms('')
424              
425             # : ("datetime" eq $dopt) # validated, so must be 'datetime '
426             : ( $starttime->ymd('') . $starttime->hms('') );
427 1 50       17 $date .= '-' if $date;
428 1 50       5 $option{logext} = ".$option{logext}" if $option{logext} =~ m(^[^.]);
429 1         6 my $log_path = "$option{logdir}/$date$option{logname}$option{logext}";
430 1 50       143 open( $logfh, ">>", $log_path )
431             or carp "cannot open log file $log_path: $!";
432 1         141 say STDERR "Appending log info to $log_path";
433 1         4 my $lock_cnt = 0;
434 1         2 while (1) {
435 1 50       18 flock $logfh, LOCK_EX and last;
436 0 0       0 croak "$0 [$$]: flock failed on $log_path: $!" if $lock_cnt > 30;
437 0 0       0 say STDERR "Waiting for lock on $log_path" unless $lock_cnt++;
438 0         0 print STDERR ".";
439 0         0 sleep(2);
440             }
441 1 50       5 say "" if $lock_cnt;
442 1         10 seek $logfh, 2, 0; # make sure we're still at the end now that it is locked
443             }
444              
445             }
446 1         10 _log_entry( join( ' ', "########", $uid, '(', $gid, ') :', $progbase, @args ) );
447              
448 1         4 my $mod = show_modules();
449 1         44 for my $key ( sort keys %$mod ) {
450 106         83 my ( $ver, $loc ) = @{ $mod->{$key} };
  106         312  
451 106         133 log_entry( MODULE => NAME => $key );
452 106         152 log_entry( MODULE => VERSION => $ver );
453 106         128 log_entry( MODULE => LOC => $loc );
454 106 50       2876 if (open my $fd, '<', $loc) {
455 106         395 my $sum = Digest::SHA->new(256);
456 106         901 $sum->addfile( $fd );
457 106         56893 log_entry( MODULE => SUM => $sum->hexdigest );
458             }
459             }
460 1         21 for my $inc (@INC) {
461 11         11 log_entry( INC => $inc );
462             }
463              
464 5         12662 log_entry( UNAME => $_->[1], do { my $out = qx( uname $_->[0] ); chomp $out; $out } )
  5         33  
  5         88  
465 1         11 for (
466             [ -s => "System" ],
467             [ -n => "Name" ],
468             [ -r => "OSRel" ],
469             [ -v => "OSVer" ],
470             [ -m => "Machine" ]
471             );
472 1         17 my $numproc = 0;
473 1         6 my $procid = 'PROC0';
474 1 50       70 if (open my $cpuinfo, '<', '/proc/cpuinfo') {
475 1         307 for (<$cpuinfo>) {
476 416         295 chomp;
477 416 100       704 next if /^\s*$/;
478 400         1009 my ($k, $v) = split /\s*:\s*/, $_, 2;
479 400 100       500 if ($k eq 'processor') {
480 16         19 $procid = "PROC$numproc";
481 16         20 ++$numproc;
482             }
483             else {
484 384 100       823 log_entry( $procid, $k, $v ) if $v =~ /\S/;
485             }
486             }
487 1         25 log_entry( PROCs, $numproc );
488             }
489 1         4 log_entry( PERL => $^X );
490 1         5 log_entry( PERLVer => $] );
491 1 50       49 if (open my $fd, '<', $^X) {
492 1         22 my $sum = Digest::SHA->new(256);
493 1         68 $sum->addfile( $fd );
494 1         15982 log_entry( PERLSUM => $sum->hexdigest );
495             }
496 1         11 log_entry( libc => $Config{libc} );
497 1 50       56 if (open my $fd, '<', $Config{libc}) {
498 0         0 my $sum = Digest::SHA->new(256);
499 0         0 $sum->addfile( $fd );
500 0         0 log_entry( libcSUM => $sum->hexdigest );
501             }
502 1         7 log_entry( User => $uid );
503 1         3 log_entry( Group => $gid );
504 1         3 my $progdir = $FindBin::Bin;
505 1         5 log_entry( ProgDir => $progdir );
506 1         4 log_entry( Program => $progbase );
507 1   50     13 log_entry( Version => ( $::VERSION // "(No VERSION)" ) );
508 1 50       70 if (open my $fd, '<', "$progdir/$progbase") {
509 1         11 my $sum = Digest::SHA->new(256);
510 1         18 $sum->addfile( $fd );
511 1         79 log_entry( ProgSUM => $sum->hexdigest );
512             }
513 1         12 log_entry( Args => scalar(@args) );
514 1         2 my $acnt = 0;
515 1         3 log_entry( " arg" => sprintf("%8d", ++$acnt), $args[$acnt-1] ) for @args;
516 1         11 log_entry( Start => $starttime->datetime() . "." . sprintf( "%03d", $starttime->millisecond ) );
517 1         5 log_entry( End => $endtime->datetime() . "." . sprintf( "%03d", $endtime->millisecond ) );
518 1         9 my $dur = $endtime->subtract_datetime_absolute($starttime);
519 1         220 log_entry( Elapsed => $dur->delta_seconds . "." .
520             sprintf( "%03d", $dur->delta_nanoseconds/1_000_000) );
521 1         4 log_entry( EndStat => $exit_status );
522              
523 1     0   2 $_->(sub { log_entry( @_ ) }) for @extra_loggers;
  0         0  
524              
525 1         3 _log_entry ""; # blank line to separate any appended later log
526              
527 1 50       338 close $logfh if $logfh;
528             }
529             }
530              
531             # Print version and loading path information for modules
532             sub show_modules {
533 1     1 0 2 my $module_infos = {};
534              
535             # %INC looks like this:
536             # {
537             # ...
538             # "Data/Dump.pm"
539             # => "/whatever/perl/lib/site_perl/5.18.1/Data/Dump.pm",
540             # ...
541             # }
542             # So let's convert it to this:
543             # {
544             # ...
545             # "Data::Dump"
546             # => [ "1.4.2",
547             # "/whatever/perl/lib/site_perl/5.18.1/Data/Dump.pm",
548             # ],
549             # ...
550             # }
551 1         113 foreach my $module_inc_name ( keys(%INC) ) {
552 106         77 my $real_name = $module_inc_name;
553 106         170 $real_name =~ s|/|::|g;
554 106         172 $real_name =~ s|\.pm$||;
555              
556 106         1131 my $version = eval { $real_name->VERSION }
557 106   66     76 // eval { ${"${real_name}::VERSION"} }
  25   100     19  
  25         125  
558             // 'unknown';
559             # stringify, in case it is a weird format
560             # - I don't think the 'invalid' alternative can be hit, but safer to have it in
561 106   50     116 $version = eval { $version . '' } // 'invalid';
  106         206  
562              
563 106         336 $module_infos->{$real_name} = [ $version, $INC{$module_inc_name} ];
564             }
565              
566 1         7 return $module_infos;
567             }
568              
569             1;