File Coverage

blib/lib/Log/Log4perl/Layout/ColoredPatternLayout.pm
Criterion Covered Total %
statement 119 139 85.6
branch 41 58 70.6
condition 24 37 64.8
subroutine 9 10 90.0
pod 0 2 0.0
total 193 246 78.4


line stmt bran cond sub pod time code
1             package Log::Log4perl::Layout::ColoredPatternLayout;
2              
3 3     3   413180 use strict;
  3         30  
  3         141  
4 3     3   23 use warnings;
  3         9  
  3         130  
5 3     3   37 use base 'Log::Log4perl::Layout::PatternLayout';
  3         10  
  3         458  
6 3     3   1444 use Term::ANSIColor 'colored';
  3         17421  
  3         2760  
7              
8             # Tom Gracey May 2018
9             # Most of what follows is taken from the original
10             # Log::Log4perl::Layout::PatternLayout
11             # See comments and manpage
12              
13             our $VERSION = '0.01';
14              
15 3     3   30 use constant _INTERNAL_DEBUG => 0;
  3         19  
  3         413  
16              
17             our $TIME_HIRES_AVAILABLE_WARNED = 0;
18             our $HOSTNAME;
19             our %GLOBAL_USER_DEFINED_CSPECS = ();
20              
21             our $CSPECS = 'cCdFHIlLmMnpPrRtTxX%';
22              
23 3     3   26 no strict qw(refs);
  3         6  
  3         5555  
