#! /usr/local/bin/perl5 # (C) Copyright 1996 Rahul Dhesi, All rights reserved. # Permission for copying and creation of derivative works is granted, # provided this copyright notice is preserved, to anybody who # does not discriminate against the copyright owner. # $Source: /local/undoc/RCS/newsyslog,v $ # $Id: newsyslog,v 1.49 2000/02/09 02:37:08 rdroot Exp $ # # Rolls over various log files. # # Originally written for perl4.036. Now works with perl 5.003. # # Current copy of this program may be obtained from: # # ftp://ftp.rahul.net/pub/dhesi/newsyslog my $BETTER_READ_THIS = <<\EOF; READ THIS DESCRIPTION CAREFULLY. THIS IS NOT A RUN-OF-THE-MILL LOG FILE ROLLOVER PROGRAM. SYNOPSIS In common use, invoke it with one or more directory names specified as arguments. (If none are specified, will use internally hard-coded list in the @DIRLIST variable below). Log files are rolled over in each directory, getting their names from a .list file. ROLLOVER ALGORITHM Each directory should contain a file called .list that lists the names of log files to be rolled over, one per line. The name .list may be overridden with the -l option. In the .list files empty lines and lines beginning with # are ignored. The filenames in the .list file may contain only * and ? as wildcard characters, with the usual meanings (* means zero or more, ? means any one). It will be expanded to find all matching filenames in the same directory, and then from these filenames any suffix of the form will be deleted, and in the resulting filenames duplicates will be suppressed. For example, if a line in the .list file is: xxx.yyy* and suppose this matches the following filenames: xxx.yyy xxx.yyy.0 xxx.yyy.1 xxx.yyy.2 xxx.yyy.zzz xxx.yyy.zzz.0 xxx.yyy.zzz.1 xxx.yyy.zzz.2 Then the list of files to be rolled over, based on this particular line in the .list file, will be: xxx.yyy xxx.yyy.zzz Log file rollover is done as follows: - rename log.9 to log.10, log.8 to log.9, then log.7 to log.8, ..., log.1 to log.2, and finally log to log.0. - the number of generations of files is not limited, so if you have files log.1 through log.50, then log.50 will be renamed to log.51, then log.49 to log.50, and so on. But see the ##max command below. - The final rename of log to log.0 is atomic with respect to the creation of a new zero-length instance of log; this is necessary in case there are processes that open log each time to append to it. This atomicity is achieved as follows: - after renaming log.1 to log.2, we hard link log to log.0. - then we create a new zero-length temp file - then we rename this temp file to log. The ownership and protections of log are preserved, except that world write permissions and all execute permissions are denied. HOW KILL -HUP IS SENT After log file rollover is done in all specified directories, we wait 40 seconds (to allow any pending writes to be completed), then we send a kill -HUP to any processes specified via ##pid lines in the .list file(s). The syntax of these lines is: ##pid For each such line found, we send kill -HUP via the shell command: kill -HUP `cat ` This is done in the order in which the ##pid lines are found in the .list file(s), but the ##pid lines may occur anywhere in the .list file(s), i.e., they may be at the beginning or end or interspersed with the rollover filenames. If a signal other than -HUP is needed, it may be specified as follows: ##pid - For example: ##pid -SIGUSR1 /usr/local/apache/logs/httpd.pid A missing - before the signal name is acceptable: ##pid SIGUSR1 /usr/local/apache/logs/httpd.pid COMMAND EXECUTION Before any kill -HUPs are sent, any commands are executed as specified by ##preexec lines. These are executed in the same way as the ##exec commands described below. After all kill -HUPs have been sent, any commands are executed as specified by ##exec lines found in the .list file(s). The syntax of these lines is: ##exec A trailing backslash may be used as continuation character: ##exec partial command \ continue here and \ and here The ##exec commands are executed in the order in which the ##exec lines occur in the .list file(s), but the ##exec lines may occur anywhere in the .list file(s), i.e., they may be at the beginning or end or interspersed with the rollover filenames. The current directory during command execution is the directory containing the .list file that contained the ##exec line. Command execution is done as whatever userid runs this program, i.e., no attempt is made to change to any other uid. If you invoke this program as root, command execution will be done as root, even if some other userid actually owns the log files. Limited macro substitution is done in ##exec lines before they are executed, the timestamp being that at the time newsyslog begins executing: %YY% four-digit year %MM% two-digit month, 1 .. 12 %DD% two-digit day of month, 1 .. 31 %HH% two-digit hour of day, 00 .. 23 %mm% two-digit minute of hour, 00 .. 59 To suppress macro substitution use \% to represent %. Also, \\ stands for a single backslash. No special meaning is assigned for other uses of backslash. LIMITING NUMBER OF GENERATIONS In the .list file a line like ##max 8 tells the program to limit the number of file generations rolled over to 8. Thus log.7 will be renamed to log.8, any existing log.8 will be lost, and any files with names of the form log.9 and higher will not be touched. USAGE HINT: WEEKLY VS NIGHTLY If you wish to have some files rolled over weekly and others nightly, create two list files, and specify each via -l. Then your cron entries might look like this: 0 5 * * * /local/bin/newsyslog -l nightly.list 15 5 * * 6 /local/bin/newsyslog -l weekly.list EXAMPLE Suppose our .list file contains these lines: #BEGIN .list ##pid /etc/syslog.pid authlog messages ##pid /local/lib/httpd/logs/apache.pid ##exec prune.logs access_log* #END .list And suppose that the wildcard line 'access_log*' matches a large number of files like this: access_log access_log.0 through access_log.9 access_log..org access_log..org.0 through access_log..org.9 access_log..com access_log..com.0 through access_log..com.9 Then the final rollover list will be: access_log access_log..org access_log..com and the actions taken will be: 1. roll over the log files 2. wait 40 seconds 3. do: kill -HUP `cat /etc/syslog.pid` 4. do: kill -HUP `cat /local/lib/httpd/logs/apache.pid' 5. do: /bin/sh -c prune.logs Presumably prune.logs is some script that does some additional pruning of the log files, e.g.: #/bin/sh # prune logs -- keep only generations 1, 2, and 3 /bin/rm -f *.[4-9] EOF my($year, $mon, $mday, $hh, $mm) = ×tamp; # used for macro substitution my $NEWSUFFIX = "new$$"; # temp suffix # this execution path is used for ##exec lines and for finding `pwd` $ENV{'PATH'} = '/local/scripts:/local/bin:/usr/ucb:/usr/bin:/bin:/local/undoc:.'; # default list of directories where log files should be rolled over @DIRLIST = ("/var/adm", "/var/log"); require 'stat.pl'; $opt_x = $opt_t = $opt_l = $trace = $opt_v = undef; $opt_h = $opt_H = undef; $myname = "newsyslog"; $RCSHEADER = '$Source: /local/undoc/RCS/newsyslog,v $' . "\n" . '$Id: newsyslog,v 1.49 2000/02/09 02:37:08 rdroot Exp $'; $usage = "usage: $myname [-vtx] [-l file] [-H] [-f files] [ dir ... ] (or -h for help)"; if ($ARGV[0] =~ "^-.+" ) { require "getopts.pl"; &Getopts("vtxhHf:l:"); } $debug = $opt_x; $trace = $opt_t; $verbose = $debug || $trace || $opt_v; $LISTFILE = $opt_l || '.list'; if ($opt_h) { &givehelp(); exit(0); } # (@ARGV < 1) && &usage_error; if ($opt_f) { for (split(/,/, $opt_f)) { $select{$_} = 1; } } sub usage_error { local($msg) = @_; if ($msg) { die "$msg\n"; } else { die "$usage\n"; } } sub givehelp { ## require 'local/page.pl'; ## &page(< $highest) { $highest = $1; } } } $debug && print "> highest generation is $highest\n"; @logs = (); undef %seen; while () { chomp; # allow continuation lines while (s/\s*\\\s*$/ /) { my($line); $line = ; chomp $line; $line =~ s/^\s+//; $_ .= $line; } s/\s+$//; # trim trailing blanks -- simplifies match(es) below $debug && print ">> $_\n"; if (/^##pid\s+(.+)/) { # if a pid filename my $sig = ''; # default is empty my $file; my $arg = $1; # rest of ##pid line if ($arg =~ /^\S+$/) { $file = $arg; } elsif ($arg =~ /^\-(\S+)\s+(\S+)$/) { $sig = $1; $file = $2; } else { next; # syntax error, ignore this line } # if not absolute path, make it relative to $DIR if ($file !~ m|^/|) { $file = "$DIR/$file"; } # save, either as 'file' or 'signal|file' if ($sig) { push(@pidfiles, "$sig|$file"); } else { push(@pidfiles, $file); } next; } if (/^##exec\s+(.+)\s*$/) { # if an exec filename push(@execfiles, "$DIR|$1"); # save dir name and filename next; } if (/^##preexec\s+(.+)\s*$/) { # if preexec filename push(@preexecfiles, "$DIR|$1"); # save dir name and filename next; } if (/^##max\s+(\d+)\s*$/) { # ##max n $highest = $1 - 1; # -- limit no. of generations next; } # skip comment and empty lines /^#/ && next; /^\s*$/ && next; # if -f was specified, skip file unless explicitly selected if ($opt_f && ! $select{$_}) { next; } if (/[\*\?]/) { # expand shell globbing characters $pat = $_; $debug && print "> pat = [$pat]\n"; $pat =~ s/\*/\255/g; # save occurrences of * $pat =~ s/\?/\254/g; # save occurrences of ? $pat =~ s/(\W)/\\$1/g; # quote metas $pat =~ s/\\\255/\255/g; # unescape \255 $pat =~ s/\\\254/\254/g; # unescape \254 $pat =~ s/\255/.*/g; # restore * as .* $pat =~ s/\254/./g; # restore ? as . ## $debug && print "> pat = [$pat]\n"; # strip any dot and digit(s) suffix in matching filenames, # and cast out duplicates for $entry (@entries) { $entry =~ s/\.\d+(\.gz)?$//; if ($entry =~ /^${pat}$/) { $debug && print ">> match $pat with [$entry]\n"; } else { # $debug && print ">> mismatch $pat with [$entry]\n"; next; } $seen{$entry} && next; $seen{$entry} = 1; push(@logs, $entry); } } else { $seen{$_} && next; $seen{$_} = 1; push(@logs, $_); } } $verbose && print "logs: @logs\n"; for $LOG (@logs) { $NEWLOG = "$LOG.$NEWSUFFIX"; # will be new log file $verbose && print "::::: will rotate $LOG\n"; for $n (reverse (0 .. $highest)) { $next = $n + 1; $oldname = "$LOG.$n"; $newname = "$LOG.$next"; if (-f $oldname) { &rename($oldname, $newname); } elsif (-f "${oldname}.gz") { &rename("${oldname}.gz", "${newname}.gz"); } } # preserve owner, group, and mode for last rotation # sets $st_* variables $st_mode = $st_gid = $st_uid = undef; # suppress warning &Stat($LOG) || warn "$myname: warning: stat failed for $LOG: $!\n"; if (! $trace) { unlink("$LOG.0"); # for safety link($LOG, "$LOG.0"); open(LOG, ">$NEWLOG"); close(LOG); # create zero-length $NEWLOG chown $st_uid, $st_gid, $NEWLOG; # restore owner and group # copy mode, but deny world write and all execute $new_mode = (0777 & $st_mode) & 0664; chmod $new_mode, $NEWLOG; } # now move it to $LOG -- this is the atomic part &rename($NEWLOG, $LOG); } } } # Now process ##preexec files for $item (@preexecfiles) { ($dir, $cmd) = split(/\|/, $item, 2); ($dir && $cmd) || do { warn "$myname: error: can't split [$item] to get dir and cmd\n"; next; }; &run($dir, $cmd); } if (! $trace && @pidfiles > 0) { sleep 40; # Let pending writes complete before sending SIGHUP } # Send -HUP to daemons, unless -H was specified if (! $opt_H) { for $pidfile (@pidfiles) { if ($pidfile =~ /^(.+)\|(.+)$/) { # if "sig|file" $cmd = "kill -$1 `cat $2`"; } else { $cmd = "kill -HUP `cat $pidfile`"; # just "file" } $verbose && print $cmd, "\n"; ! $trace && system $cmd; } } # Now process ##exec files for $item (@execfiles) { ($dir, $cmd) = split(/\|/, $item, 2); ($dir && $cmd) || do { warn "$myname: error: can't split [$item] to get dir and cmd\n"; next; }; &run($dir, $cmd); } # run a command in a specified directory sub run { my($dir, $cmd) = @_; ($dir && $cmd) || return; # do macro substitution $cmd =~ s/\\\%/\255/g; # protect \% as \255 $cmd =~ s/\\\\/\254/g; # protect \\ as \255 $cmd =~ s/%YYYY%/$year/g; $cmd =~ s/%MM%/$mon/g; $cmd =~ s/%DD%/$mday/g; $cmd =~ s/%HH%/$hh/g; $cmd =~ s/%mm%/$mm/g; $cmd =~ s/\255/%/g; # restore original \% as % $cmd =~ s/\254/%/g; # restore original \\ as \ $verbose && print "exec [$cmd] in dir $dir\n"; if (! $trace) { if (chdir($dir)) { # always execute via /bin/sh system('/bin/sh', '-c', $cmd) != 0 && warn "command [$cmd] failed: $!\n"; } else { warn "$myname: error: can't chdir to $dir: $!\n"; } } } # rename, honoring trace/verbose/debug flags sub rename { local($from, $to) = @_; $verbose && print "rename $from $to\n"; if (! $trace) { if (! rename($from, $to)) { $verbose && print STDERR "$myname: error: failed rename $from $to\n"; } } } # get YYYY, MM, DD (local time) sub timestamp { my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); $year = sprintf("%4u", 1900 + $year); $mon = sprintf("%02u", $mon + 1); $mday = sprintf("%02u", $mday); $hour = sprintf("%02u", $hour); $min = sprintf("%02u", $min); return ($year, $mon, $mday, $hour, $min); } # END