#!/usr/bin/perl # # apachelogrotate.pl - rotate apache logs cleanly # Copyright (C) 2002 - Anthony Tonns # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # --- # # revised ATonns 25Aug1998 # added subject to all mail messages sent out # separated move and gzip to quickly bring the server back up # picked fluff off the script # revised ATonns 28Aug1998 # added sleeptime between stop and start of server # so it can release the http port nicely # revised ATonns 09Sep1998 # revised to use config file # picked lotsa fluff off the script (some needed, some style) # revised ATonns 18Sep1998 # added sleep(60) to wait for midnight, as we _should_ start at 23:59 # completely rebuilt ATonns Sat Mar 31 20:06:03 EST 2001 # no rotates apache not netscape # ##### admin configuration ##### use strict; my $DEBUG = 0; # where is the config file? my $configfile="/usr/local/etc/apachelogrotate.cfg"; # how long do we want to wait between # a) stop and start (seconds) # b) between tries for checking for zero filehandles my $sleeptime=10; # maximum tries for zero filehandles my $maxtries=6; # who do we want to bug? my @victims_email = ( "devnull\@tonns.com" ); my @victims_pagers = ( "devnull\@tonns.com" ); # log files to move my @logtype = ( "access", "errors", "ssl_engine", "ssl_request" ); # date format # old - mmddyy # new - yyyymmdd # defaults to "new" if not specified my $dateformat = "new"; ##### end admin configuration ##### # stuff that shouldn't change, but should not be hardcoded my $progname="apachelogrotate.pl"; # where are we running? my $hostname = `uname -n`; chomp $hostname; # static program locations my $GZIP="/usr/bin/gzip"; my $CAT="/usr/bin/cat"; my $MV="/usr/bin/mv"; my $GREP="/usr/bin/grep"; my $MAIL="/usr/bin/mail"; my $DF="/usr/bin/df"; my $LSOF="/usr/local/bin/lsof"; # what extention do our compressed files have? my $gzip_extention="gz"; # get the date my @dateinfo = localtime(time); my $longyear = $dateinfo[5] + 1900; my $shortyear = $dateinfo[5]; $shortyear = "0$shortyear" if $shortyear < 10; my $month = $dateinfo[4] + 1; $month = "0$month" if $month < 10; my $day = $dateinfo[3]; $day = "0$day" if $day < 10; # set the default format, or use the old one my $filedate = "$longyear$month$day"; if ( $dateformat eq "old" ) { $filedate = "$month$day$shortyear"; } # for email subject status (hope for the best) my $status="success"; # some data structures to do the job # (and record it) my @sitecfg = (); my @realsites = (); my @reportlines = (); # # step 0: wait for midnight # sleep(60) if ! $DEBUG; # # step 1: read the config file # my $linenum = 0; open(CFG,"<$configfile") || &kick_the_bucket("cannot open config file $configfile"); while() { $linenum++; # ignore comments and blank lines next if ( /^$/ || /^#/ ); # setup the config my($instancedir,$logdest,$minfree) = split(/\s+/); my $sitenum = $#sitecfg + 1; $sitecfg[$sitenum]->{"instancedir"} = $instancedir; $sitecfg[$sitenum]->{"logdest"} = $logdest; $sitecfg[$sitenum]->{"minfree"} = $minfree; } close(CFG); # # step 2: sanity checking on user config 'n stuff # # fatal exceptions ( -f $GZIP ) || &kick_the_bucket("gzip($GZIP) not found"); ( -f $CAT ) || &kick_the_bucket("cat($CAT) not found"); ( -f $MV ) || &kick_the_bucket("mv($MV) not found"); ( -f $GREP ) || &kick_the_bucket("grep($GREP) not found"); ( -f $MAIL ) || &kick_the_bucket("mail($MAIL) not found"); ( -f $DF ) || &kick_the_bucket("df($DF) not found"); ( -f $LSOF ) || &kick_the_bucket("lsof($LSOF) not found"); # not so fatal exceptions for (my $sitecfgnum=0; $sitecfgnum <= $#sitecfg; $sitecfgnum++) { my $instancedir = $sitecfg[$sitecfgnum]->{"instancedir"}; if ( ! -d $instancedir ) { &admonish("instance dir: $instancedir not found, skipping"); next; } my $logdest = $sitecfg[$sitecfgnum]->{"logdest"}; if ( ! -d $logdest ) { &admonish("logdest dir: $logdest not found, skipping"); next; } my $realsitenum = $#realsites + 1; $realsites[$realsitenum]->{"instancedir"} = $sitecfg[$sitecfgnum]->{"instancedir"}; $realsites[$realsitenum]->{"logdest"} = $sitecfg[$sitecfgnum]->{"logdest"}; $realsites[$realsitenum]->{"minfree"} = $sitecfg[$sitecfgnum]->{"minfree"}; &report("adding $instancedir -> $logdest to archival operation"); } # # step 3: do the rotation # # first pass, just get the log files out of the way for (my $realsitenum=0; $realsitenum <= $#realsites; $realsitenum++) { my $instancedir = $realsites[$realsitenum]->{"instancedir"}; my $logdest = $realsites[$realsitenum]->{"logdest"}; my $minfree = $realsites[$realsitenum]->{"minfree"}; my $error = 0; $error = &move_logs($instancedir); if ($error) { &admonish("problems moving $instancedir, " . "archival operation in ugly state: $error"); $error=0; } else { &report("successfully moved $instancedir"); } $error = &restart_apache($instancedir); if ($error) { &scream("problems restarting apache in $instancedir, " . "unsure if apache is running: $error"); $error=0; } else { &report("successfully restarted apache in $instancedir"); } } sleep($sleeptime) if ! $DEBUG; # second pass, gzip the log files to "somewhere else" for (my $realsitenum=0; $realsitenum <= $#realsites; $realsitenum++) { my $instancedir = $realsites[$realsitenum]->{"instancedir"}; my $logdest = $realsites[$realsitenum]->{"logdest"}; my $minfree = $realsites[$realsitenum]->{"minfree"}; my $error = 0; $error = &checkfree($logdest,$minfree); if ($error) { &scream("not enough free space on $logdest, " . "skipping gzip: $error"); $error=0; next; } $error = &checkfilehandles($instancedir); if ($error) { &scream("logs still open for $instancedir, " . "skipping gzip: $error"); $error=0; next; } $error = &gzip_logs($instancedir,$logdest); if ($error) { &scream("couldn't gzip $instancedir, " . "archival operation hosed: $error"); $error=0; } else { &report("successfully gzip'ed $instancedir"); } } # # step 4: build report and email it # unshift @reportlines, "-" x 80 . "\n"; unshift @reportlines, "apache log rotation report for $filedate\n"; unshift @reportlines, "-" x 80 . "\n"; push @reportlines, "-" x 80 . "\n"; my $victim; foreach $victim (@victims_email) { open(REPORT, "| $MAIL -s $progname-$hostname-$status $victim"); foreach (@reportlines) { print REPORT $_; } close(REPORT); } ### support subroutines ### sub move_logs { my $instancedir = shift @_; my $log; foreach $log (@logtype) { my $abs_file_src = "$instancedir/logs/$log"; my $abs_file_dst = "$instancedir/logs/$filedate.$log"; # apache restart should create it's own files # # get the perms and mode of the src file # my @src_info = stat($abs_file_src); # my $src_mode = $src_info[2]; # my $src_uid = $src_info[4]; # my $src_gid = $src_info[5]; system("$MV $abs_file_src $abs_file_dst"); if ($?) { &admonish("mv failed for $abs_file_src " . "to $abs_file_dst. exit: $?"); return("mv failed"); } # apache restart should create it's own files # system("$CAT /dev/null > $abs_file_src"); # if ($?) { # &admonish("zero file creation for " . # "$abs_file_src failed. exit: $?"); # return("zero failed"); # } # # set the perms and mode of the src file # chmod($src_mode,$abs_file_src); # chown($src_uid,$src_gid,$abs_file_src); } return 0; } sub restart_apache { my $instancedir = shift @_; my $pidfile = "none"; my $configfile = "$instancedir/conf/httpd.conf"; my $still_looking = 1; if ( ! open(C,"<$configfile") ) { &admonish("cannot open config $configfile"); return("open config failed"); } my $line; while($still_looking and $line = ) { if ( $line =~ /^[^#]*PidFile\s+(\S+)/i ) { $pidfile = $1; $still_looking = 0; } } close(C); if ( $pidfile eq "none" ) { &admonish("could not file PidFile directive in $configfile"); return("find PidFile failed"); } if ( ! open(P,"<$pidfile") ) { &admonish("cannot read pidfile $pidfile"); return("read pidfile failed"); } my $pid =

