File Coverage

blib/lib/Log/Dispatch/Dir.pm
Criterion Covered Total %
statement 122 131 93.1
branch 32 42 76.1
condition 22 29 75.8
subroutine 15 15 100.0
pod 2 2 100.0
total 193 219 88.1


line stmt bran cond sub pod time code
1             package Log::Dispatch::Dir;
2              
3             our $DATE = '2017-07-11'; # DATE
4             our $VERSION = '0.15'; # VERSION
5              
6 2     2   166357 use 5.010001;
  2         8  
7 2     2   10 use warnings;
  2         4  
  2         46  
8 2     2   8 use strict;
  2         4  
  2         34  
9 2     2   967 use Log::Dispatch::Output;
  2         595351  
  2         100  
10 2     2   26 use base qw(Log::Dispatch::Output);
  2         7  
  2         250  
11              
12 2     2   681 use File::Slurper qw(write_text);
  2         11784  
  2         161  
13             #use File::Stat qw(:stat); # doesn't work in all platforms?
14 2     2   1051 use Params::Validate qw(validate SCALAR CODEREF);
  2         4906  
  2         133  
15 2     2   828 use POSIX;
  2         9787  
  2         18  
16              
17             Params::Validate::validation_options( allow_extra => 1 );
18              
19             sub new {
20 8     8 1 62232 my $proto = shift;
21 8   33     63 my $class = ref $proto || $proto;
22              
23 8         59 my %p = @_;
24              
25 8         27 my $self = bless {}, $class;
26              
27 8         83 $self->_basic_init(%p);
28 8         1217 $self->_make_handle(%p);
29              
30 8         53 return $self;
31             }
32              
33             sub _make_handle {
34 8     8   25 my $self = shift;
35              
36 8         1828 my %p = validate(
37             @_,
38             {
39             dirname => { type => SCALAR },
40             permissions => { type => SCALAR , optional => 1 },
41             filename_pattern => { type => SCALAR , optional => 1 },
42             filename_sub => { type => CODEREF, optional => 1 },
43             max_size => { type => SCALAR , optional => 1 },
44             max_files => { type => SCALAR , optional => 1 },
45             max_age => { type => SCALAR , optional => 1 },
46             rotate_probability => { type => SCALAR , optional => 1 },
47             });
48              
49 8         115 $self->{dirname} = $p{dirname};
50 8         30 $self->{permissions} = $p{permissions};
51             $self->{filename_pattern} = $p{filename_pattern} ||
52 8   100     64 '%Y-%m-%d-%H%M%S.pid-%{pid}.%{ext}';
53 8         22 $self->{filename_sub} = $p{filename_sub};
54 8         24 $self->{max_size} = $p{max_size};
55 8         25 $self->{max_files} = $p{max_files};
56 8         21 $self->{max_age} = $p{max_age};
57 8   100     47 $self->{rotate_probability} = ($p{rotate_probability}) || 0.25;
58 8         37 $self->_open_dir();
59             }
60              
61             sub _open_dir {
62 8     8   20 my $self = shift;
63              
64 8 100       243 unless (-e $self->{dirname}) {
65 7   100     59 my $perm = $self->{permissions} // 0755;
66 7 50       490 mkdir($self->{dirname}, $perm)
67             or die "Cannot create directory `$self->{dirname}: $!";
68 7         37 $self->{chmodded} = 1;
69             }
70              
71 8 50       125 unless (-d $self->{dirname}) {
72 0         0 die "$self->{dirname} is not a directory";
73             }
74              
75 8 100 100     80 if ($self->{permissions} && ! $self->{chmodded}) {
76             chmod $self->{permissions}, $self->{dirname}
77 1 50       18 or die "Cannot chmod $self->{dirname} to $self->{permissions}: $!";
78 1         4 $self->{chmodded} = 1;
79             }
80             }
81              
82             my $default_ext = "log";
83             my $libmagic;
84              
85             sub _resolve_pattern {
86 22     22   76 my ($self, $p) = @_;
87 22         61 my $pat = $self->{filename_pattern};
88 22         83 my $now = time;
89              
90 22         112 my @vars = qw(Y y m d H M S z Z %);
91 22         77 my $strftime = POSIX::strftime(join("|", map {"%$_"} @vars),
  220         2725  
92             localtime($now));
93 22         111 my %vars;
94 22         61 my $i = 0;
95 22         130 for (split /\|/, $strftime) {
96 220         610 $vars{ $vars[$i] } = $_;
97 220         466 $i++;
98             }
99              
100 22         80 push @vars, "{pid}";
101 22         92 $vars{"{pid}"} = $$;
102              
103 22         55 push @vars, "{ext}";
104             $vars{"{ext}"} = sub {
105 19     19   44 my $p = shift;
106 19 100       69 unless (defined $libmagic) {
107 2 50       7 if (eval { require File::LibMagic; require Media::Type::Simple }) {
  2         462  
  0         0  
108 0         0 $libmagic = File::LibMagic->new;
109             } else {
110 2         475 print "err = $@\n";
111 2         13 $libmagic = 0;
112             }
113             }
114 19 50       114 return $default_ext unless $libmagic;
115 0   0     0 my $type = $libmagic->checktype_contents($p->{message} // '');
116 0 0       0 return $default_ext unless $type;
117 0         0 $type =~ s/[; ].*//; # only get the mime type
118 0         0 my $ext = Media::Type::Simple::ext_from_type($type);
119 0 0       0 ($ext) = $ext =~ /(.+)/ if $ext; # untaint
120 0   0     0 return $ext || $default_ext;
121 22         154 };
122              
123 22         63 my $res = $pat;
124 22         152 $res =~ s[%(\{\w+\}|\S)]
125             [defined($vars{$1}) ?
126             ( ref($vars{$1}) eq 'CODE' ?
127 138 100       997 $vars{$1}->($p) : $vars{$1} ) :
    50          
128             die("Invalid filename_pattern `%$1'")]eg;
129 22         330 $res;
130             }
131              
132             sub log_message {
133 25     25 1 2004000 my $self = shift;
134 25         115 my %p = @_;
135              
136             my $filename0 = defined($self->{filename_sub}) ?
137 25 100       167 $self->{filename_sub}->(%p) :
138             $self->_resolve_pattern(\%p);
139              
140 25         91 my $filename = $filename0;
141 25         60 my $i = 0;
142 25         579 while (-e "$self->{dirname}/$filename") {
143 29         75 $i++;
144 29         514 $filename = "$filename0.$i";
145             }
146              
147 25         211 write_text("$self->{dirname}/$filename", $p{message});
148 25 100       3818 $self->_rotate(\%p) if (rand() < $self->{rotate_probability});
149             }
150              
151             sub _rotate {
152 20     20   72 my ($self, $p) = @_;
153              
154 20         52 my $ms = $self->{max_size};
155 20         52 my $mf = $self->{max_files};
156 20         50 my $ma = $self->{max_age};
157              
158 20 100 100     170 return unless (defined($ms) || defined($mf) || defined($ma));
      100        
159              
160 14         35 my @entries;
161 14         45 my $d = $self->{dirname};
162 14         35 my $now = time;
163 14         56 local *DH;
164 14         399 opendir DH, $self->{dirname};
165 14         410 while (my $e = readdir DH) {
166 68         456 ($e) = $e =~ /(.*)/s; # untaint
167 68 100 100     582 next if $e eq '.' || $e eq '..';
168 40         798 my @st = stat "$d/$e";
169 40         524 push @entries, {name => $e, age => ($now-$st[10]), size => $st[7]};
170             }
171 14         135 closedir DH;
172              
173 14         84 @entries = sort {$a->{age} <=> $b->{age}} @entries;
  41         131  
174              
175             # max files
176 14 100 100     81 if (defined($mf) && @entries > $mf) {
177 1         89 unlink "$d/$_->{name}" for (splice @entries, $mf);
178             }
179              
180             # max age
181 14 100       57 if (defined($ma)) {
182 5         17 my $i = 0;
183 5         16 for (@entries) {
184 12 100       44 if ($_->{age} > $ma) {
185 1         283 unlink "$d/$_->{name}" for (splice @entries, $i);
186 1         11 last;
187             }
188 11         28 $i++;
189             }
190             }
191              
192             # max size
193 14 100       106 if (defined($ms)) {
194 5         14 my $i = 0;
195 5         13 my $tot_size = 0;
196 5         17 for (@entries) {
197 15         35 $tot_size += $_->{size};
198 15 100       48 if ($tot_size > $ms) {
199 1         117 unlink "$d/$_->{name}" for (splice @entries, $i);
200 1         15 last;
201             }
202 14         58 $i++;
203             }
204             }
205             }
206              
207             1;
208             # ABSTRACT: Log messages to separate files in a directory, with rotate options
209              
210             __END__
211              
212             =pod
213              
214             =encoding UTF-8
215              
216             =head1 NAME
217              
218             Log::Dispatch::Dir - Log messages to separate files in a directory, with rotate options
219              
220             =head1 VERSION
221              
222             This document describes version 0.15 of Log::Dispatch::Dir (from Perl distribution Log-Dispatch-Dir), released on 2017-07-11.
223              
224             =head1 SYNOPSIS
225              
226             use Log::Dispatch::Dir;
227              
228             my $dir = Log::Dispatch::Dir->new(
229             name => 'dir1',
230             min_level => 'info',
231             dirname => 'somedir.log',
232             filename_pattern => '%Y-%m-%d-%H%M%S.%{ext}',
233             );
234             $dir->log( level => 'info', message => 'your comment\n" );
235              
236             # limit total size
237             my $dir = Log::Dispatch::Dir->new(
238             # ...
239             max_size => 10*1024*1024, # 10MB
240             );
241              
242             # limit number of files
243             my $dir = Log::Dispatch::Dir->new(
244             # ...
245             max_files => 1000,
246             );
247              
248             # limit oldest file
249             my $dir = Log::Dispatch::Dir->new(
250             # ...
251             max_age => 10*24*3600, # 10 days
252             );
253              
254             =head1 DESCRIPTION
255              
256             This module provides a simple object for logging to directories under the
257             Log::Dispatch::* system, and automatically rotating them according to different
258             constraints. Each message will be logged to a separate file the directory.
259              
260             Logging to separate files can be useful for example when dumping whole network
261             responses (like HTTP::Response content).
262              
263             =head1 METHODS
264              
265             =head2 new(%p)
266              
267             This method takes a hash of parameters. The following options are valid:
268              
269             =over 4
270              
271             =item * name ($)
272              
273             The name of the object (not the dirname!). Required.
274              
275             =item * min_level ($)
276              
277             The minimum logging level this object will accept. See the Log::Dispatch
278             documentation on L<Log Levels|Log::Dispatch/"Log Levels"> for more information.
279             Required.
280              
281             =item * max_level ($)
282              
283             The maximum logging level this obejct will accept. See the Log::Dispatch
284             documentation on L<Log Levels|Log::Dispatch/"Log Levels"> for more information.
285             This is not required. By default the maximum is the highest possible level
286             (which means functionally that the object has no maximum).
287              
288             =item * dirname ($)
289              
290             The directory to write to.
291              
292             =item * permissions ($)
293              
294             If the directory does not already exist, the permissions that it should be
295             created with. Optional. The argument passed must be a valid octal value, such as
296             0700 or the constants available from Fcntl, like S_IRUSR|S_IWUSR|S_IXUSR.
297              
298             See L<perlfunc/chmod> for more on potential traps when passing octal values
299             around. Most importantly, remember that if you pass a string that looks like an
300             octal value, like this:
301              
302             my $mode = '0644';
303              
304             Then the resulting directory will end up with permissions like this:
305              
306             --w----r-T
307              
308             which is probably not what you want.
309              
310             =item * callbacks( \& or [ \&, \&, ... ] )
311              
312             This parameter may be a single subroutine reference or an array reference of
313             subroutine references. These callbacks will be called in the order they are
314             given and passed a hash containing the following keys:
315              
316             ( message => $log_message, level => $log_level )
317              
318             The callbacks are expected to modify the message and then return a single scalar
319             containing that modified message. These callbacks will be called when either the
320             C<log> or C<log_to> methods are called and will only be applied to a given
321             message once.
322              
323             =item * filename_pattern ($)
324              
325             Names to give to each file, expressed in pattern a la strftime()'s. Optional.
326             Default is '%Y-%m-%d-%H%M%S.pid-%{pid}.%{ext}'. Time is expressed in local time.
327              
328             If file of the same name already exists, a suffix ".1", ".2", and so on will be
329             appended.
330              
331             Available pattern:
332              
333             =over 8
334              
335             =item %Y - 4-digit year number, e.g. 2009
336              
337             =item %y - 2-digit year number, e.g. 09 for year 2009
338              
339             =item %m - 2-digit month, e.g. 04 for April
340              
341             =item %d - 2-digit day of month, e.g. 28
342              
343             =item %H - 2-digit hour, e.g. 01
344              
345             =item %M - 2-digit minute, e.g. 57
346              
347             =item %S - 2-digit second, e.g. 59
348              
349             =item %z - the time zone as hour offset from GMT
350              
351             =item %Z - the time zone or name or abbreviation
352              
353             =item %{pid} - Process ID
354              
355             =item %{ext} - Guessed file extension
356              
357             Try to detect appropriate file extension using L<File::LibMagic>. For example,
358             if log message looks like an HTML document, then 'html'. If File::LibMagic is
359             not available or type cannot be detected, defaults to 'log'.
360              
361             =item %% - literal '%' character
362              
363             =back
364              
365             =item * filename_sub (\&)
366              
367             A more generic mechanism for B<filename_pattern>. If B<filename_sub> is given,
368             B<filename_pattern> will be ignored. The code will be called with the same
369             arguments as log_message() and is expected to return a filename. Will die if
370             code returns undef.
371              
372             =item * max_size ($)
373              
374             Maximum total size of files, in bytes. After the size is surpassed, oldest files
375             (based on ctime) will be deleted. Optional. Default is undefined, which means
376             unlimited.
377              
378             =item * max_files ($)
379              
380             Maximum number of files. After this number is surpassed, oldest files
381             (based on ctime) will be deleted. Optional. Default is undefined, which means
382             unlimited.
383              
384             =item * max_age ($)
385              
386             Maximum age of files (based on ctime), in seconds. After the age is surpassed,
387             files older than this age will be deleted. Optional. Default is undefined, which
388             means unlimited.
389              
390             =item * rotate_probability ($)
391              
392             A number between 0 and 1 which specifies the probability that rotate()
393             will be called after each log_message(). This is a balance between performance
394             and rotate size accuracy. 1 means always rotate, 0 means never rotate. Optional.
395             Default is 0.25.
396              
397             =back
398              
399             =head2 log_message(message => $)
400              
401             Sends a message to the appropriate output. Generally this shouldn't be called
402             directly but should be called through the C<log()> method (in
403             Log::Dispatch::Output).
404              
405             =head1 HOMEPAGE
406              
407             Please visit the project's homepage at L<https://metacpan.org/release/Log-Dispatch-Dir>.
408              
409             =head1 SOURCE
410              
411             Source repository is at L<https://github.com/perlancar/perl-Log-Dispatch-Dir>.
412              
413             =head1 BUGS
414              
415             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Log-Dispatch-Dir>
416              
417             When submitting a bug or request, please include a test-file or a
418             patch to an existing test-file that illustrates the bug or desired
419             feature.
420              
421             =head1 SEE ALSO
422              
423             L<Log::Dispatch>
424              
425             =head1 AUTHOR
426              
427             perlancar <perlancar@cpan.org>
428              
429             =head1 COPYRIGHT AND LICENSE
430              
431             This software is copyright (c) 2017, 2015, 2014, 2013, 2011 by perlancar@cpan.org.
432              
433             This is free software; you can redistribute it and/or modify it under
434             the same terms as the Perl 5 programming language system itself.
435              
436             =cut