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 = '2019-01-09'; # DATE
4             our $VERSION = '0.160'; # VERSION
5              
6 2     2   253018 use 5.010001;
  2         25  
7 2     2   11 use warnings;
  2         4  
  2         51  
8 2     2   10 use strict;
  2         4  
  2         49  
9 2     2   980 use Log::Dispatch::Output;
  2         559253  
  2         83  
10 2     2   18 use base qw(Log::Dispatch::Output);
  2         4  
  2         152  
11              
12 2     2   582 use File::Slurper qw(write_text);
  2         13745  
  2         118  
13             #use File::Stat qw(:stat); # doesn't work in all platforms?
14 2     2   1101 use Params::Validate qw(validate SCALAR CODEREF);
  2         5751  
  2         145  
15 2     2   17 use POSIX;
  2         4  
  2         14  
16              
17             Params::Validate::validation_options( allow_extra => 1 );
18              
19             sub new {
20 8     8 1 14811 my $proto = shift;
21 8   33     43 my $class = ref $proto || $proto;
22              
23 8         42 my %p = @_;
24              
25 8         19 my $self = bless {}, $class;
26              
27 8         56 $self->_basic_init(%p);
28 8         878 $self->_make_handle(%p);
29              
30 8         66 return $self;
31             }
32              
33             sub _make_handle {
34 8     8   14 my $self = shift;
35              
36 8         427 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         84 $self->{dirname} = $p{dirname};
50 8         16 $self->{permissions} = $p{permissions};
51             $self->{filename_pattern} = $p{filename_pattern} ||
52 8   100     37 '%Y-%m-%d-%H%M%S.pid-%{pid}.%{ext}';
53 8         20 $self->{filename_sub} = $p{filename_sub};
54 8         14 $self->{max_size} = $p{max_size};
55 8         12 $self->{max_files} = $p{max_files};
56 8         16 $self->{max_age} = $p{max_age};
57 8   100     28 $self->{rotate_probability} = ($p{rotate_probability}) || 0.25;
58 8         41 $self->_open_dir();
59             }
60              
61             sub _open_dir {
62 8     8   17 my $self = shift;
63              
64 8 100       174 unless (-e $self->{dirname}) {
65 7   100     40 my $perm = $self->{permissions} // 0755;
66 7 50       332 mkdir($self->{dirname}, $perm)
67             or die "Cannot create directory `$self->{dirname}: $!";
68 7         33 $self->{chmodded} = 1;
69             }
70              
71 8 50       119 unless (-d $self->{dirname}) {
72 0         0 die "$self->{dirname} is not a directory";
73             }
74              
75 8 100 100     63 if ($self->{permissions} && ! $self->{chmodded}) {
76             chmod $self->{permissions}, $self->{dirname}
77 1 50       24 or die "Cannot chmod $self->{dirname} to $self->{permissions}: $!";
78 1         7 $self->{chmodded} = 1;
79             }
80             }
81              
82             my $default_ext = "log";
83             my $libmagic;
84              
85             sub _resolve_pattern {
86 22     22   45 my ($self, $p) = @_;
87 22         44 my $pat = $self->{filename_pattern};
88 22         40 my $now = time;
89              
90 22         70 my @vars = qw(Y y m d H M S z Z %);
91 22         50 my $strftime = POSIX::strftime(join("|", map {"%$_"} @vars),
  220         1404  
92             localtime($now));
93 22         93 my %vars;
94 22         35 my $i = 0;
95 22         113 for (split /\|/, $strftime) {
96 220         413 $vars{ $vars[$i] } = $_;
97 220         299 $i++;
98             }
99              
100 22         76 push @vars, "{pid}";
101 22         73 $vars{"{pid}"} = $$;
102              
103 22         40 push @vars, "{ext}";
104             $vars{"{ext}"} = sub {
105 19     19   35 my $p = shift;
106 19 100       51 unless (defined $libmagic) {
107 2 50       5 if (eval { require File::LibMagic; require Media::Type::Simple }) {
  2         386  
  0         0  
108 0         0 $libmagic = File::LibMagic->new;
109             } else {
110 2         89 print "err = $@\n";
111 2         10 $libmagic = 0;
112             }
113             }
114 19 50       89 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         127 };
122              
123 22         45 my $res = $pat;
124 22         132 $res =~ s[%(\{\w+\}|\S)]
125             [defined($vars{$1}) ?
126 138 100       608 ( ref($vars{$1}) eq 'CODE' ?
    50          
127             $vars{$1}->($p) : $vars{$1} ) :
128 22         217 die("Invalid filename_pattern `%$1'")]eg;
129             $res;
130             }
131              
132 25     25 1 2003694 sub log_message {
133 25         80 my $self = shift;
134             my %p = @_;
135              
136 25 100       113 my $filename0 = defined($self->{filename_sub}) ?
137             $self->{filename_sub}->(%p) :
138             $self->_resolve_pattern(\%p);
139 25         60  
140 25         37 my $filename = $filename0;
141 25         478 my $i = 0;
142 29         75 while (-e "$self->{dirname}/$filename") {
143 29         410 $i++;
144             $filename = "$filename0.$i";
145             }
146 25         188  
147 25 100       3393 write_text("$self->{dirname}/$filename", $p{message});
148             $self->_rotate(\%p) if (rand() < $self->{rotate_probability});
149             }
150              
151 15     15   43 sub _rotate {
152             my ($self, $p) = @_;
153 15         28  
154 15         29 my $ms = $self->{max_size};
155 15         27 my $mf = $self->{max_files};
156             my $ma = $self->{max_age};
157 15 100 100     79  
      100        
158             return unless (defined($ms) || defined($mf) || defined($ma));
159 14         23  
160 14         27 my @entries;
161 14         23 my $d = $self->{dirname};
162 14         39 my $now = time;
163 14         319 local *DH;
164 14         288 opendir DH, $self->{dirname};
165 68         299 while (my $e = readdir DH) {
166 68 100 100     364 ($e) = $e =~ /(.*)/s; # untaint
167 40         548 next if $e eq '.' || $e eq '..';
168 40         422 my @st = stat "$d/$e";
169             push @entries, {name => $e, age => ($now-$st[10]), size => $st[7]};
170 14         154 }
171             closedir DH;
172 14         76  
  40         90  
173             @entries = sort {$a->{age} <=> $b->{age}} @entries;
174              
175 14 100 100     49 # max files
176 1         49 if (defined($mf) && @entries > $mf) {
177             unlink "$d/$_->{name}" for (splice @entries, $mf);
178             }
179              
180 14 100       34 # max age
181 5         10 if (defined($ma)) {
182 5         12 my $i = 0;
183 12 100       26 for (@entries) {
184 1         170 if ($_->{age} > $ma) {
185 1         10 unlink "$d/$_->{name}" for (splice @entries, $i);
186             last;
187 11         16 }
188             $i++;
189             }
190             }
191              
192 14 100       77 # max size
193 5         7 if (defined($ms)) {
194 5         9 my $i = 0;
195 5         11 my $tot_size = 0;
196 15         24 for (@entries) {
197 15 100       28 $tot_size += $_->{size};
198 1         59 if ($tot_size > $ms) {
199 1         12 unlink "$d/$_->{name}" for (splice @entries, $i);
200             last;
201 14         39 }
202             $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.160 of Log::Dispatch::Dir (from Perl distribution Log-Dispatch-Dir), released on 2019-01-09.
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) 2019, 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