File Coverage

blib/lib/EntityModel/Log.pm
Criterion Covered Total %
statement 27 53 50.9
branch n/a
condition 0 8 0.0
subroutine 9 17 52.9
pod n/a
total 36 78 46.1


line stmt bran cond sub pod time code
1             package EntityModel::Log;
2             # ABSTRACT: Logging class used by EntityModel
3 2     2   14641 use strict;
  2         3  
  2         77  
4 2     2   10 use warnings FATAL => 'all', NONFATAL => 'redefine';
  2         4  
  2         115  
5 2     2   857 use parent qw{Exporter};
  2         542  
  2         10  
6              
7             our $VERSION = '0.001';
8              
9             =head1 NAME
10              
11             EntityModel::Log - simple logging support for L
12              
13             =head1 SYNOPSIS
14              
15             use EntityModel::Log ':all';
16             logDebug("Test something");
17             logInfo("Object [%s] found", $obj->name);
18             logError("Fatal problem");
19             logInfo(sub { my $str = heavy_operation(); return 'Failed: %s', $str });
20              
21             =head1 DESCRIPTION
22              
23             Yet another logging class. Provides a procedural and OO interface as usual.
24              
25             =cut
26              
27             # Need to be able to switch off logging in UNITCHECK stages, since that segfaults perl5.10.1 and possibly other versions
28             our $DISABLE = 0;
29              
30 2     2   1082 use Time::HiRes qw{time};
  2         2490  
  2         8  
31 2     2   1207 use POSIX qw{strftime};
  2         11093  
  2         15  
32 2     2   2249 use Exporter;
  2         3  
  2         87  
33 2     2   7 use List::Util qw{min max};
  2         2  
  2         185  
34 2     2   1029 use IO::Handle;
  2         9963  
  2         91  
35 2     2   990 use Data::Dump ();
  2         9534  
  2         1636  
36              
37             our %EXPORT_TAGS = ( 'all' => [qw/&logDebug &logInfo &logWarning &logError &logStack/] );
38             our @EXPORT_OK = ( @{$EXPORT_TAGS{'all'}} );
39              
40             # Internal singleton instance
41             my $instance;
42 0   0 0     sub instance { my $class = shift; $instance ||= $class->new }
  0            
43              
44             =head1 PROCEDURAL METHODS
45              
46             =cut
47              
48             my @LogType = (
49             'Debug',
50             'Info',
51             'Warning',
52             'ERROR'
53             );
54              
55 0     0     sub logBase { __PACKAGE__->instance->raise(@_); }
56              
57             =head2 logDebug
58              
59             Raise a debug message, but only if the debug flag is set. Expect a high volume of these during normal operation
60             so live server has this switched off.
61              
62             =cut
63              
64 0     0     sub logDebug { unshift @_, 0; goto &logBase; }
  0            
65              
66             =head2 logInfo
67              
68             Raise an informational message, which we'd like to track for stats
69             reasons - indicates normal operations rather than an error condition.
70              
71             =cut
72              
73 0     0     sub logInfo { unshift @_, 1; goto &logBase; }
  0            
74              
75             =head2 logWarning
76              
77             Raise a warning message, for things like 'article does not exist', expect a few of these in regular operations
78             but they aren't serious enough to be potential bugs or system problems.
79              
80             =cut
81              
82 0     0     sub logWarning { unshift @_, 2; goto &logBase; }
  0            
83              
84             =head2 logError
85              
86             Raise an error - this is likely to be a genuine system problem.
87              
88             =cut
89              
90 0     0     sub logError { unshift @_, 3; goto &logBase; }
  0            
