#!/usr/bin/perl # logrotate: rotate log files and signal processes, with optional postprocessing. # Used to cycle weblogs on a monthly basis. # # History # 20000330 cmjg Initial version. # Handles multiple processes, multiple log files. # Can defer postprocessing depending on the number of processes that hold the file open. # At the moment, it just understands the period 'monthly' and expects to # be run close to a changeover (a day or so either side of the month). # Can just sleep a bit after signalling the process, or wait until the files are closed # before postprocessing them. # Intention is to run this from a crontab; it'll output sufficient results # (which ought to get mailed to the owner of the cronjob) # # Warnings: we don't try to stop spuds from rotating /etc/passwd. Caveat emptor. # ------------------------------------------------------------ # Configuration section - after a fashion. # Default values. $midnight = 'yes'; $period = 'monthly'; %kills = (); %logfiles = (); $waitmode = 'sleep'; $waittime1 = 5; $waittime2 = 0; # Postprocessing modes %modes = ( 'none', '', 'gzip', [qw(/usr/bin/gzip -9 %t)], 'echo', [qw(echo Source %s Target %t)] ); # File watching commands. You can invent new ones; they all work in the same fashion. %watch = ( 'fstat', {'threshold' => 10, 'command' => [qw(/usr/bin/fstat %t)]}, #'lsof', {'threshold' => 0, 'command' => [qw(/usr/local/bin/lsof -t %t)]}, 'fuser', {'threshold' => 1, 'command' => [qw(/usr/sbin/fuser -f %t)]}, 'echo', {'threshold' => 1, 'command' => [qw(echo one two three four five)]} ); # ------------------------------------------------------------ # Get the (corrected) local time. sub fixtime() { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime; $year += 1900; my ($feb, @lenmon); # Y2Kness... if ($year % 4 != 0) { $feb = 28; } elsif ($year % 100 != 0) { $feb = 29; } elsif ($year % 400 == 0) { $feb = 29; } else { $feb = 28; } @lenmon = (31, $feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); return ($year, $mon+1, $mday, $hour, $min, $sec, $lenmon[$mon]); } # ------------------------------------------------------------ # The program proper. close (STDIN); ($year, $mon, $mday, $hour, $min, $sec, $lenmon) = fixtime(); print "Starting logrotate at $year $mon $mday, $hour:$min:$sec\n"; # Build a list of signals on this machine %sigs = (); foreach $s (keys %SIG) { $sigs{uc $s} = $s; } # ------------------------------------------------------------ # Read in the config script. The location is held in ARGV[0]... $config = $ARGV[0] || '/usr/local/etc/logrotate.conf'; print " Reading config from $config\n"; if (not -r $config or not open (CONFIG, "<$config")) { print STDERR "Cannot read from $config\n"; exit 1; } while ($line = ) { chomp $line; $line =~ s/#.*$//; $line =~ s/^[ \t]*//; $line =~ s/[ \t]*$//; next if $line eq ""; # midnight (yes|no) # If the program is started before the turnover period, # do we wait for the exact time before sending the signals? if ($line =~ /^midnight\s+yes$/i) { $midnight = 'yes'; next; } if ($line =~ /^midnight\s+no$/i) { $midnight = 'no'; next; } # period (monthly|weekly|...) # At the moment, only monthly periods are supported. if ($line =~ /^period\s+([a-z]+)$/i) { $period = $1; next; } # kill # Send a signal to the process with PID stored in the first line of the pidfile. if ($line =~ /^kill\s+([a-z0-9]+)\s+(.*)$/i) { if (not -r $2) { print " Warning: $2 not readable; skipping\n"; next; } if (exists $sigs{uc $1}) { $kills{$2} = $sigs{uc $1}; } else { print " Warning: $1 is not a known signal\n"; } next; } # logfile # The logfile(s) are rotated using rename, and optionally postprocessed. # can be gzip or none at the moment. if ($line =~ /^logfile\s+([a-z0-9]+)\s+(.*)$/i) { if (not -f $2) { print " Warning: $2 not readable; skipping\n"; next; } if (exists $modes{lc $1}) { $logfiles{$2} = lc $1; } else { print " Warning: $1 is not a known postprocessing mode\n"; } next; } # wait sleep # Don't try to be clever about waiting for httpds to close their logfiles. if ($line =~ /^wait\s+sleep\s+([0-9]+)$/i) { $waitmode = 'sleep'; $waittime1 = $1; next; } # wait # Try to wait until everything writing to a file has closed it. # Available methods: lsof, fuser if ($line =~ /^wait\s+([a-z]+)\s+([0-9]+)\s+([0-9]+)$/i) { if (exists $watch{lc $1}) { $waitmode = lc $1; $waittime1 = $2; $waittime2 = $3; next; } } print STDERR "Warning: line \"$line\" not recognised and ignored\n"; } close (CONFIG); print "Summary:\n"; print " period $period\n"; if ($midnight eq 'yes') { print " waiting for midnight before sending signal\n"; } print " kills ". join(", ", %kills) ."\n"; print " logfiles ". join(", ", %logfiles) ."\n"; print " waitmode $waitmode $waittime1 $waittime2\n"; # ------------------------------------------------------------ # Work out which period we're dealing with. # This gives us the new target name for each logfile; # it also allows us to calculate the time until the rollover period, # if the logrotate process was started before the end of the month, for instance. if ($period eq 'monthly') { # OK, now we work out which month we're in... if ($mday < 14) { # The start of the month; assume we're dealing with logs from last month print "Last month's stats..."; $mon --; if ($mon == 0) { $mon = 12; $year --; } if ($mon < 10) { $mon = "0" . $mon; } $append = ".$year$mon"; $waitfor = 0; } elsif($mday >= $lenmon - 3) { # Fix this! # The end of the month; assume we're dealing with logs from this month print "This month's stats..."; if ($mon < 10) { $mon = "0" . $mon; } $append = ".$year$mon"; $waitfor = ((($lenmon - $mday)*24 + (23 - $hour))*60 + (59 - $min))*60 + (60 - $sec) - 2; } else { # Dunno..? print STDERR "I can't tell if this is the start or end of the month. Bailing out!\n"; exit 3; } print "Log files will have $append appended to them\n"; } else { print STDERR "Period not recognised\n"; exit 4; } # ------------------------------------------------------------ # Rotate the files... print "Rotating logfiles...\n"; $targetfiles = (); foreach $lf (keys %logfiles) { $mf = $lf . $append; $tf = $mf; $n = 0; # if logfile.YYYYMM is gone, try logfile.YYYYMM-1, logfile.YYYYMM-2, etc. while (-e $tf) { $n ++; $tf = $mf . "-" . $n; } print " Renaming $lf to $tf: "; if (rename($lf, $tf) == 1) { print "Success\n"; $targetfiles{$lf} = $tf; } else { print "Failed!\n"; } } # ------------------------------------------------------------ # Wait for midnight (if appropriate) if ($midnight ne 'yes') { $waitfor = 0; } if ($waitfor > 0) { print "Waiting $waitfor seconds for clockover...\n"; while ($waitfor > 3600) { print " Sleeping 1 hour\n"; sleep 3600; $waitfor -= 3600; } print " Sleeping $waitfor seconds\n"; sleep $waitfor; $waitfor = 0; } # ------------------------------------------------------------ # Signal all the processes we need to. ($nyear, $nmon, $nmday, $nhour, $nmin, $nsec) = fixtime(); print "Time is now $nyear $nmon $nmday $nhour:$nmin:$nsec - signalling processes\n"; foreach $pidf (keys %kills) { print " Reading process id from $pidf: "; if (open(PIDFILE, "<$pidf")) { if ($pid = ) { chomp $pid; printf "$pid - sending $kills{$pidf}: "; } else { printf "Cannot read, skipping\n"; next; } close (PIDFILE); } else { printf "Cannot open, skipping\n"; next; } if (kill($kills{$pidf}, $pid) == 1) { print "Done\n"; } else { print "Cannot signal\n"; } } # ------------------------------------------------------------ # Wait (using the chosen method) for the logfiles to be closed. # This can use the naive method or a more cunning approach, using lsof or fuser. print "Waiting for files to be closed, and processing...\n"; if ($waitmode eq "sleep") { if ($waittime1 > 0) { print " Sleeping for $waittime1 seconds\n"; sleep $waittime1; } # Postprocess each logfile. foreach $fn (keys %logfiles) { print " File $fn: "; $mode = $modes{$logfiles{$fn}}; if ($mode eq '') { print "Nothing to do\n"; delete $logfiles{$fn}; next; } print "Postprocessing with $logfiles{$fn}\n"; if (not exists $targetfiles{$fn}) { print " Skipping (no target file)\n"; delete $logfiles{$fn}; next; } $tf = $targetfiles{$fn}; $command = $mode; $command = [map { $s = $_; $s =~ s/%s/$fn/eg; $s } @$command]; $command = [map { $s = $_; $s =~ s/%t/$tf/eg; $s } @$command]; print " Executing ". join(" ", @$command) ."\n"; $res = system @$command; # Beware things that ask for input! print " Result: $res\n"; delete $logfiles{$fn}; } } else { $proggie = $watch{$waitmode}{'command'}; $thresh = $watch{$waitmode}{'threshold'}; print " Using $waitmode to determine file readyness\n"; $waited = 0; while (scalar (keys %logfiles) > 0 && $waited <= $waittime2) { # Each time through, we remove from the hash any files that we've # successfully dealt with or find that we cannot deal with. foreach $fn (keys %logfiles) { print " File $fn: "; $mode = $modes{$logfiles{$fn}}; # The simplest case: no postprocessing is required for this file. if ($mode eq '') { print "Nothing to do\n"; delete $logfiles{$fn}; next; } # If the file was not successfully renamed, we also don't do anything to it. if (not exists $targetfiles{$fn}) { print "Skipping (no target file)\n"; delete $logfiles{$fn}; next; } # The file was renamed, and requires postprocessing. Check to see if it's # still in use. print "Checking file state: "; $opencheck = $proggie; $opencheck = [map { $s = $_; $s =~ s/%s/$fn/eg; $s } @$opencheck]; $opencheck = [map { $s = $_; $s =~ s/%t/$tf/eg; $s } @$opencheck]; print join(" ", @$opencheck); @res = `@$opencheck`; # Count the number of words in the output. $res = 0; map { $res += scalar (split (/\s+/, $_)) } @res; print " = $res: "; # Is it over the threshold? if ($res > $thresh) { print "deferring\n"; next; } else { print "OK to proceed\n"; } # Postprocess this logfile. print " Postprocessing with $logfiles{$fn}\n"; $tf = $targetfiles{$fn}; $command = $mode; $command = [map { $s = $_; $s =~ s/%s/$fn/eg; $s } @$command]; $command = [map { $s = $_; $s =~ s/%t/$tf/eg; $s } @$command]; print " Executing ". join(" ", @$command) ."\n"; $res = system @$command; print " Result: $res\n"; delete $logfiles{$fn}; } next if (scalar (keys %logfiles) == 0); print " Sleeping for $waittime1 seconds\n"; sleep $waittime1; $waited += $waittime1; } # Wrap up this report with any files that failed to close in the given time. if (scalar (keys %logfiles) > 0) { print " Time is up; these logfiles still remained open:\n"; foreach $f (keys %logfiles) { print " $f\n"; } } } # ------------------------------------------------------------ # We're done. ($nyear, $nmon, $nmday, $nhour, $nmin, $nsec, $nlenmon) = fixtime(); print "Finished at $nyear $nmon $nmday $nhour:$nmin:$nsec\n";