File Coverage

blib/lib/Log/Agent/File/Rotate.pm
Criterion Covered Total %
statement 173 204 84.8
branch 67 120 55.8
condition 10 15 66.6
subroutine 23 25 92.0
pod 3 19 15.7
total 276 383 72.0


line stmt bran cond sub pod time code
1             ###########################################################################
2             #
3             # File/Rotate.pm
4             #
5             # Copyright (c) 2000 Raphael Manfredi.
6             # Copyright (c) 2002-2015 Mark Rogaski, mrogaski@cpan.org;
7             # all rights reserved.
8             #
9             # See the README file included with the
10             # distribution for license information.
11             #
12             ###########################################################################
13              
14 6     6   3264 use strict;
  6         12  
  6         227  
15              
16             ###########################################################################
17             package Log::Agent::File::Rotate;
18              
19             #
20             # A rotating logfile set
21             #
22              
23 6     6   3032 use File::stat;
  6         37508  
  6         26  
24 6     6   329 use Fcntl;
  6         10  
  6         1511  
25 6     6   3159 use Symbol;
  6         4301  
  6         351  
26 6     6   3524 use Compress::Zlib;
  6         346635  
  6         1372  
27             require LockFile::Simple;
28              
29 6     6   59 use Log::Agent; # We're using logerr() ourselves when safe to do so
  6         98  
  6         14112  