24              
25             ##################################################
26             sub new {
27             ##################################################
28             # this overrides 'new' in Log::Log4perl::Layout::PatternLayout
29             # (sub was taken from Log::Log4perl version 1.49)
30             # - but with very sparse changes
31             # changed/added lines are marked
32             # Tom Gracey May 2018
33              
34 24     24 0 29410 my $class = shift;
35 24   33     143 $class = ref ($class) || $class;
36              
37 24 100       106 my $options = ref $_[0] eq "HASH" ? shift : {};
38 24 100       92 my $layout_string = @_ ? shift : '%m%n';
39            
40             my $self = {
41             format => undef,
42             info_needed => {},
43             stack => [],
44             CSPECS => $CSPECS,
45             dontCollapseArrayRefs => $options->{dontCollapseArrayRefs}{value},
46             last_time => undef,
47             undef_column_value =>
48             (exists $options->{ undef_column_value }
49             ? $options->{ undef_column_value }
50 24 50       224 : "[undef]"),
51             };
52              
53             $self->{timer} = Log::Log4perl::Util::TimeTracker->new(
54             time_function => $options->{time_function}
55 24         174 );
56              
57             # The following lines added TG May 2018
58 24 100       1191 if(exists $options->{ColorMap}->{value}){
59 2         9 $self->{color_map} = $options->{ColorMap}->{value};
60             }
61             # End of added lines TG
62              
63 24 100       104 if(exists $options->{ConversionPattern}->{value}) {
64 2         7 $layout_string = $options->{ConversionPattern}->{value};
65             }
66              
67 24 50       75 if(exists $options->{message_chomp_before_newline}) {
68             $self->{message_chomp_before_newline} =
69 0         0 $options->{message_chomp_before_newline}->{value};
70             } else {
71 24         68 $self->{message_chomp_before_newline} = 1;
72             }
73              
74 24         61 bless $self, $class;
75              
76             #add the global user-defined cspecs
77 24         106 foreach my $f (keys %GLOBAL_USER_DEFINED_CSPECS){
78             #add it to the list of letters
79 0         0 $self->{CSPECS} .= $f;
80             #for globals, the coderef is already evaled,
81 0         0 $self->{USER_DEFINED_CSPECS}{$f} = $GLOBAL_USER_DEFINED_CSPECS{$f};
82             }
83              
84             #add the user-defined cspecs local to this appender
85 24         57 foreach my $f (keys %{$options->{cspec}}){
  24         124  
86 0         0 $self->add_layout_cspec($f, $options->{cspec}{$f}{value});
87             }
88              
89             # non-portable line breaks
90 24         90 $layout_string =~ s/\\n/\n/g;
91 24         95 $layout_string =~ s/\\r/\r/g;
92              
93 24         174 $self->define($layout_string);
94              
95 24         3229 return $self;
96             }
97              
98              
99             ##################################################
100             sub render {
101             ##################################################
102             #
103             # Tom Gracey May 2018
104             # Same situaton as 'new' (see above)
105             # ie 'render' is overridden but only minor changes
106             # made, which are marked below
107             #
108 33     33 0 9984 my($self, $message, $category, $priority, $caller_level) = @_;
109              
110 33 50       119 $caller_level = 0 unless defined $caller_level;
111              
112 33         79 my %info = ();
113              
114 33         93 $info{m} = $message;
115             # See 'define'
116 33 100       158 chomp $info{m} if $self->{message_chompable};
117              
118 33         77 my @results = ();
119              
120 33         135 my $caller_offset = Log::Log4perl::caller_depth_offset( $caller_level );
121              
122 33 50 100     780 if($self->{info_needed}->{L} or
      66        
      33        
      66        
      66        
      50        
123             $self->{info_needed}->{F} or
124             $self->{info_needed}->{C} or
125             $self->{info_needed}->{l} or
126             $self->{info_needed}->{M} or
127             $self->{info_needed}->{T} or
128             0
129             ) {
130              
131 11         116 my ($package, $filename, $line,
132             $subroutine, $hasargs,
133             $wantarray, $evaltext, $is_require,
134             $hints, $bitmask) = caller($caller_offset);
135              
136             # If caller() choked because of a whacko caller level,
137             # correct undefined values to '[undef]' in order to prevent
138             # warning messages when interpolating later
139 11 50       72 unless(defined $bitmask) {
140 0         0 for($package,
141             $filename, $line,
142             $subroutine, $hasargs,
143             $wantarray, $evaltext, $is_require,
144             $hints, $bitmask) {
145 0 0       0 $_ = '[undef]' unless defined $_;
146             }
147             }
148              
149 11         35 $info{L} = $line;
150 11         30 $info{F} = $filename;
151 11         30 $info{C} = $package;
152              
153 11 50 66     81 if($self->{info_needed}->{M} or
      50        
154             $self->{info_needed}->{l} or
155             0) {
156             # To obtain the name of the subroutine which triggered the
157             # logger, we need to go one additional level up.
158 5         24 my $levels_up = 1;
159             {
160 5         11 my @callinfo = caller($caller_offset+$levels_up);
  9         55  
161              
162 9         22 if(_INTERNAL_DEBUG) {
163             callinfo_dump( $caller_offset, \@callinfo );
164             }
165              
166 9         26 $subroutine = $callinfo[3];
167             # If we're inside an eval, go up one level further.
168 9 100 100     48 if(defined $subroutine and
169             $subroutine eq "(eval)") {
170 4         9 print "Inside an eval, one up\n" if _INTERNAL_DEBUG;
171 4         9 $levels_up++;
172 4         13 redo;
173             }
174             }
175 5 100       19 $subroutine = "main::" unless $subroutine;
176 5         12 print "Subroutine is '$subroutine'\n" if _INTERNAL_DEBUG;
177 5         15 $info{M} = $subroutine;
178 5         27 $info{l} = "$subroutine $filename ($line)";
179             }
180             }
181              
182 33         102 $info{X} = "[No curlies defined]";
183 33 50       132 $info{x} = Log::Log4perl::NDC->get() if $self->{info_needed}->{x};
184 33         94 $info{c} = $category;
185 33         86 $info{d} = 1; # Dummy value, corrected later
186 33         80 $info{n} = "\n";
187 33         80 $info{p} = $priority;
188 33         118 $info{P} = $$;
189 33         79 $info{H} = $HOSTNAME;
190              
191 33         71 my $current_time;
192              
193 33 100 66     154 if($self->{info_needed}->{r} or $self->{info_needed}->{R}) {
194 9 50 66     39 if(!$TIME_HIRES_AVAILABLE_WARNED++ and
195             !$self->{timer}->hires_available()) {
196 0         0 warn "Requested %r/%R pattern without installed Time::HiRes\n";
197             }
198 9         45 $current_time = [$self->{timer}->gettimeofday()];
199             }
200              
201 33 100       207 if($self->{info_needed}->{r}) {
202 9         32 $info{r} = $self->{timer}->milliseconds( $current_time );
203             }
204 33 100       245 if($self->{info_needed}->{R}) {
205 8         20 $info{R} = $self->{timer}->delta_milliseconds( $current_time );
206             }
207              
208             # Stack trace wanted?
209 33 50       204 if($self->{info_needed}->{T}) {
210 0         0 local $Carp::CarpLevel =
211             $Carp::CarpLevel + $caller_offset;
212 0         0 my $mess = Carp::longmess();
213 0         0 chomp($mess);
214             # $mess =~ s/(?:\A\s*at.*\n|^\s*Log::Log4perl.*\n|^\s*)//mg;
215 0         0 $mess =~ s/(?:\A\s*at.*\n|^\s*)//mg;
216 0         0 $mess =~ s/\n/, /g;
217 0         0 $info{T} = $mess;
218             }
219              
220             # As long as they're not implemented yet ..
221 33         89 $info{t} = "N/A";
222              
223 33         73 my @ops; #Added TG May 2018 - we need a key for substituting color values
224              
225             # Iterate over all info fields on the stack
226 33         76 for my $e (@{$self->{stack}}) {
  33         114  
227 88         237 my($op, $curlies) = @$e;
228              
229 88         161 my $result;
230              
231 88 50       296 if(exists $self->{USER_DEFINED_CSPECS}->{$op}) {
    50          
232 0 0       0 next unless $self->{info_needed}->{$op};
233 0         0 $self->{curlies} = $curlies;
234 0         0 $result = $self->{USER_DEFINED_CSPECS}->{$op}->($self,
235             $message, $category, $priority,
236             $caller_offset+1);
237             } elsif(exists $info{$op}) {
238 88         193 $result = $info{$op};
239 88 100       218 if($curlies) {
240             $result = $self->curly_action($op, $curlies, $info{$op},
241 37         208 $self->{printformat}, \@results);
242             } else {
243             # just for %d
244 51 50       145 if($op eq 'd') {
245 0         0 $result = $info{$op}->format($self->{timer}->gettimeofday());
246             }
247             }
248             } else {
249 0         0 warn "Format %'$op' not implemented (yet)";
250 0         0 $result = "FORMAT-ERROR";
251             }
252              
253 88 100       1123 $result = $self->{undef_column_value} unless defined $result;
254              
255 88         219 push @results, $result;
256 88         261 push @ops,$op; # Added TG May 2018 - collect op codes for key
257             }
258              
259             # dbi appender needs that
260 33 50 66     137 if( scalar @results == 1 and
261             !defined $results[0] ) {
262 0         0 return undef;
263             }
264 33         123 return +$self->_color_message(\@ops,\@results); # Changed TG May 2018
265             }
266              
267             ##################################################
268             sub _color_message {
269             ##################################################
270             #
271             # Tom Gracey May 2018
272             # Deliver a colored message from an array of
273             # op codes and accompaning results. Get color
274             # mappings from $self->{color_map}
275             #
276             # Slightly more difficult than it seems due to
277             # ANSI color characters playing havoc with
278             # formatting. It is necessary to substitute in the
279             # uncolored values first, then substitute the uncolored
280             # param for the colored one.
281             #
282             # But that could also cause problems if an uncolored
283             # string appears in the formatting string as well as
284             # the parameter. (You'd end up with 2 colored
285             # strings instead of the desired 1)
286             #
287             # So to catch this issue, break up the formatting string
288             # so that parameters appear once per fragment and
289             # at the front of the string, then perform 1
290             # substitution only per fragment. Finally rebuild.
291              
292 33     33   92 my ($self,$ops,$results) = @_;
293              
294 33         101 my @sfrags;
295 33         69 my $counter = 0;
296 33         153 foreach my $psection (split(/%%/,$self->{printformat})){
297              
298 36         312 my @pitems = split(/%(?!%)/,$psection);
299 36         148 foreach my $i (1..$#pitems){
300 88         251 $pitems[$i] = '%'.$pitems[$i];
301             }
302              
303 36         83 my @ifrags;
304 36         95 for my $i (1..$#pitems){
305 88         364 my $ifrag = sprintf( $pitems[$i], $results->[$counter] );
306 88         249 my $color = $self->{color_map}->{ $ops->[$counter] };
307 88 100       225 if ( $color ){
308 12 100   0   91 if ( ref $color eq ref (sub{}) ){
309 4         170 $color = $color->($results->[$counter]);
310             }
311 12         105 my $orig_res = $results->[$counter];
312 12         55 my $new_res = colored( $results->[$counter], $color );
313 12         838 $ifrag =~ s/\Q$orig_res\E/$new_res/;
314             }
315 88         223 push (@ifrags,$ifrag);
316 88         215 $counter++;
317             }
318 36         199 push(@sfrags,$pitems[0].join('',@ifrags));
319             }
320 33         280 return +join('%',@sfrags);
321             }
322              
323             1;
324              
325             =head1 NAME
326              
327             Log::Log4perl::Layout::ColoredPatternLayout - multicolor log messages
328              
329             =head1 SYNOPSIS
330              
331              
332             # in the logger config:
333              
334             log4j.appender.appndr1.layout.ColorMap = sub{
335             return {
336             d => 'blue on_white',
337             m => 'blue',
338             p => sub {
339             my $colors = {
340             trace => "green",
341             debug => "bold green",
342             info => "white",
343             warn => "yellow",
344             error => "red",
345             fatal => "bold red"
346             };
347             return +$colors->{ lc($_[0]) };
348             }
349             };
350             }
351              
352             log4j.appender.appndr1.layout.ConversionPattern
353             = '%d %-5p: %m%n'
354              
355              
356             # .. and log as usual in your code
357              
358             $logger->debug("A debug message");
359              
360             # Logs a debug message with the following colors:
361             #
362             # 2018-05-02 12:22:16 DEBUG A debug message
363             #
364             # ^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^
365             # 1 2 3
366             #
367             # 1 = blue on_white
368             # 2 = bold green
369             # 3 = blue
370              
371              
372             $logger->info("An info message");
373              
374             # Logs an info message with the following colors:
375             #
376             # 2018-05-02 12:22:16 INFO An info message
377             #
378             # ^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^
379             # 1 2 3
380             #
381             # 1 = blue on_white
382             # 2 = white
383             # 3 = blue
384              
385            
386              
387             =head1 DESCRIPTION
388              
389             There's no doubt Log::Log4perl is a fantastic logging system. It's a great weight off ones mind not having to worry (much!) about logging since Log::Log4perl seems to pretty much cover every eventuality and is very well battletested.
390              
391             An appender does exist which can colorise the whole message based on its log level (L). However, I wanted to colorise individual I of a message, rather than the whole thing. It can be easier on the eye, and save screen space by reducing the need for separators.
392              
393             I started with the assumpion that I could do this in a similar way to - ie by creating an appender. However, unfortunately the C sub only appears to get handed the final formatted message, rather than the message components. There doesn't seem to be any way to access this information from the inherited class.
394              
395             So instead this module inherits from L in order to solve the conundrum. It can be used as a replacement for L - but remember it only makes sense with I type appenders (otherwise ANSI color characters will be written to places where they shouldn't be).
396              
397             =head1 USAGE
398              
399             Usage is straightforward. Declare a I in your config - basically a hash which maps formatting codes (C<%p>, C<%d>, etc.) to ansi colors. (See L for valid color values). See the synopsis for an example.
400              
401             A value in the color map can be a simple string C, C etc. - or a sub that returns a string
402              
403             log4j.appender.appndr1.layout.ColorMap = sub {
404             return {
405             p => 'blue', # simple string
406              
407             F => sub { # sub returning simple string
408              
409             my ($filename) = @_;
410            
411             my $color = $filename = 'important.file'?'red':'white'
412            
413             return $color;
414             }
415              
416             };
417             };
418              
419             In this example if the filename where the logging event occurs (corrsponding to C<%F>) happens to be C then this will get printed to the terminal in red, while other filenames will be plain white.
420              
421             color map subs get passed a single parameter, containing the value of the variable corresponding to the formatting code which you can use to determine the output color (e.g. C, C for C<%p>, a date for <%d> etc).
422              
423             =head1 CAVEATS
424              
425             =over
426              
427             =item 1.
428              
429             As mentioned previously, this is for screen output only. Use C for anything else.
430              
431             =item 2.
432              
433             You can only colorise parts of the string corresponding to a formatting code. e.g. if your formatting string is:
434              
435             log4j.appender.appndr1.layout.ConversionPattern
436             = '[%d] %-5p: %m%n'
437              
438             then there is no way to colorise those square brackets. Sorry! However, perhaps with color the brackets are not necessary?
439              
440             =item 3.
441              
442             This won't work with L
443              
444             =item 4.
445              
446             I'm not entirely comfortable with the fact this inherits from L, and less comfortable still that it overrides C and another big subroutine C just to make minor changes. Unfortunately this seems necessary because those subs are large and the required info is buried somewhere in the middle. Thus an update to L has the potential to break this module. Should this happen I will attempt to review the method in general and fix where possible. But no guarantees.
447              
448             Of course if does get modified so it receives more information at some point in the future, then this module may not be necessary.
449              
450             =back
451              
452             =head1 SEE ALSO
453              
454             L
455             L
456             L
457             L
458             L
459             L
460             L
461              
462             =head1 AUTHOR
463              
464             Tom Gracey Etomgracey@gmail.comE
465              
466             =head1 COPYRIGHT AND LICENSE
467              
468             Copyright (C) 2017 by Tom Gracey
469              
470             This library is free software; you can redistribute it and/or modify
471             it under the same terms as Perl itself.
472              
473             =cut