File Coverage

blib/lib/Log/Any/Adapter/Filtered.pm
Criterion Covered Total %
statement 90 155 58.0
branch 17 56 30.3
condition 11 22 50.0
subroutine 22 38 57.8
pod 6 8 75.0
total 146 279 52.3


line stmt bran cond sub pod time code
1             package Log::Any::Adapter::Filtered;
2 2     2   1213 use strict;
  2         3  
  2         66  
3 2     2   9 use warnings;
  2         2  
  2         52  
4 2     2   925 use parent 'Log::Any::Adapter::Base';
  2         527  
  2         9  
5 2     2   20805 use Carp ();
  2         3  
  2         24  
6 2     2   6 use Scalar::Util ();
  2         378  
  2         27  
7 2     2   6 use Data::Dumper ();
  2         2  
  2         261  
8              
9             our $VERSION= '0.000000';
10              
11             # ABSTRACT: Logging adapter base class with support for filtering
12              
13              
14             our %level_map; # mapping from level name to numeric level
15             BEGIN {
16             # Initialize globals, and use %ENV vars for defaults
17 2     2   15 %level_map= (
18             min => -1,
19             trace => -1,
20             debug => 0,
21             info => 1,
22             notice => 2,
23             warning => 3,
24             error => 4,
25             critical => 5,
26             alert => 6,
27             emergency => 7,
28             max => 7,
29             );
30             # Make sure we have numeric levels for all the core logging methods
31 2         7 for ( Log::Any->logging_methods() ) {
32 18 50       37 if (!defined $level_map{$_}) {
33             # This is an attempt at being future-proof to the degree that a new level
34             # added to Log::Any won't kill a program using this logging adapter,
35             # but will emit a warning so it can be fixed properly.
36 0         0 warn __PACKAGE__." encountered unknown log level '$_'";
37 0         0 $level_map{$_}= 4;
38             }
39             }
40             # Now add numeric values for all the aliases, too
41 2         7 my %aliases= Log::Any->log_level_aliases;
42             $level_map{$_} ||= $level_map{$aliases{$_}}
43 2   33     373 for keys %aliases;
44             }
45              
46 68     68   2360 sub _log_level_value { $level_map{$_[1]} }
47              
48              
49 0     0 1 0 sub filter { $_[0]{filter} }
50              
51              
52             sub _coerce_filter_level {
53 14     14   2043 my ($class, $val)= @_;
54 14         15 my $n;
55 14 50 100     82 return (!defined $val || $val eq 'none')? $class->_log_level_value('min') - 1
    100 33        
    100          
    100          
56             : ($val eq 'all')? $class->_log_level_value('max')
57             : defined ($n= $class->_log_level_value($val))? $n
58             : ($val =~ /^([A-Za-z]+)([-+][0-9]+)$/) && defined ($n= $class->_log_level_value(lc $1))? $n + $2
59             : Carp::croak "unknown log level '$val'";
60             }
61              
62              
63             our %_default_filter;
64             BEGIN {
65 2     2   325 %_default_filter= ( '' => 'debug' );
66             }
67              
68             sub _default_filter_stack {
69 13     13   26 return ( \%_default_filter );
70             }
71              
72             sub _init_default_filter_var {
73 1     1   1 my $class= shift;
74 1         2 local $@;
75 1 50   5   92 eval '
  5         23  
76             package '.$class.';
77             our %_default_filter;
78             sub _default_filter_stack {
79             return ( \%_default_filter, $_[0]->SUPER::_default_filter_stack );
80             }
81             1;' == 1
82             or die my $e= $@;
83             }
84              
85             sub default_filter_for {
86 13     13 1 423 my ($class, $category)= @_;
87 13         166 my @filter_stack= $class->_default_filter_stack;
88 13 100 66     61 if (defined $category && length $category) {
89             defined $_->{$category} && return $_->{$category}
90 8   100     47 for @filter_stack;
91             }
92             defined $_->{''} && return $_->{''}
93 7   100     42 for @filter_stack;
94             }
95              
96             sub set_default_filter_for {
97 6     6 1 14 my ($class, $category, $value)= @_;
98 6         14 $class->_coerce_filter_level($value); # just testing for validity
99 6 50       10 $category= '' unless defined $category;
100 2     2   9 no strict 'refs';
  2         2  
  2         1325  
101 6 100       5 defined *{ $class . '::_default_filter' } or $class->_init_default_filter_var;
  6         31  
102 6         6 ${ $class . '::_default_filter' }{ $category }= $value;
  6         20  
103             }
104              
105              
106             sub dumper {
107 0 0   0 1 0 $_[0]{dumper}= $_[1] if @_ > 1;
108 0   0     0 $_[0]{dumper} ||= $_[0]->default_dumper
109             }
110              
111             sub default_dumper {
112 0     0 1 0 return \&_default_dumper;
113             }
114              
115             sub _default_dumper {
116 0     0   0 my $val= shift;
117 0 0       0 my $s= Data::Dumper->new([$val])->Indent(0)->Terse(1)->Useqq(1)->Quotekeys(0)
118             ->Maxdepth(Scalar::Util::blessed($val)? 2 : 4)->Sortkeys(1)->Dump;
119 0 0       0 substr($s, 2000-3)= '...' if length $s > 2000;
120 0         0 $s;
121             }
122              
123 0     0 0 0 sub category { $_[0]{category} }
124              
125              
126             sub write_msg {
127 0     0 1 0 my ($self, $level_name, $str)= @_;
128 0         0 print STDERR "$level_name: $str\n";
129             }
130              
131              
132             sub init {
133 0     0 0 0 my $self= shift;
134             # Apply default dumper if not set
135 0   0     0 $self->{dumper} ||= $self->default_dumper;
136             # Apply default filter if not set
137 0 0       0 defined $self->{filter}
138             or $self->{filter}= $self->default_filter_for($self->{category});
139            
140             # Rebless to a "level filter" package, which is a subclass of this one
141             # but with some methods replaced by empty subs.
142             # If log level is less than the minimum value, we show all messages, so no need to rebless.
143 0 0       0 (ref($self).'::Filter0')->can('info') or $self->_build_filtered_subclasses;
144 0         0 my $filter_value= $self->_coerce_filter_level($self->filter);
145 0         0 my $min_value= $self->_log_level_value('min');
146 0 0       0 if ($filter_value >= $min_value) {
147 0         0 my $max_value= $self->_log_level_value('max');
148 0 0       0 $filter_value= $max_value if $filter_value > $max_value;
149 0         0 my $pkg_suffix= $filter_value - $min_value;
150 0         0 bless $self, ref($self)."::Filter$pkg_suffix"
151             }
152            
153 0         0 return $self;
154             }
155              
156              
157              
158             # Programmatically generate all the info, infof, is_info ... methods
159             sub _build_logging_methods {
160 2     2   3 my $class= shift;
161 2 50       10 $class= ref $class if Scalar::Util::blessed($class);
162 2         2 my %seen;
163             # We implement the stock methods, but also 'fatal' because in my mind, fatal is not
164             # an alias for 'critical' and I want to see a prefix of "fatal" on messages.
165 2         5 for my $method ( grep { !$seen{$_}++ } Log::Any->logging_methods(), 'fatal' ) {
  20         34  
166 20         13 my ($impl, $printfn);
167 20 100       27 if ($class->_log_level_value($method) >= $class->_log_level_value('info')) {
168             # Standard logging. Concatenate everything as a string.
169             $impl= sub {
170 0 0   0   0 (shift)->write_msg($method, join('', map { !defined $_? '' : $_ } @_));
  0         0  
171 16         47 };
172             # Formatted logging. We dump data structures (because Log::Any says to)
173             $printfn= sub {
174 0     0   0 my $self= shift;
175 0 0       0 $self->write_msg($method, sprintf((shift), map { !defined $_? '' : !ref $_? $_ : $self->dumper->($_) } @_));
  0 0       0  
176 16         37 };
177             } else {
178             # Debug and trace logging. For these, we trap exceptions and dump data structures
179             $impl= sub {
180 0     0   0 my $self= shift;
181 0         0 local $@;
182 0 0       0 eval { $self->write_msg($method, join('', map { !defined $_? '' : !ref $_? $_ : $self->dumper->($_) } @_)); 1 }
  0 0       0  
  0 0       0  
  0         0  
183             or $self->warn("$@");
184 4         21 };
185             $printfn= sub {
186 0     0   0 my $self= shift;
187 0         0 local $@;
188 0 0       0 eval { $self->write_msg($method, sprintf((shift), map { !defined $_? '' : !ref $_? $_ : $self->dumper->($_) } @_)); 1; }
  0 0       0  
  0 0       0  
  0         0  
189             or $self->warn("$@");
190 4         107 };
191             }
192            
193             # Install methods in base package
194 2     2   10 no strict 'refs';
  2         2  
  2         224  
195 20         15 *{"${class}::$method"}= $impl;
  20         62  
196 20         15 *{"${class}::${method}f"}= $printfn;
  20         56  
197 20     0   50 *{"${class}::is_$method"}= sub { 1 };
  20         68  
  0         0  
198             }
199             # Now create any alias that isn't handled
200 2         19 my %aliases= Log::Any->log_level_aliases;
201 2         18 for my $method (grep { !$seen{$_}++ } keys %aliases) {
  10         14  
202 2     2   10 no strict 'refs';
  2         2  
  2         353  
203 8         6 *{"${class}::$method"}= *{"${class}::$aliases{$method}"};
  8         20  
  8         16  
204 8         6 *{"${class}::${method}f"}= *{"${class}::$aliases{$method}f"};
  8         20  
  8         12  
205 8         7 *{"${class}::is_$method"}= *{"${class}::is_$aliases{$method}"};
  8         75  
  8         11  
206             }
207             }
208              
209             # Create per-filter-level packages
210             # This is an optimization for minimizing overhead when using disabled levels
211             sub _build_filtered_subclasses {
212 0     0   0 my $class= shift;
213 0 0       0 $class= ref $class if Scalar::Util::blessed($class);
214 0         0 my $min_level= $class->_log_level_value('min');
215 0         0 my $max_level= $class->_log_level_value('max');
216 0         0 my $pkg_suffix_ofs= 0 - $min_level;
217            
218             # Create packages, inheriting from $class
219 0         0 for ($min_level .. $max_level) {
220 0         0 my $suffix= $_ - $min_level;
221 2     2   8 no strict 'refs';
  2         2  
  2         139  
222 0         0 push @{"${class}::Filter${suffix}::ISA"}, $class;
  0         0  
223             }
224             # For each method, mask it in any package of a higher filtering level
225 0         0 for my $method (keys %level_map) {
226 0         0 my $level= $class->_log_level_value($method);
227             # Suppress methods in all higher filtering level packages
228 0         0 for ($level .. $max_level) {
229 0         0 my $suffix= $_ - $min_level;
230 2     2   15 no strict 'refs';
  2         2  
  2         246  
231 0     0   0 *{"${class}::Filter${suffix}::$method"}= sub {};
  0         0  
  0         0  
232 0     0   0 *{"${class}::Filter${suffix}::${method}f"}= sub {};
  0         0  
  0         0  
233 0     0   0 *{"${class}::Filter${suffix}::is_$method"}= sub { 0 }
  0         0  
234 0         0 }
235             }
236             }
237              
238             BEGIN {
239 2     2   10 __PACKAGE__->_build_logging_methods;
240             }
241              
242             1;
243              
244             __END__