30              
31             my $DEBUG = 0;
32              
33             #
34             # ->make
35             #
36             # Creation routine.
37             #
38             # Attributes initialized by parameters:
39             # path file path
40             # config rotating configuration (a Log::Agent::Rotate object)
41             #
42             # Other attributes:
43             # fd currently opened file descriptor
44             # handle symbol used for Perl handle
45             # warned records calls made to hardwired warn() to only do them once
46             # written total amount written since opening
47             # size logfile size
48             # opened time when opening occurred
49             # dev device holding logfile
50             # ino inode number of logfile
51             # lockmgr lockfile manager
52             # rotating within the rotate() routine
53             #
54             sub make {
55 12     12 1 46059 my $self = bless {}, shift;
56 12         34 my ($path, $config) = @_;
57 12         48 $self->{'path'} = $path;
58 12         29 $self->{'config'} = $config;
59 12         35 $self->{'fd'} = undef;
60 12         50 $self->{'handle'} = gensym;
61 12         189 $self->{'warned'} = {};
62 12         27 $self->{'rotating'} = 0;
63 12         68 $self->{'lockmgr'} = LockFile::Simple->make(
64             -autoclean => 1,
65             -delay => 1, # until sleep(.25) is supported
66             -efunc => undef,
67             -hold => 60,
68             -max => 5,
69             -nfs => !$config->single_host,
70             -stale => 1,
71             -warn => 0,
72             -wfunc => undef
73             );
74 12         6416 return $self;
75             }
76              
77             #
78             # Attribute access
79             #
80              
81 75     75 0 188 sub path { $_[0]->{'path'} }
82 130     130 1 294 sub config { $_[0]->{'config'} }
83 135     135 0 290 sub fd { $_[0]->{'fd'} }
84 34     34 0 66 sub handle { $_[0]->{'handle'} }
85 34     34 0 98 sub warned { $_[0]->{'warned'} }
86 0     0 0 0 sub written { $_[0]->{'written'} }
87 0     0 0 0 sub opened { $_[0]->{'opened'} }
88 65     65 0 161 sub size { $_[0]->{'size'} }
89 8     8 0 161 sub dev { $_[0]->{'dev'} }
90 8     8 0 71 sub ino { $_[0]->{'ino'} }
91 31     31 0 122 sub lockmgr { $_[0]->{'lockmgr'} }
92 31     31 0 84 sub rotating { $_[0]->{'rotating'} }
93              
94             #
95             # ->print
96             #
97             # Print to file.
98             # This is where all the monitoring is performed:
99             #
100             # . If the file was renamed underneath us, re-open it.
101             # This costs a stat() system call each time a log is to be emitted
102             # and can be avoided by setting config->is_alone.
103             #
104             sub print {
105 65     65 1 17896 my $self = shift;
106 65         191 my $str = join('', @_);
107              
108 65         153 my $fd = $self->fd;
109 65         145 my $cf = $self->config;
110              
111             #
112             # If the file was renamed underneath us, re-open it.
113             # This costs a stat() system call each time a log is to be emitted
114             # and can be avoided by setting config->is_alone when appropriate.
115             #
116              
117 65 100 100     268 if (defined $fd && !$cf->is_alone) {
118 10         23 my $st = stat($self->path);
119 10 50 66     1087 if (!$st || $st->dev != $self->dev || $st->ino != $self->ino) {
      66        
120 2         7 $self->close;
121 2         4 undef $fd; # Will be re-opened below
122             }
123             }
124              
125             #
126             # Open file if not already done.
127             #
128              
129 65 100       160 unless (defined $fd) {
130 34         93 $fd = $self->open;
131 34 50       117 return unless defined $fd;
132             }
133              
134             #
135             # Write to logfile
136             #
137              
138 65 50       1460 return unless syswrite($fd, $str, length $str);
139              
140             #
141             # If the overall logfile size is monitored, update it.
142             # Unless we're alone, we have to fstat() the file descriptor.
143             #
144              
145 65 50       327 if ($cf->max_size) {
146 65 100       161 if ($cf->is_alone) {
147 47         101 $self->{'size'} += length $str;
148             } else {
149 18         38 my $st = stat($fd);
150 18 50       1827 if ($st) {
151 18         250 $self->{'size'} = $st->size; # Paranoid test
152             } else {
153 0         0 $self->{'size'} += length $str;
154             }
155             }
156 65 100       315 if ($self->size > $cf->max_size) {
157 31         103 $self->rotate;
158 31         113 return;
159             }
160             }
161              
162             #
163             # If the amount of bytes written exceeds the threshold,
164             # rotate the files.
165             #
166              
167 34 50       96 if ($cf->max_write) {
168 0         0 $self->{'written'} += length $str;
169 0 0       0 if ($self->written > $cf->max_write) {
170 0         0 $self->rotate;
171 0         0 return;
172             }
173             }
174              
175             #
176             # If the opening time is exceeded, rotate the files.
177             #
178              
179 34 50       88 if ($cf->max_time) {
180 0 0       0 if (time - $self->opened > $cf->max_time) {
181 0         0 $self->rotate;
182 0         0 return;
183             }
184             }
185              
186             # Did not rotate anything
187 34         100 return;
188             }
189              
190             #
191             # ->open
192             #
193             # Open current logfile.
194             # Returns opened handle, or nothing if error.
195             #
196             sub open {
197 34     34 0 56 my $self = shift;
198 34         95 my $fd = $self->handle;
199 34         92 my $path = $self->path;
200 34         58 my $mode = O_CREAT|O_APPEND|O_WRONLY;
201 34         98 my $perm = ($self->config)->file_perm;
202 34 50       79 warn "opening $path\n" if $DEBUG;
203              
204 34 50       2052 unless (sysopen($fd, $path, $mode, $perm)) {
205             #
206             # Can't log errors via Log::Agent since we might recurse down here.
207             # Therefore, use warn(), but only once, and clear condition when
208             # opening is successful.
209             #
210              
211             warn "$0: can't open logfile \"$path\": $!\n"
212 0 0       0 unless $self->warned->{$path}++;
213 0         0 return;
214             }
215              
216 34         232 my $st = stat($fd); # An fstat(), really
217 34         5076 $self->warned->{$path} = 0; # Clear warning condition
218 34         121 $self->{'fd'} = $fd; # Records: file opened
219 34         81 $self->{'written'} = 0; # Amount written
220 34         62 $self->{'opened'} = time; # Opening time
221 34 50       637 $self->{'size'} = $st ? $st->size : 0; # Current size
222 34         604 $self->{'dev'} = $st->dev;
223 34         596 $self->{'ino'} = $st->ino;
224              
225 34         305 return $fd;
226             }
227              
228             #
229             # ->close
230             #
231             # Close current logfile.
232             #
233             sub close {
234 39     39 0 1243 my $self = shift;
235 39         99 my $fd = $self->fd;
236 39 100       114 return unless defined $fd; # Already closed
237 34 50       87 warn "closing logfile\n" if $DEBUG;
238 34         333 close($fd);
239 34         149 $self->{'fd'} = undef; # Mark as closed
240             }
241              
242             #
243             # ->rotate
244             #
245             # Perform logfile rotation, as configured, and log any returned error
246             # to the error channel.
247             #
248             sub rotate {
249 31     31 0 57 my $self = shift;
250 31 50       100 return if $self->rotating; # no recusion if error & limits too small
251 31         60 $self->{'rotating'} = 1;
252              
253 31         94 my @errors = $self->do_rotate;
254 31 50       83 unless (@errors) {
255 31         81 $self->{'rotating'} = 0;
256 31         62 return;
257             }
258              
259             #
260             # Errors are logged using logerr(). There's no danger we could
261             # recurse down here since we're protected by the `rotating' flag.
262             #
263              
264 0 0       0 my $error = @errors == 1 ? "error" : sprintf("%d errors", scalar @errors);
265 0         0 logerr "the following $error occurred while rotating logfiles:";
266 0         0 foreach my $err (@errors) {
267 0         0 logerr $err;
268 0 0       0 warn "ERROR: $err\n" if $DEBUG;
269             }
270              
271 0         0 $self->{'rotating'} = 0;
272             }
273              
274             #
275             # ->do_rotate
276             #
277             # Perform logfile rotation, as configured.
278             # Returns nothing if OK, an array of error messages otherwise.
279             #
280             sub do_rotate {
281 31     31 0 53 my $self = shift;
282 31         69 my $path = $self->path;
283 31         64 my $cf = $self->config;
284 31         75 my $lock = $self->lockmgr->lock($path);
285              
286             #
287             # Emission of errors has to be delayed, since we're in the middle of
288             # logfile rotation, which could be the error channel.
289             #
290              
291 31         10768 my @errors = ();
292              
293 31 50       92 push(@errors, "proceeded with rotation of $path without lock")
294             unless defined $lock;
295              
296             #
297             # We're unix-centric in the following code fragment, but I don't know
298             # how to do the same thing on non-unix operating systems. Sorry.
299             #
300              
301 31         219 my ($dir, $file) = ($path =~ m|^(.*)/(.*)|);
302 31 50       100 ($dir, $file) = (".", $path) unless $dir;
303              
304 31         85 local *DIR;
305 31 50       1255 unless (opendir(DIR, $dir)) {
306 0         0 my $error = "can't open directory \"$dir\" to rotate $path: $!";
307 0 0       0 $lock->release if defined $lock;
308 0         0 return ($error);
309             }
310 31         893 my @files = readdir DIR;
311 31         407 closedir DIR;
312              
313             #
314             # Identify the logfiles already present.
315             #
316             # We use the common convention of renaming un-compressed logfiles
317             # as "path.0", "path.1", etc... the .0 being the more recent file,
318             # and use "path.0.gz", "path.1.gz", etc... for compressed logfiles.
319             #
320              
321 31         91 my @logfiles = (); # Logfiles to rotate
322 31         56 my @unlink = (); # Logfiles to unlink
323 31         97 my $lookfor = "$file.";
324 31         164 my $unlink_at = $cf->backlog - 1;
325              
326 31 50       99 warn "unlink_at=$unlink_at\n" if $DEBUG;
327              
328 31         83 foreach my $f (@files) {
329 420 100       790 next unless substr($f, 0, length $lookfor) eq $lookfor;
330 110         464 my ($idx) = ($f =~ /\.(\d+)(?:\.gz)?$/);
331 110 50       206 warn "f=$f, idx=$idx\n" if $DEBUG;
332 110 100       209 next unless defined $idx;
333 79 50       286 $f = $1 if $f =~ /^(.*)$/; # untaint
334 79 100       179 if ($idx >= $unlink_at) {
335 8         21 push(@unlink, $f);
336             } else {
337 71         136 $logfiles[$idx] = $f;
338             }
339             }
340              
341 31 50       70 if ($DEBUG) {
342 0         0 warn "unlink=@unlink\n";
343 0         0 warn "logfiles=@logfiles\n";
344             }
345              
346             #
347             # Delete old files, if any.
348             #
349              
350 31         64 foreach my $f (@unlink) {
351 8 50       386 unlink("$dir/$f") or push(@errors, "can't unlink $dir/$f: $!");
352             }
353              
354             #
355             # File rotation section...
356             #
357             # If backlog=5 and unzipped=2, then, when things have stabilized,
358             # we have the following logfiles:
359             #
360             # path.4.gz was unlinked above
361             # path.3.gz renamed as path.4.gz
362             # path.2.gz renamed as path.3.gz
363             # path.1 compressed as path.2.gz
364             # path.0 renamed as path.1
365             # path current logfile, closed and renamed path.0
366             #
367             # The code below is prepared to deal with missing files, or policy
368             # changes. Compressed file are not uncompressed though.
369             #
370              
371 31         99 my $last = $cf->backlog - 2; # Oldest logfile already deleted
372 31         89 my $gz_limit = $cf->unzipped; # Files up to that index are .gz
373              
374 31 50       71 warn "last=$last, gz_limit=$gz_limit\n" if $DEBUG;
375              
376             #
377             # Handle renaming of compressed files
378             #
379              
380 31         101 for (my $i = $last; $i >= $gz_limit; $i--) {
381 99 100       246 next unless defined $logfiles[$i]; # Not that much backlog yet?
382 27         72 my $old = "$dir/$logfiles[$i]";
383 27         70 my $new = "$path." . ($i+1) . ".gz";
384 27 50       52 warn "compressing old=$old, new=$new\n" if $DEBUG;
385 27 100       107 if ($old =~ /\.gz$/) {
386 24 50       763 rename($old, $new) or
387             push(@errors, "can't rename $old to $new: $!");
388             } else {
389             # Compression policy changed?
390 3         11 my $err = $self->mv_gzip($old, $new);
391 3 50       145 push(@errors, $err) if defined $err;
392             }
393             }
394              
395             #
396             # Handle compression and renaming of the oldest uncompressed file
397             #
398              
399 31 100 66     219 if ($gz_limit > 0 && defined $logfiles[$gz_limit-1]) {
400 14         74 my $old = "$dir/$logfiles[$gz_limit-1]";
401 14         61 my $new = "$path.$gz_limit.gz";
402 14 50       54 warn "rename and compress old=$old, new=$new\n" if $DEBUG;
403 14 100       59 if ($old !~ /\.gz$/) {
404 13         50 my $err = $self->mv_gzip($old, $new);
405 13 50       756 push(@errors, $err) if defined $err;
406             } else {
407             # Compression policy changed?
408 1 50       36 rename($old, $new) or
409             push(@errors, "can't rename $old to $new: $!");
410             }
411             }
412              
413             #
414             # Handle renaming of uncompressed files
415             #
416              
417 31         126 for (my $i = $gz_limit - 2; $i >= 0; $i--) {
418 39 100       130 next unless defined $logfiles[$i]; # Not that much backlog yet?
419 30         85 my $old = "$dir/$logfiles[$i]";
420 30         96 my $new = "$path." . ($i+1);
421 30 50       84 warn "rename old=$old, new=$new\n" if $DEBUG;
422 30 100       99 $new .= ".gz" if $old =~ /\.gz$/; # Compression policy changed?
423 30 50       1058 rename($old, $new) or
424             push(@errors, "can't rename $old to $new: $!");
425             }
426              
427             #
428             # Mark rotation, in case they "tail -f" on it.
429             #
430              
431 31         131 my $fd = $self->fd;
432 31         1372 syswrite($fd, "*** LOGFILE ROTATED ON " . scalar(localtime) . "\n");
433              
434             #
435             # Finally, close current logfile and rename it.
436             #
437              
438 31         172 $self->close;
439 31 50       89 if ($gz_limit) {
440 31 50       1023 rename($path, "$path.0") or
441             push(@errors, "can't rename $path to $path.0: $!");
442             } else {
443 0         0 my $err = $self->mv_gzip($path, "$path.0.gz");
444 0 0       0 push(@errors, $err) if defined $err;
445             }
446              
447             #
448             # Unlock logfile and propagate errors to be logged in new current file.
449             #
450              
451 31 50       277 $lock->release if defined $lock;
452 31 50       5958 return @errors if @errors;
453 31         362 return;
454             }
455              
456             #
457             # ->mv_gzip
458             #
459             # Compress old file into new file and unlink old file, propagating mtime.
460             # Returns error string, nothing if OK.
461             #
462             sub mv_gzip {
463 16     16 0 32 my $self = shift;
464 16         54 my ($old, $new) = @_;
465              
466 16         47 local *FILE;
467 16         50 my $st = stat($old);
468 16 50 33     2570 unless (defined $st && CORE::open(FILE, $old)) {
469 0         0 return "can't open $old to compress into $new: $!";
470             }
471 16         111 my $gz = gzopen($new, "wb9");
472 16 50       27736 unless (defined $gz) {
473 0         0 CORE::close FILE;
474 0         0 return "can't write into $new: $gzerrno";
475             }
476              
477 16         32 local $_;
478 16         29 my $error;
479 16         308 while () {
480 48 50       3551 unless ($gz->gzwrite($_)) {
481 0         0 $error = "error while compressing $old in $new: $gzerrno";
482 0         0 last;
483             }
484             }
485 16         1410 CORE::close FILE;
486 16         95 $gz->gzclose();
487              
488 16         4520 utime $st->atime, $st->mtime, $new; # don't care if it fails
489 16 50       1411 unlink $old or do { $error = "can't unlink $old: $!" };
  0         0  
490              
491 16 50       69 return $error if defined $error;
492 16         143 return;
493             }
494              
495             1; # for require
496              
497             __END__