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   70547 use strict;
  2         12  
  2         62  
15 2     2   10 use warnings;
  2         4  
  2         140  
16             our $VERSION = "1.7";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.17 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 2     2   1110 use IO::Handle qw();
  2         13259  
  2         60  
24 2     2   419 use No::Worries qw($HostName $ProgramName);
  2         5  
  2         12  
25 2     2   861 use No::Worries::Date qw(date_stamp);
  2         5  
  2         14  
26 2     2   13 use No::Worries::Export qw(export_control);
  2         4  
  2         32  
27 2     2   952 use No::Worries::File qw(file_read);
  2         6  
  2         15  
28 2     2   15 use No::Worries::Die qw(dief);
  2         4  
  2         12  
29              
30             #
31             # constants
32             #
33              
34 2     2   13 use constant _LEVEL_ERROR => "error";
  2         4  
  2         116  
35 2     2   13 use constant _LEVEL_WARNING => "warning";
  2         13  
  2         152  
36 2     2   14 use constant _LEVEL_INFO => "info";
  2         4  
  2         96  
37 2     2   11 use constant _LEVEL_DEBUG => "debug";
  2         4  
  2         104  
38 2     2   11 use constant _LEVEL_TRACE => "trace";
  2         4  
  2         6993  
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   88 my($partial, $attr, $op, $value) = @_;
89              
90             # for partial filtering, we do not care about the message
91 46 100 100     100 return("1") if $attr eq "message" and $partial;
92             # for the attributes we know about, it is easy
93 44 100       256 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         101 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   63 my($partial, @filter) = @_;
105 20         33 my(@code, $code, $sub);
106              
107 20         48 @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         36 foreach my $expr (@filter) {
116 72 100       138 if (ref($expr) eq "ARRAY") {
117 46         61 push(@code, " " . _expr_code($partial, @{ $expr }));
  46         93  
118             } else {
119 26         50 push(@code, " $expr");
120             }
121             }
122 20         46 $code[-1] .= ";";
123 20         66 push(@code,
124             " return(0);",
125             "}",
126             );
127 20         66 $code = join("\n", @code);
128 1     1   7 eval($code); ## no critic 'BuiltinFunctions::ProhibitStringyEval'
  1     1   2  
  1     1   38  
  1     1   7  
  1     1   2  
  1     1   168  
  1     1   8  
  1     1   1  
  1     1   35  
  1     1   6  
  1     1   2  
  1     1   129  
  1     1   7  
  1     1   2  
  1     1   36  
  1     1   6  
  1     1   2  
  1     1   131  
  1     1   8  
  1     1   2  
  1     1   36  
  1     1   6  
  1     1   2  
  1     1   101  
  1     1   7  
  1     1   2  
  1     1   23  
  1     1   4  
  1     1   2  
  1     1   89  
  1     1   7  
  1     1   2  
  1     1   21  
  1     1   4  
  1     1   2  
  1     1   80  
  1     1   7  
  1     1   2  
  1     1   23  
  1     1   5  
  1         2  
  1         91  
  1         7  
  1         2  
  1         34  
  1         7  
  1         2  
  1         104  
  1         7  
  1         2  
  1         37  
  1         7  
  1         2  
  1         100  
  1         7  
  1         2  
  1         23  
  1         5  
  1         2  
  1         105  
  1         8  
  1         2  
  1         36  
  1         7  
  1         2  
  1         109  
  1         7  
  1         3  
  1         37  
  1         7  
  1         2  
  1         104  
  1         7  
  1         2  
  1         21  
  1         5  
  1         2  
  1         130  
  1         7  
  1         1  
  1         22  
  1         5  
  1         2  
  1         96  
  1         7  
  1         3  
  1         25  
  1         5  
  1         2  
  1         89  
  1         7  
  1         2  
  1         21  
  1         5  
  1         2  
  1         77  
  1         7  
  1         2  
  1         25  
  1         5  
  1         2  
  1         81  
  1         7  
  1         1  
  1         22  
  1         5  
  1         2  
  1         77  
  1         7  
  1         2  
  1         21  
  1         5  
  1         2  
  1         111  
  1         7  
  1         2  
  1         21  
  1         5  
  1         3  
  1         156  
  20         1479  
