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