; chomp($pid); close(P); &report("giving pid $pid signal SIGUSR1 for instance $instancedir"); my $num = kill("SIGUSR1",$pid); if ( $num != 1 ) { &admonish("could not kill pid $pid"); return("could not kill"); } return 0; } sub checkfree { # check freespace my ($logdest,$minfree) = @_; my ($free,$dfstring); chomp ( $dfstring = `$DF -k $logdest | $GREP dev` ); #Filesystem 1024-blocks Used Available Capacity Mounted on #/dev/dsk/c0t3d0s0 81463 26516 54866 33% / ($free) = ($dfstring=~/^\S+\s+\S+\s+\S+\s+(\S+)/); if ($free < $minfree) { &admonish("only $free kb available, $minfree kb required"); return("only $free kb available, $minfree kb required"); } return 0; } sub checkfilehandles { # check filehandles my $instancedir = shift @_; my $log; foreach $log (@logtype) { my $filename = "$instancedir/logs/$filedate.$log"; my $tries = 0; my $count; do { $count = 0; my $cmd = "$LSOF -f -- $filename"; if ( ! open(FH,"$cmd |") ) { &admonish("cannot run command: '$cmd'"); return("lsof failed"); } while () { $count++; } close(FH); $tries++; if ( $count ) { &admonish("attempt $tries: some processes " . "still have file $filename open"); if ( $tries > $maxtries ) { &admonish("maximum of $maxtries attempts made for " . "zero filehandles on file $filename"); return("still have filehandles"); } else { sleep($sleeptime); } } } until ( $count == 0 ); } return 0; } sub gzip_logs { my $instancedir = shift @_; my $logdest = shift @_; my $log; foreach $log (@logtype) { my $abs_file_src = "$instancedir/logs/$filedate.$log"; my $abs_file_dst = "$logdest/$filedate.$log.$gzip_extention"; if ( ! -f $abs_file_src ) { &admonish("could not find $abs_file_src - " . "did the move occur?"); return("no moved logfile"); } system("$GZIP -c < $abs_file_src > $abs_file_dst"); if ($?) { &admonish("gzip failed for $abs_file_src to " . "$abs_file_dst. exit: $?"); return("gzip failed"); } unlink $abs_file_src; if ( -f $abs_file_src ) { &admonish("log removal failed for $abs_file_src."); return("removal failed"); } } return 0; } sub report { my $info = shift @_; push @reportlines, "INFO: $info\n"; print "INFO: $info\n" if $DEBUG; } sub admonish { my $admonishment = shift @_; # escalate the status, if applicable $status = "warnings" unless $status eq "errors"; push @reportlines, "WARNING: $admonishment\n"; print "WARNING: $admonishment\n" if $DEBUG; } sub scream { my $shouting = shift @_; # escalate the status $status = "errors"; my $victim; foreach $victim (@victims_pagers) { open(SCREAM,"| $MAIL -s $progname-$hostname-$status $victim"); print SCREAM "$progname-$hostname\n"; print SCREAM "$shouting\n"; close(SCREAM); } push @reportlines, "FATAL: $shouting\n"; print "FATAL: $shouting\n" if $DEBUG; } sub kick_the_bucket { my $reason = shift @_; &scream($reason); die("$reason: $!\n"); }