File Coverage

blib/lib/No/Worries/Log.pm
Criterion Covered Total %
statement 302 352 85.8
branch 61 90 67.7
condition 23 36 63.8
subroutine 65 75 86.6
pod 14 14 100.0
total 465 567 82.0


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: No/Worries/Log.pm #
4             # #
5             # Description: logging without worries #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package No::Worries::Log;
14 2     2   55159 use strict;
  2         11  
  2         51  
15 2     2   7 use warnings;
  2         4  
  2         128  
16             our $VERSION = "1.6";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.17 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 2     2   963 use IO::Handle qw();
  2         10186  
  2         47  
24 2     2   336 use No::Worries qw($HostName $ProgramName);
  2         4  
  2         12  
25 2     2   725 use No::Worries::Date qw(date_stamp);
  2         6  
  2         12  
26 2     2   11 use No::Worries::Export qw(export_control);
  2         3  
  2         27  
27 2     2   796 use No::Worries::File qw(file_read);
  2         6  
  2         12  
28 2     2   12 use No::Worries::Die qw(dief);
  2         4  
  2         12  
29              
30             #
31             # constants
32             #
33              
34 2     2   11 use constant _LEVEL_ERROR => "error";
  2         4  
  2         125  
35 2     2   11 use constant _LEVEL_WARNING => "warning";
  2         4  
  2         91  
36 2     2   9 use constant _LEVEL_INFO => "info";
  2         13  
  2         110  
37 2     2   11 use constant _LEVEL_DEBUG => "debug";
  2         4  
  2         79  
38 2     2   9 use constant _LEVEL_TRACE => "trace";
  2         3  
  2         5496  
