File Coverage

blib/lib/Log/Tree.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             package Log::Tree;
2             {
3             $Log::Tree::VERSION = '0.15';
4             }
5             BEGIN {
6 1     1   45765 $Log::Tree::AUTHORITY = 'cpan:TEX';
7             }
8             # ABSTRACT: lightweight but highly configurable logging class
9              
10 1     1   23 use 5.010_000;
  1         3  
  1         29  
11 1     1   879 use mro 'c3';
  1         818  
  1         7  
12 1     1   50 use feature ':5.10';
  1         2  
  1         119  
13              
14 1     1   413 use Moose;
  0            
  0            
15             use namespace::autoclean;
16              
17             # use IO::Handle;
18             # use autodie;
19             # use MooseX::Params::Validate;
20              
21             use English qw( -no_match_vars );
22             use Log::Dispatch;
23             use Log::Dispatch::Screen;
24             use Data::Tree;
25             use IO::Interactive qw();
26              
27             has 'dispatcher' => (
28             'is' => 'ro',
29             'isa' => 'Log::Dispatch',
30             'required' => 0,
31             'lazy' => 1,
32             'builder' => '_init_dispatcher',
33             );
34              
35             has 'filename' => (
36             'is' => 'ro',
37             'isa' => 'Str',
38             'lazy' => 1,
39             'builder' => '_init_filename',
40             );
41              
42             has 'facility' => (
43             'is' => 'ro',
44             'isa' => 'Str',
45             'required' => 1,
46             );
47              
48             has 'recipients' => (
49             'is' => 'rw',
50             'isa' => 'ArrayRef[Str]',
51             );
52              
53             has '_buffer' => (
54             'is' => 'rw',
55             'isa' => 'ArrayRef',
56             'default' => sub { [] },
57             );
58              
59             has 'prefix_caller' => (
60             'is' => 'rw',
61             'isa' => 'Bool',
62             'default' => 1,
63             );
64              
65             has 'prefix_ts' => (
66             'is' => 'rw',
67             'isa' => 'Bool',
68             'default' => 1,
69             );
70              
71             has 'prefix_level' => (
72             'is' => 'rw',
73             'isa' => 'Bool',
74             'default' => 1,
75             );
76              
77             has 'prefix' => (
78             'is' => 'rw',
79             'isa' => 'Str',
80             'default' => q{},
81             );
82              
83             has 'verbosity' => (
84             'is' => 'rw',
85             'isa' => 'Int',
86             'default' => 0,
87             'trigger' => \&_set_level,
88             );
89              
90             has 'loglevels' => (
91             'is' => 'rw',
92             'isa' => 'Data::Tree',
93             'lazy' => 1,
94             'builder' => '_init_loglevels',
95             );
96              
97             has 'severities' => (
98             'is' => 'ro',
99             'isa' => 'ArrayRef',
100             'lazy' => 1,
101             'builder' => '_init_severities',
102             );
103              
104             has 'syslog' => (
105             'is' => 'ro',
106             'isa' => 'Bool',
107             'default' => 0,
108             );
109              
110             has 'config' => (
111             'is' => 'rw',
112             'isa' => 'Config::Yak',
113             'required' => 0,
114             'trigger' => \&_set_config,
115             );
116              
117             sub _init_severities {
118             return [qw(debug info notice warning error critical alert emergency)];
119             }
120              
121             sub _init_loglevels {
122             my $self = shift;
123              
124             my $Tree = Data::Tree::->new();
125             $Tree->set( '__LEVEL__', 'debug' );
126              
127             $self->_update_loglevels();
128              
129             return $Tree;
130             }
131              
132             sub _update_loglevels {
133             my $self = shift;
134              
135             return unless $self->config();
136             # TODO read config and set apt levels
137             }
138              
139             sub _set_level {
140             my ( $self, $new_value, $old_value ) = @_;
141              
142             if ( $self->dispatcher()->output('Screen') ) {
143             $self->dispatcher()->output('Screen')->{'min_level'} = $self->_verbosity_to_level($new_value);
144             }
145              
146             return;
147             }
148              
149             sub _set_config {
150             my ( $self, $new_value, $old_value ) = @_;
151              
152             $self->_update_loglevels();
153              
154             return;
155             }
156              
157             sub _verbosity_to_level {
158             my $self = shift;
159             my $verbosity = shift;
160              
161             my $level = 7;
162             my $default_level = 4;
163              
164             $level = ( $default_level - $verbosity );
165              
166             if ( $level < 0 ) {
167             $level = 0;
168             }
169             elsif ( $level > 7 ) {
170             $level = 7;
171             }
172             return $level;
173             }
174              
175             sub severity_to_level {
176             my $self = shift;
177             my $sev = shift;
178              
179             # already numeric? so it's a level
180             if ( !$sev || $sev =~ m/^\d+$/ ) {
181             return $sev;
182             }
183              
184             if ( $sev =~ m/debug/i ) {
185             return 0;
186             }
187             elsif ( $sev =~ m/info/i ) {
188             return 1;
189             }
190             elsif ( $sev =~ m/notice/i ) {
191             return 2;
192             }
193             elsif ( $sev =~ m/warn(?:ing)?/i ) {
194             return 3;
195             }
196             elsif ( $sev =~ m/err(?:or)?/i ) {
197             return 4;
198             }
199             elsif ( $sev =~ m/crit(?:ical)?/i ) {
200             return 5;
201             }
202             elsif ( $sev =~ m/alert/i ) {
203             return 6;
204             }
205             elsif ( $sev =~ m/emerg(?:ency)/i ) {
206             return 7;
207             }
208             else {
209             return 0;
210             }
211             }
212              
213             sub level_to_severity {
214             my $self = shift;
215             my $level = shift;
216              
217             # doesn't look like a level ... so bail out
218             if ( !$level || $level !~ m/^\d+$/ ) {
219             return $level;
220             }
221              
222             if ( $level < 0 ) {
223             return 'debug';
224             }
225             elsif ( $level > 7 ) {
226             return 'emergency';
227             }
228             else {
229             return $self->severities()->[$level];
230             }
231             }
232              
233             sub get_buffer {
234             my $self = shift;
235             my $min_level = shift || 0;
236              
237             # make sure it's a numeric value
238             $min_level = $self->severity_to_level($min_level);
239              
240             my @lines = ();
241             if ( $min_level < 1 ) {
242             @lines = @{ $self->_buffer() };
243             }
244             else {
245              
246             # filter out only those whose severity is important enough
247             foreach my $line ( @{ $self->_buffer() } ) {
248             if ( $self->severity_to_level( $line->{'level'} ) >= $min_level ) {
249             push( @lines, $line );
250             }
251             }
252             }
253              
254             return \@lines;
255             }
256              
257             sub clear_buffer {
258             my $self = shift;
259             $self->_buffer( [] );
260              
261             return;
262             }
263              
264             # clean up after forking
265             sub forked {
266             my $self = shift;
267              
268             $self->clear_buffer();
269              
270             return 1;
271             }
272              
273             sub add_to_buffer {
274             my $self = shift;
275             my $obj = shift;
276              
277             # make sure the buffer doesn't get too big
278             if ( @{ $self->_buffer() } > 1_000_000 ) {
279             shift @{ $self->_buffer() };
280             }
281             push( @{ $self->_buffer() }, $obj );
282              
283             return 1;
284             }
285              
286             sub _init_filename {
287             my $self = shift;
288              
289             my $name = lc( $self->facility() );
290             $name =~ s/\W/-/g;
291             $name =~ s/_/-/g;
292             if ( $name !~ m/\.log$/ ) {
293             $name .= '.log';
294             }
295             if ( -w '/var/log/' ) {
296             return '/var/log/' . $name;
297             }
298             else {
299             return '/tmp/' . $name;
300             }
301             }
302              
303             sub _check_filename {
304             my $self = shift;
305             my $filename = shift;
306              
307             if ( -f $filename ) {
308             if ( -w $filename ) {
309             return $filename;
310             }
311             else {
312             return $self->_init_filename();
313             }
314             }
315             else {
316             my @path = split /\//, $filename;
317             pop @path;
318             my $basedir = join '/', @path;
319             if ( -w $basedir ) {
320             return $filename;
321             }
322             else {
323             return $self->_init_filename();
324             }
325             }
326             }
327              
328             sub _init_dispatcher {
329             my $self = shift;
330              
331             my $log = Log::Dispatch::->new();
332              
333             # only log to screen if running interactively
334             if(IO::Interactive::is_interactive()) {
335             $log->add(
336             Log::Dispatch::Screen::->new(
337             name => 'screen',
338             min_level => $self->_verbosity_to_level( $self->verbosity() ),
339             )
340             );
341             }
342              
343             if ( $self->syslog() && $self->facility() ) {
344             require Log::Dispatch::Syslog;
345             $log->add(
346             Log::Dispatch::Syslog::->new(
347             name => 'syslog',
348             min_level => 'warning',
349             ident => $self->facility(),
350             )
351             );
352             }
353              
354             if ( $self->filename() ) {
355             require Log::Dispatch::File::Locked;
356             $log->add(
357             Log::Dispatch::File::Locked::->new(
358             name => 'file',
359             min_level => 'debug',
360             'mode' => 'append',
361             'close_after_write' => 1,
362             filename => $self->filename(),
363             )
364             );
365             }
366              
367             if ( $self->recipients() ) {
368             require Log::Dispatch::Email::MailSender;
369             $log->add(
370             Log::Dispatch::Email::MailSender::->new(
371             name => 'email',
372             min_level => 'emerg',
373             to => join( ',', @{ $self->recipients() } ),
374             subject => $self->facility() . ' - EMERGENCY',
375             )
376             );
377             }
378             return $log;
379             }
380              
381             # DGR: speeeed
382             ## no critic (RequireArgUnpacking)
383             sub _real_caller {
384              
385             # $_[0] -> self
386             # $_[1] -> calldepth
387             my $max_depth = 255;
388             my $min_depth = 2;
389             $min_depth += $_[1] if $_[1];
390              
391             # 0 is this sub -> not relevant
392             # 1 is Logger::log -> not relevant
393             # we want to know who called Logger::log (unless its an eval or Try)
394             foreach my $i ( 1 .. $max_depth ) {
395             my @c = caller($i);
396             return caller( $i - 1 ) unless @c; # no caller information?
397             next unless $c[0];
398             next if $c[0] eq 'Try::Tiny'; # package Try::Tiny? Skip.
399             next unless $c[3];
400             next if $c[3] eq 'Log::Tree::log';
401             next if $c[3] eq 'Try::Tiny::try'; # calling sub Try::Tiny::try? Skip.
402             next if $c[3] eq '(eval)'; # calling sub some kind of eval? Skip.
403             next if $c[3] =~ m/__ANON__/; # calling sub some kind of anonymous sub? Skip.
404             return @c;
405             }
406             return ();
407             }
408             ## use critic
409              
410             # DGR: speeeed
411             ## no critic (RequireArgUnpacking)
412             sub _would_log {
413              
414             # $_[0] -> self
415             # $_[1] -> caller
416             # $_[2] -> level
417              
418             my @cp = ();
419             if ( $_[1] ) {
420             @cp = split /::/, $_[1];
421             }
422              
423             while (@cp) {
424             my $min_sev = $_[0]->loglevels()->get( [ @cp, '__LEVEL__' ] );
425             if ($min_sev) {
426             my $min_lvl = $_[0]->severity_to_level($min_sev);
427             if ( defined($min_lvl) && $_[0]->severity_to_level( $_[2] ) >= $min_lvl ) {
428             return 1;
429             }
430             }
431             pop @cp;
432             }
433             my $min_sev = $_[0]->loglevels()->get('__LEVEL__');
434             if ($min_sev) {
435             my $min_lvl = $_[0]->severity_to_level($min_sev);
436             if ( defined($min_lvl) && $_[0]->severity_to_level( $_[2] ) >= $min_lvl ) {
437             return 1;
438             }
439             }
440             return;
441             }
442             ## use critic
443              
444             ## no critic (ProhibitBuiltinHomonyms RequireArgUnpacking)
445             sub log {
446             ## use critic
447             my $self = shift;
448              
449             my %params = ();
450             my ( $package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, $hints, $bitmask, $hinthash ) = $self->_real_caller();
451             if ( $package eq 'main' && $subroutine eq 'Log::Tree::log' ) {
452             $subroutine = q{};
453             }
454              
455             if ( scalar(@_) % 2 == 0 ) {
456             %params = @_;
457             }
458             else {
459             $params{'message'} = 'Incorrect usage of log in ' . $subroutine . '. Args: ' . join( q{ }, @_ );
460             $params{'level'} = 'error';
461             }
462              
463             $params{'ts'} = time();
464             $params{'level'} ||= 'debug';
465              
466             # skip messages we don't want to log
467             return unless $self->_would_log( $subroutine, $params{'level'} );
468             $subroutine ||= 'n/a';
469             $params{'caller'} = $subroutine unless $params{'caller'};
470              
471             # resolve any code ref
472             if ( $params{'message'} && ref( $params{'message'} ) eq 'CODE' ) {
473             $params{'message'} = &{ $params{'message'} }();
474             }
475              
476             $self->add_to_buffer( \%params );
477              
478             # IMPORTANT: Since we add a hash_REF to the buffer, everything we do to the hash itself affects the buffer, too
479             # So if we want to modify the hash given to the dispatcher, but not the one in the buffer we have to create a copy.
480             # Otherwise the buffer is cluttered with information we don't want.
481             my %params_disp = %params;
482              
483             # we use tabs to separated the fields, so remove any tabs already present
484             $params_disp{'message'} =~ s/\t/ /g;
485              
486             # prepend log level
487             if ( $self->prefix_level() ) {
488             $params_disp{'message'} = uc( $params_disp{'level'} ) . "\t" . $params_disp{'message'};
489             }
490              
491             # prepend log message w/ the caller
492             if ( $self->prefix_caller() ) {
493             $params_disp{'message'} = $params_disp{'caller'} . "\t" . $params_disp{'message'};
494             }
495              
496             # prepend a user-supplied prefix, e.g. [CHILD 24324/234342]
497             if ( $self->prefix() ) {
498             $params_disp{'message'} = $self->prefix() . "\t" . $params_disp{'message'};
499             }
500              
501             # prepend log message w/ a timestamp
502             if ( $self->prefix_ts() ) {
503             my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime( $params{'ts'} );
504             $year += 1900;
505             $mon++;
506             $params_disp{'message'} = sprintf( '%04i.%02i.%02i-%02i:%02i:%02i', $year, $mon, $mday, $hour, $min, $sec ) . "\t" . $params_disp{'message'};
507             }
508             $params_disp{'message'} .= "\n";
509             return $self->dispatcher()->log(%params_disp);
510             }
511              
512             around BUILDARGS => sub {
513             my $orig = shift;
514             my $class = shift;
515              
516             if ( @_ == 1 && !ref $_[0] ) {
517             return $class->$orig( facility => $_[0] );
518             }
519             else {
520             return $class->$orig(@_);
521             }
522             };
523              
524             sub BUILD {
525             my $self = shift;
526             my $args = shift;
527              
528             if ( $args->{'filename'} ) {
529             $self->{'filename'} = $self->_check_filename( $args->{'filename'} );
530             }
531             else {
532             $self->{'filename'} = $self->_init_filename();
533             }
534              
535             return 1;
536             }
537              
538             no Moose;
539             __PACKAGE__->meta->make_immutable;
540              
541             1;
542              
543             __END__
544              
545             =pod
546              
547             =encoding utf-8
548              
549             =head1 NAME
550              
551             Log::Tree - lightweight but highly configurable logging class
552              
553             =head1 SYNOPSIS
554              
555             use Log::Tree;
556              
557             my $logger = Log::Tree::->new('foo');
558             ...
559              
560             =head1 ATTRIBUTES
561              
562             =head2 facility
563              
564             Only mandatory attirbute. Used as the syslog faclity and to auto-construct a suiteable
565             filename for logging to file.
566              
567             =head1 METHODS
568              
569             =head2 add_to_buffer
570              
571             This method is usually not needed from by callers but may be in some rare ocasions
572             that's why it's made part of the public API. It just adds the passed data to the
573             internal buffer w/o logging it in the usual ways.
574              
575             =head2 clear_buffer
576              
577             This method clears the internal log buffer.
578              
579             =head2 forked
580              
581             This method should be called after it has been fork()ed to clear the internal
582             log buffer.
583              
584             =head2 get_buffer
585              
586             Retrieve those entries from the buffer that are gte the given severity.
587              
588             =head2 log
589              
590             Log a message. Takes a hash containing at least "message" and "level".
591              
592             =head2 BUILD
593              
594             Call on instatiation to set this class up.
595              
596             =head2 level_to_severity
597              
598             Translates a numeric level to severity string.
599              
600             =head2 severity_to_level
601              
602             Translates a severity string to a numeric level.
603              
604             =head1 NAME
605              
606             Log::Tree - Lightyweight logging w/ a tree based verbosity configuration
607             similar to Log4perl.
608              
609             =head1 AUTHOR
610              
611             Dominik Schulz <dominik.schulz@gauner.org>
612              
613             =head1 COPYRIGHT AND LICENSE
614              
615             This software is copyright (c) 2012 by Dominik Schulz.
616              
617             This is free software; you can redistribute it and/or modify it under
618             the same terms as the Perl 5 programming language system itself.
619              
620             =cut