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 = '2015-12-16'; # DATE
4             our $VERSION = '0.14'; # VERSION
5              
6 2     2   101997 use 5.010001;
  2         7  
7 2     2   11 use warnings;
  2         3  
  2         60  
8 2     2   10 use strict;
  2         2  
  2         46  
9 2     2   1607 use Log::Dispatch::Output;
  2         31933  
  2         59  
10 2     2   15 use base qw(Log::Dispatch::Output);
  2         3  
  2         139  
11              
12 2     2   843 use File::Slurp::Tiny qw(write_file);
  2         6232  
  2         145  
13             #use File::Stat qw(:stat); # doesn't work in all platforms?
14 2     2   12 use Params::Validate qw(validate SCALAR CODEREF);
  2         3  
  2         101  
15 2     2   1652 use POSIX;
  2         13613  
  2         11  
16              
17             Params::Validate::validation_options( allow_extra => 1 );
18              
19             sub new {
20 8     8 1 14928 my $proto = shift;
21 8   33     55 my $class = ref $proto || $proto;
22              
23 8         48 my %p = @_;
24              
25 8         21 my $self = bless {}, $class;
26              
27 8         64 $self->_basic_init(%p);
28 8         993 $self->_make_handle(%p);
29              
30 8         55 return $self;
31             }
32              
33             sub _make_handle {
34 8     8   17 my $self = shift;
35              
36 8         410 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         94 $self->{dirname} = $p{dirname};
50 8         19 $self->{permissions} = $p{permissions};
51             $self->{filename_pattern} = $p{filename_pattern} ||
52 8   100     50 '%Y-%m-%d-%H%M%S.pid-%{pid}.%{ext}';
53 8         17 $self->{filename_sub} = $p{filename_sub};
54 8         16 $self->{max_size} = $p{max_size};
55 8         17 $self->{max_files} = $p{max_files};
56 8         15 $self->{max_age} = $p{max_age};
57 8   100     38 $self->{rotate_probability} = ($p{rotate_probability}) || 0.25;
58 8         31 $self->_open_dir();
59             }
60              
61             sub _open_dir {
62 8     8   14 my $self = shift;
63              
64 8 100       221 unless (-e $self->{dirname}) {
65 7   100     36 my $perm = $self->{permissions} // 0755;
66 7 50       13273 mkdir($self->{dirname}, $perm)
67             or die "Cannot create directory `$self->{dirname}: $!";
68 7         37 $self->{chmodded} = 1;
69             }
70              
71 8 50       188 unless (-d $self->{dirname}) {
72 0         0 die "$self->{dirname} is not a directory";
73             }
74              
75 8 100 100     82 if ($self->{permissions} && ! $self->{chmodded}) {
76             chmod $self->{permissions}, $self->{dirname}
77 1 50       27 or die "Cannot chmod $self->{dirname} to $self->{permissions}: $!";
78 1         5 $self->{chmodded} = 1;
79             }
80             }
81              
82             my $default_ext = "log";
83             my $libmagic;
84              
85             sub _resolve_pattern {
86 22     22   88 my ($self, $p) = @_;
87 22         52 my $pat = $self->{filename_pattern};
88 22         50 my $now = time;
89              
90 22         96 my @vars = qw(Y y m d H M S z Z %);
91 22         56 my $strftime = POSIX::strftime(join("|", map {"%$_"} @vars),
  220         4893  
92             localtime($now));
93 22         76 my %vars;
94 22         38 my $i = 0;
95 22         118 for (split /\|/, $strftime) {
96 220         497 $vars{ $vars[$i] } = $_;
97 220         398 $i++;
98             }
99              
100 22         61 push @vars, "{pid}";
101 22         74 $vars{"{pid}"} = $$;
102              
103 22         42 push @vars, "{ext}";
104             $vars{"{ext}"} = sub {
105 19     19   39 my $p = shift;
106 19 100       57 unless (defined $libmagic) {
107 2 50       5 if (eval { require File::LibMagic; require Media::Type::Simple }) {
  2         806  
  0         0  
108 0         0 $libmagic = File::LibMagic->new;
109             } else {
110 2         516 print "err = $@\n";
111 2         8 $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         129 };
122              
123 22         54 my $res = $pat;
124 22         136 $res =~ s[%(\{\w+\}|\S)]
125             [defined($vars{$1}) ?
126             ( ref($vars{$1}) eq 'CODE' ?
127 138 100       859 $vars{$1}->($p) : $vars{$1} ) :
    50          
128             die("Invalid filename_pattern `%$1'")]eg;
129 22         283 $res;
130             }
131              
132             sub log_message {
133 25     25 1 2003743 my $self = shift;
134 25         103 my %p = @_;
135              
136             my $filename0 = defined($self->{filename_sub}) ?
137 25 100       162 $self->{filename_sub}->(%p) :
138             $self->_resolve_pattern(\%p);
139              
140 25         65 my $filename = $filename0;
141 25         45 my $i = 0;
142 25         560 while (-e "$self->{dirname}/$filename") {
143 29         45 $i++;
144 29         590 $filename = "$filename0.$i";
145             }
146              
147 25         198 write_file("$self->{dirname}/$filename", $p{message});
148 25 100       4529 $self->_rotate(\%p) if (rand() < $self->{rotate_probability});
149             }
150              
151             sub _rotate {
152 17     17   36 my ($self, $p) = @_;
153              
154 17         31 my $ms = $self->{max_size};
155 17         30 my $mf = $self->{max_files};
156 17         42 my $ma = $self->{max_age};
157              
158 17 100 100     123 return unless (defined($ms) || defined($mf) || defined($ma));
      100        
159              
160 14         19 my @entries;
161 14         27 my $d = $self->{dirname};
162 14         24 my $now = time;
163 14         34 local *DH;
164 14         318 opendir DH, $self->{dirname};
165 14         324 while (my $e = readdir DH) {
166 68         262 ($e) = $e =~ /(.*)/s; # untaint
167 68 100 100     504 next if $e eq '.' || $e eq '..';
168 40         730 my @st = stat "$d/$e";
169 40         359 push @entries, {name => $e, age => ($now-$st[10]), size => $st[7]};
170             }
171 14         108 closedir DH;
172              
173 14         50 @entries = sort {$a->{age} <=> $b->{age}} @entries;
  40         87  
174              
175             # max files
176 14 100 100     56 if (defined($mf) && @entries > $mf) {
177 1         81 unlink "$d/$_->{name}" for (splice @entries, $mf);
178             }
179              
180             # max age
181 14 100       42 if (defined($ma)) {
182 5         8 my $i = 0;
183 5         13 for (@entries) {
184 12 100       32 if ($_->{age} > $ma) {
185 1         293 unlink "$d/$_->{name}" for (splice @entries, $i);
186 1         7 last;
187             }
188 11         23 $i++;
189             }
190             }
191              
192             # max size
193 14 100       89 if (defined($ms)) {
194 5         9 my $i = 0;
195 5         7 my $tot_size = 0;
196 5         12 for (@entries) {
197 15         23 $tot_size += $_->{size};
198 15 100       36 if ($tot_size > $ms) {
199 1         98 unlink "$d/$_->{name}" for (splice @entries, $i);
200 1         11 last;
201             }
202 14         53 $i++;
203             }
204             }
205             }
206              
207             1;
208             # ABSTRACT: Log messages to separate files in a directory, with rotate options
209              
210             __END__