91              
92             =head2 logStack
93              
94             Raise an error with stack - this is likely to be a genuine system problem.
95              
96             =cut
97              
98             sub logStack {
99 0     0     my $txt = __PACKAGE__->instance->parseMessage(@_);
100              
101             $txt .= join("\n", map {
102 0           sprintf("%s:%s %s", $_->{filename}, $_->{line}, $_->{subroutine})
103 0           } stackTrace());
104 0           logBase(3, $txt);
105             }
106              
107             =head2 stackTrace
108              
109             Get a stack trace, as an array of hashref entries, skipping the top two levels.
110              
111             =cut
112              
113             sub stackTrace {
114 0     0     my $idx = 1;
115 0           my @trace;
116 0           my $basePath = '';
117 0   0       while($idx < 99 && (my @stack = caller($idx))) {
118 0           my %info;
119 0           foreach (qw/package filename line subroutine hasargs wantarray evaltext is_require hints bitmask hinthash/) {
120 0   0       $info{$_} = (shift(@stack) // '');
121             }
122 0           if(0) { # could include source context using something like $info{filename} ~~ m{^$basePath/(.*)$} || $info{filename} ~~ m{^/perl-module-path/(.*)$}) {
123             my $file = $1;
124             if(-r $info{filename}) {
125             my $start = max(0, ($info{line} // 0) - 5);
126             $info{code} = '';
127             open my $fh, '<', $info{filename} or die $!;
128             if($start) {
129             <$fh> for 0..$start;
130             }
131             $info{code} .= sprintf("%5d %s", $fh->input_line_number + 1, scalar(<$fh>)) for 0..10;
132             close $fh;
133             }
134             $info{filename} = $file;
135             }
136 0           push @trace, \%info;
137 0           ++$idx;
138             }
139 0           return @trace;
140             }
141              
142             sub levelFromString {
143             my $str = lc(shift);
144             my $idx = 0;
145             foreach (@LogType) {
146             return $idx if $str ~~ lc($_);
147             ++$idx;
148             }
149             die "Bad log level [$str]";
150             }
151              
152             sub timestamp {
153             my $now = Time::HiRes::time;
154             return strftime("%Y-%m-%d %H:%M:%S", gmtime($now)) . sprintf(".%03d", int($now * 1000.0) % 1000.0);
155             }
156              
157             =head2 OO METHODS
158              
159             =cut
160              
161             =head2 new
162              
163             Constructor - currently doesn't do much.
164              
165             =cut
166              
167             sub new { bless { path => 'entitymodel.log' }, shift }
168              
169             sub path {
170             my $self = shift;
171             if(@_) {
172             $self->close if $self->isOpen;
173             $self->{path} = shift;
174             $self->open;
175             return $self;
176             }
177             return $self->{path};
178             }
179              
180             sub handle {
181             my $self = shift;
182             if(@_) {
183             $self->close if $self->isOpen;
184             $self->{handle} = shift;
185             $self->isOpen(1);
186             $self->pid($$);
187             return $self;
188             }
189             return $self->{handle};
190             }
191              
192             sub pid {
193             my $self = shift;
194             if(@_) {
195             $self->{pid} = shift;
196             return $self;
197             }
198             return $self->{pid};
199             }
200              
201             sub isOpen {
202             my $self = shift;
203             if(@_) {
204             $self->{isOpen} = shift;
205             return $self;
206             }
207             return $self->{isOpen};
208             }
209              
210             sub disabled {
211             my $self = shift;
212             if(@_) {
213             $self->{disabled} = shift;
214             return $self;
215             }
216             return $self->{disabled};
217             }
218              
219             sub close : method {
220             my $self = shift;
221             return $self unless $self->isOpen;
222             if($self->handle) {
223             close $self->handle;
224             }
225              
226             # Clear handle *after* isOpen status
227             $self->isOpen(0)->handle(undef);
228             }
229              
230             sub open : method {
231             my $self = shift;
232             return $self if $self->isOpen;
233             open my $fh, '>>', $self->path or die $! . " for " . $self->path;
234             binmode $fh, ':encoding(utf8)';
235             $fh->autoflush(1);
236             $self->handle($fh)->isOpen(1)->pid($$);
237             }
238              
239             sub reopen {
240             my $self = shift;
241             $self->close if $self->isOpen;
242             $self->open;
243             return $self;
244             }
245              
246             =head2 parseMessage
247              
248             Generate appropriate text based on whatever we get passed.
249              
250             Each item in the parameter list is parsed first, then the resulting items are passed through L. If only a single item is in the list then the resulting string is returned directly.
251              
252             Item parsing handles the following types:
253              
254             =over 4
255              
256             =item * Single string is passed through unchanged
257              
258             =item * Any coderef is expanded in place
259              
260             =item * Arrayref or hashref is expanded via L
261              
262             =item * Other references are stringified
263              
264             =item * Undef items are replaced with the text 'undef'
265              
266             =back
267              
268             =cut
269              
270             sub parseMessage {
271             my $self = shift;
272             return '' unless @_;
273              
274             # Decompose parameters into strings
275             my @data;
276             ITEM:
277             while(@_) {
278             my $entry = shift;
279              
280             # Convert to string if we can
281             if(my $ref = ref $entry) {
282             if($ref ~~ /^CODE/) {
283             unshift @_, $entry->();
284             next ITEM;
285             } elsif($ref ~~ [qw{ARRAY HASH}]) {
286             $entry = Data::Dump::dump($entry);
287             } else {
288             $entry = "$entry";
289             }
290             }
291             $entry //= 'undef';
292             push @data, $entry;
293             }
294              
295             # Format appropriately
296             my $fmt = shift(@data) // '';
297             return $fmt unless @data;
298              
299             return sprintf($fmt, @data);
300             }
301              
302             sub min_level {
303             my $self = shift;
304             if(@_) {
305             $self->{min_level} = shift;
306             return $self;
307             }
308             return $self->{min_level};
309             }
310              
311             =head2 raise
312              
313             Raise a log message
314              
315             =over 4
316              
317             =item * $level - numeric log level
318              
319             =item * @data - message data
320              
321             =back
322              
323             =cut
324              
325             sub raise {
326             my $self = shift;
327             return $self if $self->disabled;
328              
329             my $level = shift;
330             my ($pkg, $file, $line, $sub) = caller(1);
331              
332             # caller(0) gives us the wrong sub for our purposes - we want whatever raised the logXX line
333             (undef, undef, undef, $sub) = caller(2);
334              
335             # Apply minimum log level based on method, then class, then default 'info'
336             my $minLevel = ($sub ? $self->{mask}->{$sub}->{level} : undef);
337             $minLevel //= $self->{mask}->{$pkg}->{level};
338             $minLevel //= $self->{min_level};
339             $minLevel //= 1;
340             return $self if $minLevel > $level;
341              
342             my $txt = $self->parseMessage(@_);
343              
344             # Explicitly get time from Time::HiRes for ms accuracy
345             my $ts = timestamp();
346              
347             my $type = sprintf("%-8.8s", $LogType[$level]);
348             $self->reopen unless $$ ~~ $self->pid;
349             $self->open unless $self->isOpen;
350             $self->handle->print("$ts $type $file:$line $txt\n");
351             return $self;
352             }
353              
354              
355             =head2 debug
356              
357             Log a message at debug level.
358              
359             =cut
360              
361             sub debug {
362             my $self = shift;
363             }
364              
365             END { $instance->close if $instance; }
366              
367             1;
368              
369             =head1 AUTHOR
370              
371             Tom Molesworth
372              
373             =head1 LICENSE
374              
375             Copyright Tom Molesworth 2008-2011. Licensed under the same terms as Perl itself.
376