File Coverage

blib/lib/Log/Declare.pm
Criterion Covered Total %
statement 147 161 91.3
branch 49 66 74.2
condition 36 45 80.0
subroutine 16 18 88.8
pod 0 6 0.0
total 248 296 83.7


line stmt bran cond sub pod time code
1             package Log::Declare;
2             # ABSTRACT: A high performance Perl logging module.
3              
4 2     2   59474 use 5.10.0; # for //
  2         9  
  2         107  
5 2     2   12 use strict;
  2         4  
  2         98  
6 2     2   12 use warnings;
  2         7  
  2         75  
7              
8 2     2   2226 use Devel::Declare::Lexer;
  2         117840  
  2         17  
9 2     2   2238 use Devel::Declare::Lexer::Token::Raw;
  2         493  
  2         58  
10 2     2   1859 use POSIX qw(strftime);
  2         17382  
  2         16  
11 2     2   2646 use Data::Dumper; # for d: statements
  2         4  
  2         3063  
12              
13             our $VERSION = '0.10';
14              
15             my %LEVEL = (
16             ALL => -1,
17             TRACE => 1,
18             DEBUG => 2,
19             INFO => 3,
20             WARN => 4,
21             ERROR => 5,
22             AUDIT => 6,
23             OFF => 7,
24             DISABLE => 7,
25             );
26              
27             # XXX be careful about removing/renaming this: it's required by MojoX::Log::Declare
28             our @level_priority = qw(audit error warn info debug trace);
29              
30             my ($LEVEL, $LEVEL_NAME);
31             __PACKAGE__->startup_level($ENV{'LOG_DECLARE_STARTUP_LEVEL'} || 'ERROR'); # sets $LEVEL and $LEVEL_NAME
32              
33             my $log_statement = "Log::Declare->log('%s', [%s], %s)%s";
34              
35             unless($ENV{LOG_DECLARE_NO_STARTUP_NOTICE}) {
36             Log::Declare->log('INFO', ['LOGGER'], "Got logger startup level of $LEVEL_NAME");
37             }
38              
39             # this provides a way to globally override the behaviour of the injected keywords.
40             # if replaced by e.g. a sub which returns 0, the level will be completely disabled and
41             # the log writer won't be called. The original implementations can be restored at any
42             # time by deleting the hooks.
43             # XXX be careful about removing/renaming this: it's required for namespace hooks (see
44             # the NAMESPACES section in the POD).
45             our %levels;
46              
47             # define the exported trace, debug &c. subs. These delegate to the hooked implementations
48             # in %levels (if defined); otherwise they return true/false if the level is enabled/disabled
49             my %EXPORT;
50             for my $name (@level_priority) {
51             my $hook;
52             my $level = $LEVEL{uc $name};
53             # goto &sub: make sure caller() works as expected in the hooked sub
54 100 100   100   14401 $EXPORT{$name} = sub { ($hook = $levels{$name}) ? goto &$hook : $level >= $LEVEL };
55             }
56              
57             BEGIN {
58             my $callback = sub {
59 30         28472 my ($stream_r) = @_;
60 30         101 my @stream = @$stream_r;
61              
62             # Get the declarator
63 30         999 my $decl = $stream[0];
64              
65 30         42 shift @stream; # remove the declarator
66 30         117 while (ref($stream[0]) =~ /Devel::Declare::Lexer::Token::Whitespace/) {
67 30         76 shift @stream; # remove the whitespace
68             }
69              
70 30 50       85 if(ref($stream[$#stream]) =~ /Devel::Declare::Lexer::Token::Newline/) {
71 0         0 pop @stream; # remove the newline
72             }
73 30         36 pop @stream; # remove the semicolon
74              
75             # Work backwards from the end looking for if statement
76 30         38 my $nested = 0;
77 30         34 my $ifStart = -1;
78 30         76 for(my $i = $#stream; $i >= 0; $i--) {
79 238         261 my $token = $stream[$i];
80              
81 238 100 100     539 if(ref($token) =~ /Devel::Declare::Lexer::Token::RightBracket/ &&
82             $token->{value} =~ /\]/) {
83 12         14 $nested++;
84 12         29 next;
85             }
86 226 100 100     602 if(ref($token) =~ /Devel::Declare::Lexer::Token::LeftBracket/ &&
87             $token->{value} =~ /\[/) {
88 12         14 $nested--;
89 12         25 next;
90             }
91 214 100 100     1221 if($nested == 0 && ref($token) =~ /Devel::Declare::Lexer::Token::Bareword/ &&
      100        
      66        
92             ($token->{value} eq 'if' || $token->{value} eq 'unless')) {
93 4         5 $ifStart = $i;
94 4         8 last;
95             }
96             }
97              
98             # Extract the conditional tokens
99 30         28 my @condTokens;
100 30 100       50 if($ifStart > -1) {
101 4         5 my $soc = $ifStart;
102 4         7 my $eoc = $#stream;
103 4         14 @condTokens = @stream[$soc .. $eoc];
104 4         15 @stream = @stream[0 .. $ifStart - 1];
105             }
106              
107             # Work backwards from the end looking for categories
108 30         34 $nested = 0;
109 30         34 my $catStart = -1;
110 30         73 for(my $i = $#stream; $i >= 0; $i--) {
111 148         159 my $token = $stream[$i];
112              
113 148 100 100     341 if(ref($token) =~ /Devel::Declare::Lexer::Token::RightBracket/ &&
114             $token->{value} =~ /\]/) {
115 11         10 $nested++;
116 11         23 next;
117             }
118 137 100 100     440 if(ref($token) =~ /Devel::Declare::Lexer::Token::LeftBracket/ &&
119             $token->{value} =~ /\[/) {
120 11         12 $nested--;
121 11 50       16 if($nested == 0) {
122 11 100 66     68 if($stream[$i-1] && ref($stream[$i-1]) !~ /Devel::Declare::Lexer::Token::Whitespace/) {
123 1         3 next;
124             }
125 10         11 $catStart = $i;
126 10         17 last;
127             }
128 0         0 next;
129             }
130             }
131              
132             # Extract the category tokens
133 30         31 my @catTokens;
134 30 100       53 if($catStart > -1) {
135 10         13 my $soc = $catStart + 1;
136 10         15 my $eoc = $#stream - 1;
137 10         32 @catTokens = @stream[$soc .. $eoc];
138 10         56 @stream = @stream[0 .. $catStart - 1];
139             }
140              
141             # Convert the tokens into a list of category names
142 30         32 my @categories;
143 30 100       52 if(scalar @catTokens) {
144 10         16 my $buf = '';
145 10         19 for my $token (@catTokens) {
146 46 50       101 if(ref($token) =~ /Devel::Declare::Lexer::Token::Comma/) {
147 0 0       0 push @categories, (uc "\"$buf\"") if $buf;
148 0         0 $buf = '';
149 0         0 next;
150             }
151 46 50 66     99 next if $buf eq '' && ref($token) =~ /Devel::Declare::Lexer::Token::Whitespace/;
152 46         81 $buf .= $token->{value};
153             }
154 10 50       40 push @categories, uc("\"$buf\"") if $buf;
155             }
156 30 100       75 push @categories, "\"GENERAL\"" if scalar @categories == 0;
157              
158             # Create a new stream from whats left
159 30         66 my @ns = ();
160 30         136 tie @ns, "Devel::Declare::Lexer::Stream";
161              
162             # See how many arguments we have
163 30         156 my $nest = 0;
164 30         34 my $bits = 0;
165              
166 30         46 for my $tok (@stream) {
167 164 100       308 if(ref($tok) =~ /Devel::Declare::Lexer::Token::LeftBracket/) {
168 5         6 $nest++;
169 5         8 next;
170             }
171 159 100       263 if(ref($tok) =~ /Devel::Declare::Lexer::Token::RightBracket/) {
172 5         7 $nest++;
173 5         6 next;
174             }
175 154 100 100     756 if($nest == 0 && ref($tok) =~ /Devel::Declare::Lexer::Token::Operator/ &&
      100        
176             $tok->{value} =~ /,/) {
177 21         29 $bits++;
178             }
179             }
180              
181             # Reconstruct the log statement
182 30         63 my $level = $decl->{value};
183 30         54 my $cats = join ', ', @categories;
184 30         52 my $inner = join '', map { $_->get } @stream;
  164         821  
185              
186             # Handle prefixes
187 30         241 $inner =~ s/([\s,])d:([\\\$\@\%\&\*]+[^\s,]+)/$1Data::Dumper::Dumper($2)/g;
188 30         49 $inner =~ s/([\s,])r:([\\\$\@\%\&\*]+[^\s,]+)/$1ref($2)/g;
189              
190 30         35 my $msg = '';
191 30 100       58 if ($bits) {
192 14 50       27 $msg = 'sprintf(' if $bits;
193 14         17 $msg .= $inner;
194 14 50       27 $msg .= ')' if $bits;
195             } else {
196 16         22 $msg = $inner;
197             }
198 30         53 my $cond = ' ' . join '', map { $_->get } @condTokens;
  16         96  
199              
200 30         240 my $output = Devel::Declare::Lexer::Token::Raw->new(
201             value => sprintf($log_statement, $level, $cats, $msg, $cond)
202             );
203              
204             return [
205 30         529 $decl,
206             Devel::Declare::Lexer::Token::Whitespace->new(value => ' '), $output,
207             Devel::Declare::Lexer::Token::EndOfStatement->new,
208             Devel::Declare::Lexer::Token::Newline->new
209             ];
210 2     2   16 };
211              
212             # Setup callbacks for each of the keywords
213 2         12 Devel::Declare::Lexer::lexed(audit => $callback);
214 2         28 Devel::Declare::Lexer::lexed(info => $callback);
215 2         13 Devel::Declare::Lexer::lexed(warn => $callback);
216 2         12 Devel::Declare::Lexer::lexed(error => $callback);
217 2         11 Devel::Declare::Lexer::lexed(debug => $callback);
218 2         25 Devel::Declare::Lexer::lexed(trace => $callback);
219             }
220              
221             # -----------------------------------------------------------------------------
222              
223             # set the global log level
224             # FIXME this should be called level
225             sub startup_level {
226 18     18 0 40371 my $self = shift;
227              
228 18 50       95 if (@_) {
229 18   50     51 my $level = shift // '';
230 18         47 $LEVEL_NAME = uc $level;
231             # ALL: be forgiving if the name is invalid/mistyped (see below)
232 18   66     91 $LEVEL = $LEVEL{$LEVEL_NAME} // $LEVEL{ALL};
233             } else {
234 0         0 return $LEVEL_NAME;
235             }
236             }
237              
238             # -----------------------------------------------------------------------------
239              
240             sub log_statement {
241 0     0 0 0 my ($self, $statement) = @_;
242              
243 0 0       0 return $log_statement unless $statement;
244 0         0 $log_statement = $statement;
245 0         0 return $log_statement;
246             }
247              
248             # -----------------------------------------------------------------------------
249              
250             sub log {
251 30     30 0 307 my ($self, $level_name, $categories, $message) = @_;
252              
253 30   50     83 $level_name = uc($level_name // '');
254              
255             # be forgiving if the log level is mistyped/invalid: it's going
256             # to be easier to remove an unwanted log message than to track
257             # down a bug that isn't being logged because of a typo
258 30   33     76 my $level = $LEVEL{$level_name} // $LEVEL;
259              
260 30 100       66 return unless $level >= $LEVEL;
261              
262 28 50       56 if($categories) {
263 28 50       77 $categories = scalar @$categories > 0 ? (join ', ', @$categories) : '';
264 28         57 $categories = " [$categories]";
265             }
266              
267 28 50 50     1570 my $ts = strftime $ENV{'LOG_DECLARE_DATE_FORMAT'} // "%a %b %e %H:%M:%S %Y",
268             ($ENV{'LOG_DECLARE_USE_LOCALTIME'} ? localtime : gmtime);
269              
270 28 100       108 $message .= "\n" if substr($message,-1) ne "\n";
271              
272 28         179 return CORE::print STDERR "$$ [$ts] [$level_name]$categories $message";
273             }
274              
275             # -----------------------------------------------------------------------------
276              
277             sub capture {
278 2     2 0 454 my ($self, $capture, $coderef) = @_;
279              
280             {
281 2     2   716 no strict 'refs';
  2         3  
  2         646  
  2         4  
282 2         37 *{$capture} = sub {
283 2     2   9 my $logger = shift;
284 2 100       9 @_ = $coderef->(@_) if $coderef;
285 2         13 $self->log('debug', [ref($logger)], @_);
286 2         13 };
287             }
288             }
289              
290             # -----------------------------------------------------------------------------
291              
292             sub import {
293 2     2   11 my ($class, @tags) = @_;
294              
295 2         4 my $caller = caller;
296 2         7 Log::Declare->do_import($caller, @tags);
297             }
298              
299             # -----------------------------------------------------------------------------
300              
301             sub export_to_level {
302 0     0 0 0 my ($class, $level, @tags) = @_;
303              
304 0         0 my $caller = caller($level);
305 0         0 Log::Declare->do_import($caller, @tags);
306             }
307              
308             # -----------------------------------------------------------------------------
309              
310             sub do_import {
311 2     2 0 4 my ($class, $caller, @tags) = @_;
312              
313 2         5 my %t = map { $_ => 1 } @tags;
  0         0  
314 2 50       6 return if $t{':nosyntax'};
315              
316             # Inject each of the keywords into the caller's namespace
317 2         4 for my $name (@level_priority) {
318 12 50       617 Devel::Declare::Lexer::import_for($caller, {
319             $name => $EXPORT{$name}
320             }) if !$t{":no$name"};
321             }
322             }
323              
324             # -----------------------------------------------------------------------------
325              
326             =pod
327              
328             =head1 NAME
329              
330             Log::Declare - A high performance Perl logging module
331              
332             =head1 OVERVIEW
333              
334             Creates syntactic sugar for logging using categories with sprintf support.
335              
336             Complex logging statements can be written without impacting on performance
337             when those log levels are disabled.
338              
339             For example, using a typical logger, this would incur a penalty even if
340             the logging is disabled:
341              
342             $self->log(Dumper $myobject);
343              
344             but with Log::Declare we incur almost no performance penalty if 'info' level is
345             disabled, since the following log statement:
346              
347             info Dumper $myobject [mycategory];
348              
349             gets rewritten as:
350              
351             info && $Log::Declare::logger->log('info', ['mycategory'], Dumper $myobject);
352              
353             which means if 'info' returns 0, nothing else gets evaluated.
354              
355             =head1 SYNOPSIS
356              
357             use Log::Declare;
358             use Log::Declare qw/ :nosyntax /; # disables syntactic sugar
359             use Log::Declare qw/ :nowarn :noerror ... /; # disables specific sugar
360              
361             # with syntactic sugar
362             debug "My debug message" [category];
363             error "My error message: %s", $error [category1, category2];
364              
365             # auto-dump variables with Data::Dumper
366             debug "Using sprintf format: %s", d:$error [category];
367              
368             # auto-ref variables with ref()
369             debug "Using sprintf format: %s", r:$error [category];
370              
371             # capture other loggers (loses Log::Declare performance)
372             Log::Declare->capture('Test::Logger::log');
373             Log::Declare->capture('Test::Logger::log' => sub {
374             my ($logger, @args) = @_;
375             # manipulate logger args here
376             return @args;
377             });
378              
379             =head1 NAMESPACES
380              
381             If you're using a namespace-aware logger, Log::Declare can use your logger's
382             namespacing to determine log levels. For example:
383              
384             $Log::Declare::levels{'debug'} = sub {
385             Log::Log4perl->get_logger(caller)->is_debug;
386             };
387              
388             =cut
389              
390             1;
391