39              
40             #
41             # global variables
42             #
43              
44             our($Handler);
45              
46             our(
47             %_KnownLevel, # hash with known levels
48             %_InterestingLevel, # hash with interesting levels
49             %_Level2Char, # hash mapping levels to chars for the output
50             $_MaybeInterestingInfo, # filtering sub (partial)
51             $_InterestingInfo, # filtering sub (complete)
52             $_ConfigTag, # dev:ino:mtime of the last configuration file used
53             );
54              
55             #+++############################################################################
56             # #
57             # configuring #
58             # #
59             #---############################################################################
60              
61             #
62             # configure the module from the given file (if needed)
63             #
64              
65             sub log_configure ($) {
66 0     0 1 0 my($path) = @_;
67 0         0 my(@stat, $tag);
68              
69 0 0       0 @stat = stat($path) or dief("cannot stat(%s): %s", $path, $!);
70 0         0 $tag = join(":", $stat[0], $stat[1], $stat[9]);
71 0 0 0     0 return(0) if $_ConfigTag and $_ConfigTag eq $tag;
72 0         0 log_filter(file_read($path));
73 0         0 $_ConfigTag = $tag;
74 0         0 return(1);
75             }
76              
77             #+++############################################################################
78             # #
79             # filtering #
80             # #
81             #---############################################################################
82              
83             #
84             # return the Perl code to use for a given filtering expression
85             #
86              
87             sub _expr_code ($@) {
88 46     46   84 my($partial, $attr, $op, $value) = @_;
89              
90             # for partial filtering, we do not care about the message
91 46 100 100     695 return("1") if $attr eq "message" and $partial;
92             # for the attributes we know about, it is easy
93 44 100       197 return("\$info->{$attr} $op $value")
94             if $attr =~ /^(level|time|program|host|file|line|sub|message)$/;
95             # for the other attributes, the test always fails if not defined
96 14         87 return("(defined(\$info->{$attr}) and \$info->{$attr} $op $value)");
97             }
98              
99             #
100             # compile the given filter
101             #
102              
103             sub _compile_filter ($@) {
104 20     20   42 my($partial, @filter) = @_;
105 20         27 my(@code, $code, $sub);
106              
107 20         36 @code = (
108             "package No::Worries::Log::Filter;",
109             "use strict;",
110             "use warnings;",
111             "\$sub = sub {",
112             " my(\$info) = \@_;",
113             " return(1) if",
114             );
115 20         32 foreach my $expr (@filter) {
116 72 100       126 if (ref($expr) eq "ARRAY") {
117 46         49 push(@code, " " . _expr_code($partial, @{ $expr }));
  46         78  
118             } else {
119 26         35 push(@code, " $expr");
120             }
121             }
122 20         39 $code[-1] .= ";";
123 20         64 push(@code,
124             " return(0);",
125             "}",
126             );
127 20         76 $code = join("\n", @code);
128 1     1   7 eval($code); ## no critic 'BuiltinFunctions::ProhibitStringyEval'
  1     1   2  
  1     1   20  
  1     1   4  
  1     1   2  
  1     1   138  
  1     1   11  
  1     1   3  
  1     1   26  
  1     1   4  
  1     1   2  
  1     1   100  
  1     1   6  
  1     1   2  
  1     1   26  
  1     1   5  
  1     1   1  
  1     1   98  
  1     1   6  
  1     1   2  
  1     1   26  
  1     1   5  
  1     1   2  
  1     1   85  
  1     1   5  
  1     1   2  
  1     1   18  
  1     1   4  
  1     1   2  
  1     1   71  
  1     1   5  
  1     1   2  
  1     1   15  
  1     1   4  
  1     1   2  
  1     1   58  
  1     1   6  
  1     1   2  
  1     1   20  
  1     1   4  
  1         1  
  1         84  
  1         6  
  1         2  
  1         19  
  1         4  
  1         2  
  1         89  
  1         7  
  1         1  
  1         26  
  1         5  
  1         1  
  1         79  
  1         5  
  1         2  
  1         26  
  1         5  
  1         1  
  1         82  
  1         6  
  1         2  
  1         17  
  1         4  
  1         2  
  1         82  
  1         6  
  1         2  
  1         26  
  1         5  
  1         2  
  1         97  
  1         6  
  1         2  
  1         17  
  1         3  
  1         2  
  1         86  
  1         6  
  1         2  
  1         17  
  1         4  
  1         2  
  1         87  
  1         7  
  1         1  
  1         19  
  1         4  
  1         1  
  1         77  
  1         5  
  1         1  
  1         17  
  1         5  
  1         1  
  1         61  
  1         7  
  1         2  
  1         351  
  1         7  
  1         2  
  1         69  
  1         6  
  1         3  
  1         19  
  1         5  
  1         1  
  1         78  
  1         6  
  1         2  
  1         28  
  1         5  
  1         1  
  1         95  
  1         6  
  1         2  
  1         17  
  1         4  
  1         2  
  1         90  
  20         1242  
129 20 50       59 dief("invalid code built: %s", $@) if $@;
130 20         61 return($sub);
131             }
132              
133             #
134             # parse a single filtering expression
135             #
136              
137             sub _parse_expr ($$$) {
138 27     27   42 my($line, $level, $expr) = @_;
139 27         32 my($attr, $op, $value);
140              
141             # we first parse the (attr, op, value) triplet
142 27 100 66     133 if ($_KnownLevel{$expr}) {
    100 100        
    100          
143             # there can be only one level per filter line and we keep track of it
144 14 100       16 dief("invalid filter line: %s", $line) if ${ $level };
  14         29  
145 13         14 ${ $level } = $expr;
  13         20  
146 13         21 ($attr, $op, $value) = ("level", "==", $expr);
147             } elsif ($expr =~ /^(\w+)(==|!=)$/ and $1 ne "level") {
148             # special case for comparison with empty string
149 1         4 ($attr, $op, $value) = ($1, $2, "");
150             } elsif ($expr =~ /^(\w+)(==|!=|=~|!~|>=?|<=?)(\S+)$/ and $1 ne "level") {
151             # normal case
152 10         25 ($attr, $op, $value) = ($1, $2, $3);
153             } else {
154 2         9 dief("invalid filter expression: %s", $expr);
155             }
156             # we then check the value
157 24 100 100     81 if ($op eq "=~" or $op eq "!~") {
    100 100        
158             # match: check that the value is a valid regular expression
159 4         5 eval { $expr =~ /$value/ };
  4         45  
160 4 50       8 dief("invalid regexp: %s", $value) if $@;
161 4         8 $value = "m\0$value\0";
162             } elsif ($op eq "==" or $op eq "!=") {
163             # equality: adjust according to type
164 17 100       45 unless ($value =~ /^-?\d+$/) {
165 15 50       26 $op = $op eq "==" ? "eq" : "ne";
166 15         24 $value = "qq\0$value\0";
167             }
168             } else {
169             # numerical: check that the value is a valid integer
170 3 50       20 dief("invalid integer: %s", $value) unless $value =~ /^-?\d+$/;
171             }
172             # so far, so good
173 24         59 return([ $attr, $op, $value ]);
174             }
175              
176             #
177             # parse and compile the filter to use
178             #
179              
180             sub log_filter ($) {
181 13     13 1 5399 my($filter) = @_;
182 13         22 my($and_re, $or_re, $level, @list, @filter, %il, $ii, $mii);
183              
184             # strip comments and empty lines and extra spaces
185 13         19 @list = ();
186 13         36 foreach my $line (split(/\n/, $filter)) {
187 19         40 $line =~ s/^\s+//;
188 19         40 $line =~ s/\s+$//;
189 19         47 $line =~ s/\s+/ /g;
190 19 100 100     71 next if $line eq "" or $line =~ /^\#/;
191 15         30 push(@list, $line);
192             }
193 13         26 $filter = join("\n", @list);
194             # find out how to split lines and expressions
195 13 100       31 if ($filter =~ /\s(and|or)\s/) {
196             # with syntactical sugar
197 2         14 $and_re = qr/\s+and\s+/;
198 2         6 $or_re = qr/\s+or\s+/;
199             } else {
200             # without syntactical sugar
201 11         30 $and_re = qr/ /;
202 11         35 $or_re = qr/\n/;
203             }
204             # parse line by line
205 13         43 foreach my $line (split($or_re, $filter)) {
206 16         224 $line =~ s/^\s+//;
207 16         31 $line =~ s/\s+$//;
208 16 50 33     51 next if $line eq "" or $line =~ /^\#/;
209 16         19 $level = "";
210 16         25 @list = ();
211 16         46 foreach my $expr (split($and_re, $line)) {
212 27         49 $expr = _parse_expr($line, \$level, $expr);
213             # each expression within a line is AND'ed
214 24         44 push(@list, $expr, "and");
215             }
216 13 100       21 if ($level) {
217             # one level specified
218 12         24 $il{$level}++;
219             } else {
220             # no level specified => all are potentially interesting
221 1         3 foreach my $kl (keys(%_KnownLevel)) {
222 5         7 $il{$kl}++;
223             }
224             }
225             # remove the last "and"
226 13         16 pop(@list);
227             # each line within a filter is OR'ed
228 13         24 push(@filter, @list, "or");
229             }
230 10 50       19 if (@filter) {
231             # non-empty filter => remove the last "or"
232 10         12 pop(@filter);
233             } else {
234             # empty filter => default behavior
235 0         0 %il = (_LEVEL_INFO() => 1);
236 0         0 @filter = ("1");
237             }
238 10         19 $ii = _compile_filter(0, @filter);
239 10         19 $mii = _compile_filter(1, @filter);
240             # so far, so good...
241 10         37 %_InterestingLevel = %il;
242 10         15 $_InterestingInfo = $ii;
243 10         50 $_MaybeInterestingInfo = $mii;
244             }
245              
246             #+++############################################################################
247             # #
248             # outputting #
249             # #
250             #---############################################################################
251              
252             #
253             # default handler: print compact yet user friendly output to STDOUT or STDERR
254             #
255              
256             sub log2std ($) {
257 0     0 1 0 my($info) = @_;
258 0         0 my($id, $string, $fh);
259              
260 0 0       0 $id = $INC{"threads.pm"} ? "$info->{pid}.$info->{tid}": $info->{pid};
261             $string = sprintf("%s %s %s[%s]: %s\n",
262             $_Level2Char{$info->{level}}, date_stamp($info->{time}),
263 0         0 $info->{program}, $id, $info->{message});
264 0 0       0 $fh = $info->{level} eq _LEVEL_INFO ? *STDOUT : *STDERR;
265 0         0 $fh->print($string);
266 0         0 $fh->flush();
267 0         0 return(1);
268             }
269              
270             #
271             # dump handler: print all attributes to STDERR
272             #
273              
274             sub log2dump ($) {
275 0     0 1 0 my($info) = @_;
276 0         0 my(@list);
277              
278 0         0 foreach my $attr (sort(keys(%{ $info }))) {
  0         0  
279 0 0       0 if ($info->{$attr} =~ /^[\w\.\-\/]*$/) {
280 0         0 push(@list, "$attr=$info->{$attr}");
281             } else {
282 0         0 push(@list, "$attr=\'$info->{$attr}\'");
283             }
284             }
285 0         0 STDERR->print("% @list\n");
286 0         0 STDERR->flush();
287 0         0 return(1);
288             }
289              
290             #+++############################################################################
291             # #
292             # formatting #
293             # #
294             #---############################################################################
295              
296             #
297             # format the message
298             #
299              
300             sub _message ($$) {
301 18     18   23 my($message, $info) = @_;
302 18         19 my(@list, $format, $pos);
303              
304 18         20 @list = @{ $message };
  18         27  
305 18 100       58 unless (@list) {
306             # no message given => empty string
307 1         2 return("");
308             }
309 17         24 $format = shift(@list);
310 17 100       35 if (ref($format) eq "CODE") {
311             # code message => result of the call
312 3         6 return($format->(@list));
313             }
314 14 100       23 if (ref($format)) {
315             # unexpected first argument
316 1         3 dief("unexpected argument: %s", $format);
317             }
318 13 100       20 unless (@list) {
319             # plain message
320 6         16 return($format);
321             }
322             # sprintf message => format it
323 7         9 $pos = 0;
324 7         13 foreach my $arg (@list) {
325 7 100       19 if (ref($arg) eq "SCALAR") {
    100          
326             # attribute argument
327 1         4 dief("unknown attribute: %s", ${ $arg })
328 2 100       4 unless defined($info->{${ $arg }});
  2         5  
329 1         3 $arg = $info->{${ $arg }};
  1         2  
330             } elsif (not ref($arg)) {
331             # plain argument
332 3 100       8 dief("undefined argument at position %d", $pos)
333             unless defined($arg);
334             } else {
335 2         5 dief("unexpected argument: %s", $arg);
336             }
337 3         6 $pos++;
338             }
339 3         10 return(sprintf($format, @list));
340             }
341              
342             #+++############################################################################
343             # #
344             # handling #
345             # #
346             #---############################################################################
347              
348             #
349             # handle information
350             #
351              
352             sub _handle ($$) {
353 23     23   33 my($message, $info) = @_;
354 23         27 my(@list);
355              
356             # build the info to log with minimal (= cheap to get) information
357 23         37 $info->{time} = time();
358 23         31 $info->{program} = $ProgramName;
359 23         37 $info->{host} = $HostName;
360 23         44 $info->{pid} = $$;
361 23 50       45 $info->{tid} = threads->tid() if $INC{"threads.pm"};
362 23         123 @list = caller(1);
363 23         47 $info->{file} = $list[1];
364 23         33 $info->{line} = $list[2];
365 23         55 @list = caller(2);
366 23 100       59 $info->{caller} = defined($list[3]) ? $list[3] : "main";
367             # check if we may care about this info
368 23 100       463 return(0) unless $_MaybeInterestingInfo->($info);
369             # format the message
370 18         35 $info->{message} = _message($message, $info);
371             # we always strip trailing spaces
372 13         41 $info->{message} =~ s/\s+$//;
373             # check if we really care about this info
374 13 50       196 return(0) unless $_InterestingInfo->($info);
375             # now send it to the right final handler
376 13         29 return($Handler->($info));
377             }
378              
379             #+++############################################################################
380             # #
381             # public API #
382             # #
383             #---############################################################################
384              
385             #
386             # check whether a level is "active"
387             #
388              
389 0     0 1 0 sub log_wants_error () { return($_InterestingLevel{_LEVEL_ERROR()}) }
390 0     0 1 0 sub log_wants_warning () { return($_InterestingLevel{_LEVEL_WARNING()}) }
391 4     4 1 105 sub log_wants_info () { return($_InterestingLevel{_LEVEL_INFO()}) }
392 4     4 1 15 sub log_wants_debug () { return($_InterestingLevel{_LEVEL_DEBUG()}) }
393 4     4 1 12 sub log_wants_trace () { return($_InterestingLevel{_LEVEL_TRACE()}) }
394              
395             #
396             # log error information
397             #
398              
399             sub log_error (@) {
400 0     0 1 0 my(@args) = @_;
401 0         0 my($attrs);
402              
403 0 0       0 return(0) unless $_InterestingLevel{_LEVEL_ERROR()};
404 0 0 0     0 if (@args and ref($args[-1]) eq "HASH") {
405 0         0 $attrs = pop(@args);
406             } else {
407 0         0 $attrs = {};
408             }
409 0         0 return(_handle(\@args, { %{ $attrs }, level => _LEVEL_ERROR }));
  0         0  
410             }
411              
412             #
413             # log warning information
414             #
415              
416             sub log_warning (@) {
417 0     0 1 0 my(@args) = @_;
418 0         0 my($attrs);
419              
420 0 0       0 return(0) unless $_InterestingLevel{_LEVEL_WARNING()};
421 0 0 0     0 if (@args and ref($args[-1]) eq "HASH") {
422 0         0 $attrs = pop(@args);
423             } else {
424 0         0 $attrs = {};
425             }
426 0         0 return(_handle(\@args, { %{ $attrs }, level => _LEVEL_WARNING }));
  0         0  
427             }
428              
429             #
430             # log informational information ;-)
431             #
432              
433             sub log_info (@) {
434 14     14 1 4696 my(@args) = @_;
435 14         19 my($attrs);
436              
437 14 100       35 return(0) unless $_InterestingLevel{_LEVEL_INFO()};
438 13 100 100     49 if (@args and ref($args[-1]) eq "HASH") {
439 1         3 $attrs = pop(@args);
440             } else {
441 12         20 $attrs = {};
442             }
443 13         20 return(_handle(\@args, { %{ $attrs }, level => _LEVEL_INFO }));
  13         40  
444             }
445              
446             #
447             # log debugging information
448             #
449              
450             sub log_debug (@) {
451 10     10 1 39 my(@args) = @_;
452 10         11 my($attrs);
453              
454 10 50       21 return(0) unless $_InterestingLevel{_LEVEL_DEBUG()};
455 10 100 66     41 if (@args and ref($args[-1]) eq "HASH") {
456 2         4 $attrs = pop(@args);
457             } else {
458 8         15 $attrs = {};
459             }
460 10         14 return(_handle(\@args, { %{ $attrs }, level => _LEVEL_DEBUG }));
  10         31  
461             }
462              
463             #
464             # log tracing information (fixed message)
465             #
466              
467             sub log_trace () {
468 0 0   0 1 0 return(0) unless $_InterestingLevel{_LEVEL_TRACE()};
469 0         0 return(_handle(
470             [ "in %s at %s line %s", \ "caller", \ "file", \ "line" ],
471             { level => _LEVEL_TRACE },
472             ));
473             }
474              
475             #+++############################################################################
476             # #
477             # module initialization #
478             # #
479             #---############################################################################
480              
481             # we select the relevant handler to use
482             if ($ENV{NO_WORRIES} and $ENV{NO_WORRIES} =~ /\b(log2dump)\b/) {
483             $Handler = \&log2dump;
484             } else {
485             $Handler = \&log2std;
486             };
487              
488             # here are all the known levels
489             %_Level2Char = (
490             error => "!",
491             warning => "?",
492             info => ":",
493             debug => "#",
494             trace => "=",
495             );
496             foreach my $level (keys(%_Level2Char)) {
497             $_KnownLevel{$level}++;
498             }
499              
500             # by default we only care about informational level or higher
501             %_InterestingLevel = %_KnownLevel;
502             delete($_InterestingLevel{_LEVEL_DEBUG()});
503             delete($_InterestingLevel{_LEVEL_TRACE()});
504              
505             # by default we do not filter anything out
506             $_MaybeInterestingInfo = $_InterestingInfo = sub { return(1) };
507              
508             #
509             # export control
510             #
511              
512             sub import : method {
513 1     1   9 my($pkg, %exported);
514              
515 1         3 $pkg = shift(@_);
516 1         11 grep($exported{$_}++,
517             map("log_$_", qw(configure filter)),
518             map("log_$_", qw(error warning info debug trace)),
519             map("log_wants_$_", qw(error warning info debug trace)),
520             );
521 1     0   4 $exported{"log2std"} = sub { $Handler = \&log2std };
  0         0  
522 1     0   3 $exported{"log2dump"} = sub { $Handler = \&log2dump };
  0         0  
523 1         4 export_control(scalar(caller()), $pkg, \%exported, @_);
524             }
525              
526             1;
527              
528             __DATA__