129 20 50       72 dief("invalid code built: %s", $@) if $@;
130 20         71 return($sub);
131             }
132              
133             #
134             # parse a single filtering expression
135             #
136              
137             sub _parse_expr ($$$) {
138 27     27   53 my($line, $level, $expr) = @_;
139 27         40 my($attr, $op, $value);
140              
141             # we first parse the (attr, op, value) triplet
142 27 100 66     155 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       15 dief("invalid filter line: %s", $line) if ${ $level };
  14         35  
145 13         23 ${ $level } = $expr;
  13         20  
146 13         29 ($attr, $op, $value) = ("level", "==", $expr);
147             } elsif ($expr =~ /^(\w+)(==|!=)$/ and $1 ne "level") {
148             # special case for comparison with empty string
149 1         3 ($attr, $op, $value) = ($1, $2, "");
150             } elsif ($expr =~ /^(\w+)(==|!=|=~|!~|>=?|<=?)(\S+)$/ and $1 ne "level") {
151             # normal case
152 10         34 ($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     93 if ($op eq "=~" or $op eq "!~") {
    100 100        
158             # match: check that the value is a valid regular expression
159 4         8 eval { $expr =~ /$value/ };
  4         39  
160 4 50       12 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       56 unless ($value =~ /^-?\d+$/) {
165 15 50       30 $op = $op eq "==" ? "eq" : "ne";
166 15         33 $value = "qq\0$value\0";
167             }
168             } else {
169             # numerical: check that the value is a valid integer
170 3 50       23 dief("invalid integer: %s", $value) unless $value =~ /^-?\d+$/;
171             }
172             # so far, so good
173 24         78 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 6493 my($filter) = @_;
182 13         27 my($and_re, $or_re, $level, @list, @filter, %il, $ii, $mii);
183              
184             # strip comments and empty lines and extra spaces
185 13         24 @list = ();
186 13         42 foreach my $line (split(/\n/, $filter)) {
187 19         57 $line =~ s/^\s+//;
188 19         51 $line =~ s/\s+$//;
189 19         57 $line =~ s/\s+/ /g;
190 19 100 100     90 next if $line eq "" or $line =~ /^\#/;
191 15         36 push(@list, $line);
192             }
193 13         35 $filter = join("\n", @list);
194             # find out how to split lines and expressions
195 13 100       40 if ($filter =~ /\s(and|or)\s/) {
196             # with syntactical sugar
197 2         7 $and_re = qr/\s+and\s+/;
198 2         7 $or_re = qr/\s+or\s+/;
199             } else {
200             # without syntactical sugar
201 11         35 $and_re = qr/ /;
202 11         26 $or_re = qr/\n/;
203             }
204             # parse line by line
205 13         55 foreach my $line (split($or_re, $filter)) {
206 16         35 $line =~ s/^\s+//;
207 16         32 $line =~ s/\s+$//;
208 16 50 33     55 next if $line eq "" or $line =~ /^\#/;
209 16         26 $level = "";
210 16         32 @list = ();
211 16         92 foreach my $expr (split($and_re, $line)) {
212 27         59 $expr = _parse_expr($line, \$level, $expr);
213             # each expression within a line is AND'ed
214 24         51 push(@list, $expr, "and");
215             }
216 13 100       29 if ($level) {
217             # one level specified
218 12         27 $il{$level}++;
219             } else {
220             # no level specified => all are potentially interesting
221 1         4 foreach my $kl (keys(%_KnownLevel)) {
222 5         9 $il{$kl}++;
223             }
224             }
225             # remove the last "and"
226 13         21 pop(@list);
227             # each line within a filter is OR'ed
228 13         28 push(@filter, @list, "or");
229             }
230 10 50       21 if (@filter) {
231             # non-empty filter => remove the last "or"
232 10         16 pop(@filter);
233             } else {
234             # empty filter => default behavior
235 0         0 %il = (_LEVEL_INFO() => 1);
236 0         0 @filter = ("1");
237             }
238 10         24 $ii = _compile_filter(0, @filter);
239 10         25 $mii = _compile_filter(1, @filter);
240             # so far, so good...
241 10         43 %_InterestingLevel = %il;
242 10         19 $_InterestingInfo = $ii;
243 10         56 $_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   30 my($message, $info) = @_;
302 18         24 my(@list, $format, $pos);
303              
304 18         22 @list = @{ $message };
  18         35  
305 18 100       37 unless (@list) {
306             # no message given => empty string
307 1         3 return("");
308             }
309 17         32 $format = shift(@list);
310 17 100       36 if (ref($format) eq "CODE") {
311             # code message => result of the call
312 3         9 return($format->(@list));
313             }
314 14 100       26 if (ref($format)) {
315             # unexpected first argument
316 1         4 dief("unexpected argument: %s", $format);
317             }
318 13 100       23 unless (@list) {
319             # plain message
320 6         17 return($format);
321             }
322             # sprintf message => format it
323 7         11 $pos = 0;
324 7         13 foreach my $arg (@list) {
325 7 100       20 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         8  
329 1         2 $arg = $info->{${ $arg }};
  1         3  
330             } elsif (not ref($arg)) {
331             # plain argument
332 3 100       10 dief("undefined argument at position %d", $pos)
333             unless defined($arg);
334             } else {
335 2         7 dief("unexpected argument: %s", $arg);
336             }
337 3         5 $pos++;
338             }
339 3         13 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   43 my($message, $info) = @_;
354 23         32 my(@list);
355              
356             # build the info to log with minimal (= cheap to get) information
357 23         43 $info->{time} = time();
358 23         43 $info->{program} = $ProgramName;
359 23         37 $info->{host} = $HostName;
360 23         60 $info->{pid} = $$;
361 23 50       48 $info->{tid} = threads->tid() if $INC{"threads.pm"};
362 23         163 @list = caller(1);
363 23         53 $info->{file} = $list[1];
364 23         47 $info->{line} = $list[2];
365 23         82 @list = caller(2);
366 23 100       80 $info->{caller} = defined($list[3]) ? $list[3] : "main";
367             # check if we may care about this info
368 23 100       571 return(0) unless $_MaybeInterestingInfo->($info);
369             # format the message
370 18         41 $info->{message} = _message($message, $info);
371             # we always strip trailing spaces
372 13         47 $info->{message} =~ s/\s+$//;
373             # check if we really care about this info
374 13 50       235 return(0) unless $_InterestingInfo->($info);
375             # now send it to the right final handler
376 13         46 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 121 sub log_wants_info () { return($_InterestingLevel{_LEVEL_INFO()}) }
392 4     4 1 17 sub log_wants_debug () { return($_InterestingLevel{_LEVEL_DEBUG()}) }
393 4     4 1 16 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 5673 my(@args) = @_;
435 14         23 my($attrs);
436              
437 14 100       40 return(0) unless $_InterestingLevel{_LEVEL_INFO()};
438 13 100 100     62 if (@args and ref($args[-1]) eq "HASH") {
439 1         3 $attrs = pop(@args);
440             } else {
441 12         21 $attrs = {};
442             }
443 13         23 return(_handle(\@args, { %{ $attrs }, level => _LEVEL_INFO }));
  13         48  
444             }
445              
446             #
447             # log debugging information
448             #
449              
450             sub log_debug (@) {
451 10     10 1 48 my(@args) = @_;
452 10         15 my($attrs);
453              
454 10 50       25 return(0) unless $_InterestingLevel{_LEVEL_DEBUG()};
455 10 100 66     52 if (@args and ref($args[-1]) eq "HASH") {
456 2         5 $attrs = pop(@args);
457             } else {
458 8         15 $attrs = {};
459             }
460 10         18 return(_handle(\@args, { %{ $attrs }, level => _LEVEL_DEBUG }));
  10         78  
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         14 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         6 export_control(scalar(caller()), $pkg, \%exported, @_);
524             }
525              
526             1;
527              
528             __DATA__