#!/usr/bin/perl # # logrotation - periodically process system log files # syntax: logrotation [explist=file] [gzip=file] [gz=ext] # $Id: logrotation,v 3.7 2001/06/09 04:55:38 cos Exp $ # # Reads from explist (defined below) a list of log files to process. # Each entry in the explist file should have four fields, separated # by whitespace. The last field actually contains everything from # the fourth whitespace-separated column to the end of line, so it # may itself contain whitespace. # # The fields are, in order: # - full absolute pathname of the log file # - period in which to process this log file # - method for log file processing # - arguments, depending on method # Only logs in the period matching the command line argument will be # processed. For example, "logrotation monthly" will process only # entries which say "monthly" in the second field. # # There are five methods defined for log file processing: # (there is also a pseudo-method "pidfile" - see below) # # * rotate - compress the current logfile and start a new one. # In this case, the fourth field is a number indicating how many old # logs to keep. For example, a log file rotated once a week with # "rotate 5" will keep the past five weeks, compressed. Old copies # of the log file are numbered, with .1 being the most recent # # * lines - shrink the logile to a specified number of entries. # The last lines of the logfile will be kept, the rest # discarded, with being the fourth field from explist. # # The logfile must be plaintext. By default, each line is a single # newline delimited line of text. If log entries are multiline, but # have a predictable separator, you can specify this separator as a # fifth field, after the number of lines. This fifth field may # contain whitespace, and perl interpolation metacharacters such as # \n. For example, if each log entry is a block of text followed by a # single blank line, put \n\n in the fifth field. # # * pipe - send the contents of this log to an external program. # This does not alter the log file in any way, but opens a pipe to the # program specified in the arguments (fourth field) and sends the # entire text of the logfile to that process's standard input. # # * program - a separate program will perform the processing. # The program name and arguments are taken from the fourth field. # An @ in the fourth field will be replaced by the name of the log # file being processed, and a $ by its directory. If you want an # actual @ or $, use \@ or \$ respectively. # # * rename - rename the file, optionally by date. # The logfile is renamed and compressed. # A new logfile by the same name as the original is started. No files # are deleted, so manual cleanup will eventually be necessary. # # There are two optional arguments, space separated. The first is # a format to use for renaming the file, which may include special # format substrings that will be replaced by the current date. The # second is an optional date offset, in minutes, can be specified in # the argument field. For example, if logrotation is run at 2am and # you'd like the file to be dated as the previous day, put "-60*3" to # "backdate" it by three hours. The default is "@.YYYYMMDD -60". # # In the rename format, the following strings are special: # @ original name of the log file # YYYY 4-digit year # YY 2-digit year # MMM month name, 3-letter form # MM month, numeric form # DD day of month, numeric form # JJJ "Julian" date - day of year # DDD day of week, 3-letter form # HH hour, 24 hour time # :MM minutes, preceded by a colon # :SS seconds, preceded by a colon # # After processing logs using any method, a SIGHUP is sent to syslogd. # Also, the current or new logfile is reset to the same ownership and # permissions that the current or old file had before processing. # # For some logs, there are other programs that may need to be sent # SIGHUP after the log is processed. You can define this using the # "pidfile" pseudo-method. Make an entry, preceding the real log file # entry, and with identical directory, file, and period names. The # method is "pidfile" and the argument is the name of a file that # contains the PID of the process to receive a SIGHUP. You may define # multiple pidfiles for a single log - just make sure they all precede # that log and are identical in the first two columns. # # Some sample explist entries: # /var/adm/sulog monthly lines 20000 # /var/cron/log monthly lines 10000 \n> CMD: # /var/adm/wtmp weekly program rotate_wtmp 180 # /usr/local/man/windex weekly program catman -w -M $ # /usr/spool/mqueue/syslog daily pidfile /etc/sendmail.pid # /usr/spool/mqueue/syslog daily rotate 7 # /web/logs/httpd.log daily pipe webstats mail=ops # /web/logs/httpd.log daily rename @.DDD -30 # # Some sample crontab entries: # 0 0 * * * /usr/local/etc/logrotation daily > /dev/null # 0 0 * * 0 /usr/local/etc/logrotation weekly > /dev/null # 0 0 1 * * /usr/local/etc/logrotation monthly > /dev/null # 0 0 1 1 * /usr/local/etc/logrotation yearly > /dev/null # # If you don't have gzip installed, you can use compress: # logrotation gzip=/usr/ucb/compress gz=.Z # # If you'd like to test things out with a different explist file: # logrotation explist=/path/file # Otherwise, the default explist is /usr/local/etc/logs.expire. # # Bug: There is a small chance that some logging information may be # lost if the system attempts to write it during the brief moment # during which this script is replacing the log file. # # This script is distributed under the terms of Larry Wall's # excellent Artistic License. If you have bug fixes or enhancements, # email cos@leftbank.com. For a copy of the Artistic License, see: # http://language.perl.com/misc/Artistic.html # # (C)opyright 1998 Ofer Inbar require 5; require "timelocal.pl"; @wname = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat","Sun"); @mname = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"); #%mnum = ("Jan","01", "Feb","02", "Mar","03", "Apr","04", "May","05", "Jun","06", # "Jul","07", "Aug","08", "Sep","09", "Oct","10", "Nov","11", "Dec","12"); @pidfiles = ("/etc/syslog.pid"); $explist = "/usr/local/etc/logs.expire"; $gzip = "/usr/local/bin/gzip"; $gz = ".gz"; eval "warn \$die='Unknown switch $1\n' if \$$1 eq ''; \$$1=\$2;" while ($ARGV[0] =~ /^(\w+)=(.*)/ && shift); exit 1 if $die; # process any FOO=bar switches $thisperiod = shift or die "usage: $0 [explist=path] [gzip=path [gz=extension]] period\n"; eval "warn \$die='Unknown switch $1\n' if \$$1 eq ''; \$$1=\$2;" while ($ARGV[0] =~ /^(\w+)=(.*)/ && shift); exit 1 if $die; # process any FOO=bar switches open EXPLIST, "< $explist" or die "can't open $explist: $!\n"; while () { next if /^#/; next if /^\s*$/; my @entry = m#^\s*(/\S+)?/([^/\s]+)\s+(\w+)\s+(\w+)\s+(.+)$# or warn "$0: syntax error in $explist line $.: $_" and next; push @list, \@entry; } close EXPLIST; foreach $entry (@list) { my ($dir,$file,$period,$method,$args) = @$entry; $dir ||= "/"; $method = lc($method); if (lc($period) eq lc($thisperiod)) { chdir $dir or warn "$0: can't chdir to $dir: $!\n(skipping $file)\n" and next; $dir = "" if $dir eq "/"; my ($mod,$uid,$gid) = (stat($file))[2,4,5] or warn "$0: can't stat $dir/$file:$!\n" and next; -f _ or warn "$0: $dir/$file not a plain file, skipping it\n" and next; # -------------------- PIDFILE if ($method eq "pidfile") { push @{$pidfiles{"$dir$file"}}, $args } # -------------------- PROGRAM elsif ($method eq "program") { $args =~ s"([^\\])\$"$1$dir"g; $args =~ s"([^\\])\@"$1$file"g; $args =~ s"\\$"\$"g; $args =~ s"\\@"\@"g; $args =~ s"\\\\"\\"g; $output = `$args`; } # -------------------- PIPE elsif ($method eq "pipe") { my $prog = $args; open PIPE, "| $prog" or warn "$0: can't run $prog: $!\n(skipping $dir/$file)\n" and next; open LOG, "< $file" or warn "$0: can't read $file: $!\n(skipping $dir/$file)\n" and close PIPE and next; $sigpiped = 0; $SIG{'PIPE'} = sub { $sigpiped = 1 }; while (defined($_ = ) and not $sigpiped) {print PIPE} warn "$0: $prog died prematurely, on $dir/$file\n" if $sigpiped; close PIPE or warn "$0: $prog failed, on $dir/$file: $!\n"; close LOG; $SIG{'PIPE'} = 'DEFAULT'; } # -------------------- ROTATE elsif ($method eq "rotate") { for ($ver=$args; $ver > 1; $ver--) { $nxt = $ver-1; if (-f "$file.$nxt$gz" ) { $output .= `mv $file.$nxt$gz $file.$ver$gz` } } $output .= `mv $file $file.1`; $output .= `touch $file`; $output .= sendhups(@pidfiles, @{$pidfiles{"$dir$file"}}); push @zfiles, "$dir/$file.1"; } # -------------------- LINES elsif ($method eq "lines") { local($/) = $/; my($lines,$separator) = $args =~ /^\s*(\d+)\s+(\S.*)/; if ($separator) {$/ = eval "\"$separator\"" } else { $lines = $args } if ($lines < 1) { warn "$0: can't chop to $lines lines! skipping $dir/$file\n"; next } unless (-T _) { warn "$0: $dir/$file not a text file, skipping it\n"; next } open LOG, $file or warn "$0: can't read $dir/$file: $!\n" and next; @lines = ; # while () { push @lines, $_; shift @lines if @lines > $lines } close LOG; next if @lines < $lines; open LOG, "> $file" or warn "$0: can't write $dir/$file: $!\n" and next; print LOG @lines[$#lines-$lines+1 .. $#lines] or warn "$0: can't write $dir/$file: $!\n"; close LOG; $output .= sendhups(@pidfiles, @{$pidfiles{"$dir$file"}}); } # -------------------- RENAME elsif ($method eq "rename") { my ($format,$offset) = $args =~ /^(\S+)\s*(.*)$/; $offset = -60 if $offset =~ /^\s*$/; my $time = time + 60 * eval($offset); my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); $format = "@.YYYYMMDD" if $format =~ /^\s*$/; my $fstring = ""; @fvars = (); while($format) { if ($format=~s/^YYYY//) { $fstring .= "%4d"; push @fvars, $year+1900 } elsif ($format=~s/^YY//) { $fstring .= "%2d"; push @fvars, $year } elsif ($format=~s/^MMM//) { $fstring .= "%s"; push @fvars, $mname[$mon] } elsif ($format=~s/^DDD//) { $fstring .= "%s"; push @fvars, $wname[$wday] } elsif ($format=~s/^JJJ//) { $fstring .= "%03d"; push @fvars, $yday } elsif ($format=~s/^MM//) { $fstring .= "%02d"; push @fvars, $mon+1 } elsif ($format=~s/^DD//) { $fstring .= "%02d"; push @fvars, $mday } elsif ($format=~s/^HH//) { $fstring .= "%02d"; push @fvars, $hour } elsif ($format=~s/^:MM//) { $fstring .= ":%02d"; push @fvars, $min } elsif ($format=~s/^:SS//) { $fstring .= ":%02d"; push @fvars, $sec } elsif ($format=~s/^@//) { $fstring .= "%s"; push @fvars, $file } else { $format =~ s/^(.)/($fstring .= $1), ""/se } } my $newname = sprintf($fstring, @fvars); $output .= `mv $file $newname`; unless (-f $newname) { warn "$0: can't rename $dir/$file to $newname\n"; next } $output .= `touch $file`; $output .= sendhups(@pidfiles, @{$pidfiles{"$dir$file"}}); push @zfiles, "$dir/$newname"; } else { warn "skipping $dir/$file - unknown method: $method\n" } if ((chmod $mod, $file) != 1) { warn "$0: couldn't chmod $file to $mod\n" } if ((chown $uid, $gid, $file) != 1) { warn "$0: couldn't chown $file to $uid, $gid\n" } warn $output unless $output =~ /^\s*$/; $output = ""; } } foreach $file (@zfiles) { print `$gzip $file` } sub sendhups { my (@pidfiles, @pids) = @_; foreach $pidfile (@pidfiles) { open (PIDFILE, $pidfile) or warn "can't open $pidfile: $!\n" and next; chomp ($pid = ); push @pids, $pid; } return unless @pids; sleep 10; # in case something was signaled on previous loop and needs to recover return `kill -HUP @pids`; }