httpd-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Andrew Ford <and...@icarus.demon.co.uk>
Subject Re: alternative log rotation program
Date Mon, 09 Dec 1996 13:10:01 GMT
I noticed a problem with the log file rotation program I sent on
Friday.  The problem shows up if there are periods of log file
inactivity -- messages get written to log files named with the wrong
date.  I am enclosing an updated, fixed version of the program.

Is anyone (apart from me) actually interested in this program?

Andrew

/*
 * Simple program to rotate Apache logs without having to kill the server.
 *
 * Contributed by Andrew Ford <andrew@icarus.demon.co.uk> (written 01-Nov-1996)
 * 			      (http://www.nhbs.co.uk/aford/)
 *
 * Based on a program by Ben Laurie <ben@algroup.co.uk>
 *
 * The argument to this program is the log file name template as an
 * strftime format string.  For example to generate new error and
 * access logs each day stored in subdirectories by year and month add
 * the following lines to the httpd.conf:
 *
 *	TransferLog "|/www/etc/strftimelog /www/logs/access/%Y/%m/%Y-%m-%d.log"
 *	ErrorLog    "|/www/etc/strftimelog /www/logs/error/%Y/%m/%Y-%m-%d.log"
 *
 * Note that the program does not create new directories that may be needed, so
 * if as in this example the log file template specifies directories with names that
 * depend on the date, these directories must be created by an external program
 * before they are needed.
 *
 * If compiled with -DDEBUG then the option "-x file" specifies that debugging 
 * messages should be written to "file" (e.g. /dev/console).
 *
 * Compiling with -DTESTHARNESS generates a test program for testing the functions
 * that determine the start of each period -- run with:
 *
 * 	testprog spec count 
 *
 * 
 * Version 1.0 06-Dec-1996 - Andrew Ford <andrew@icarus.demon.co.uk>
 *      Initial version sent to Apache developers' mailing list
 *    
 * Version 1.1 09-Dec-1996 - Andrew Ford <andrew@icarus.demon.co.uk>
 *	Fixed problem with log files being created with the wrong
 *	dates.  (When a new log file is due to be created, the time
 *	used for filling out the template should be the start of the
 *	current period and not the time at which a new log file is
 *	due.  This was observed with daily logs where there were no
 *	transfers for two days; the next log file generated had the
 *	date of the first of the days with no transfers.)
 *
 *	Also added more comments and fixed handling of week numbers
 *	where weeks start on a Monday .  
 */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>


/* Size of read buffer and filename buffer */

#define BUFSIZE			65536
#define MAX_PATH		1024


/* Seconds per minute, hour and day */

#define SECS_PER_MIN		60
#define SECS_PER_HOUR		(60 * SECS_PER_MIN)
#define SECS_PER_DAY		(24 * SECS_PER_HOUR)


/* Allowance for leap seconds (does Unix actually know about them?) */

#define LEAP_SECOND_ALLOWANCE	3


/* If log files are not rotated then this is when the first file
 * should be closed. */

#define FAR_DISTANT_FUTURE	LONG_MAX



/* How often the log is rotated */

typedef enum 
{
    PER_MINUTE, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY, ONCE_ONLY
}
PERIODICITY;

char	*periods[] = 
{
    "minute",
    "hour",
    "day",
    "week",
    "month",
    "year",
    "aeon"
};


static PERIODICITY	determine_periodicity(char *);
static time_t		start_of_next_period(time_t, PERIODICITY);
static time_t		start_of_this_period(time_t, PERIODICITY);


/* America and Europe disagree on whether weeks start on Sunday or
 * Monday - this is set if a %U specifier is encountered. */

int	weeks_start_on_mondays = 0;
    

#ifndef TESTHARNESS
void 
main(int argc,
     char **argv)
{
    PERIODICITY	periodicity;
    char	*logfile_spec;
    time_t	time_now;
    time_t 	start_of_period;
    time_t	next_period;
    char	filename[MAX_PATH];
    char 	read_buf[BUFSIZE];
    int 	n_bytes_read;
    char	ch;
    struct tm 	*tm;
    int 	log_fd = -1;

#ifdef DEBUG
    char	debug_buf[BUFSIZE];
    int		debug_fd = 0;
#endif
    
    while ((ch = getopt(argc, argv, "x:")) != EOF)
    {
	switch (ch)
	{
	case 'x':
#ifdef DEBUG
	    debug_fd = open(optarg, O_WRONLY);
#endif
	    break;
	    
	case '?':
	    fprintf(stderr, "usage: %s [-x file] <logfile-spec>\n\n", argv[0]);
	    exit(1);
	}
    }

    if (optind == argc)
    {
	fprintf(stderr, "usage: %s <logfile-spec>\n\n", argv[0]);
	exit(1);
    }
    
    logfile_spec = argv[optind];
    periodicity  = determine_periodicity(logfile_spec);
#ifdef DEBUG
    if (debug_fd)
    {
	sprintf(debug_buf, "periodicity = %s\n", periods[periodicity]);
	write(debug_fd, debug_buf, strlen(debug_buf));
    }
#endif

    /* Loop, waiting for data on standard input */

    for (;;)
    {
	/* Read a buffer's worth of log file data, exiting on errors
	 * or end of file.
	 */
	n_bytes_read = read(0, read_buf, sizeof read_buf);
	if (n_bytes_read == 0)
	{
	    exit(3);
	}
	if ((n_bytes_read < 0) && (errno != EINTR))
	{
	    exit(4);
	}

	/* If there is a log file open and the current period has
	 * finished, close the log file.
	 */
	time_now = time(NULL);
	if (log_fd >= 0 && (time_now >= next_period))
	{
	    close(log_fd);
	    log_fd = -1;
	}

	/* If there is no log file determine the start of the current
	 * period, generate the log file name from the template,
	 * determine the end of the period and open the new log file.
	 */
	if (log_fd < 0)
	{
	    start_of_period = start_of_this_period(time_now, periodicity);
	    tm = localtime(&start_of_period);
	    strftime(filename, BUFSIZE, logfile_spec, tm);
	    next_period = start_of_next_period(time_now, periodicity);
	
#ifdef DEBUG
	    if (debug_fd)
	    {
		sprintf(debug_buf, "using log file %s\n until %ld (current time is %ld)\n", 
			filename, next_period, time_now);
		write(debug_fd, debug_buf, strlen(debug_buf));
	    }
#endif

	    log_fd = open(filename, O_WRONLY|O_CREAT|O_APPEND, 0666);
	    if (log_fd < 0)
	    {
		perror(filename);
		exit(2);
	    }
	}

	/* Write out the log data to the current log file.
	 */
	if (write(log_fd, read_buf, n_bytes_read) != n_bytes_read)
	{
	    perror(filename);
	    exit(5);
	}
    }

    /* NOTREACHED */
}

#else

/* Test harness for determine_periodicity and start_of_this/next_period
 */
void
main(int argc, char **argv)
{
    PERIODICITY periodicity;
    time_t	now;
    struct tm 	*tm;
    char	*spec;
    int		n;
    int		i;
    char	buf[BUFSIZE];
    
    if (argc != 3)
    {
	fprintf(stderr, "usage: %s spec count\n", argv[0]);
	exit(1);
    }
    
    spec = argv[1];
    n    = atoi(argv[2]);
    periodicity = determine_periodicity(spec);

    now = time(NULL);
    printf("Rotation period is per %s\n", periods[periodicity]);

    tm = localtime(&now);
    strftime(buf, BUFSIZE, "%c", tm);
    printf("start time is %s (%ld)\n", buf, now);
    now = start_of_this_period(time(NULL), periodicity);

    for (i = 1; i <= n; i++)
    {
	tm = localtime(&now);
	strftime(buf, BUFSIZE, "%c", tm);
	printf("period %3d starts at %s (%ld):   ", i, buf, now);
	strftime(buf, BUFSIZE, argv[1], tm);
	printf("\"%s\"\n", buf);
	now = start_of_next_period(now, periodicity);
    }
}
#endif




/* Examine the log file name specifier for strftime conversion
 * specifiers and determine the period between log files. 
 * Smallest period allowed is per minute.
 */
PERIODICITY
determine_periodicity(char *spec)
{
    PERIODICITY	periodicity = ONCE_ONLY;
    char 	ch;
    
    while ((ch = *spec++) != 0)
    {
	if (ch == '%')
	{
	    ch = *spec++;
	    if (!ch) break;
	    
	    switch (ch)
	    {
	    case 'y':		/* two digit year */
	    case 'Y':		/* four digit year */
		if (periodicity > YEARLY)
		{
		    periodicity = YEARLY;
		}
		break;

	    case 'b':		/* abbreviated month name */
	    case 'B':		/* full month name */
	    case 'm':		/* month as two digit number (with
				   leading zero) */
		if (periodicity > MONTHLY)
		{
		    periodicity = MONTHLY;
		}
  	        break;
		
	    case 'U':		/* week number (weeks start on Sunday) */
	    case 'W':		/* week number (weeks start on Monday) */
	        if (periodicity > WEEKLY)
		{
		    periodicity = WEEKLY;
		    if (ch == 'U')
		    {
			weeks_start_on_mondays = 1;
		    }
		}
		break;
		
	    case 'a':		/* abbreviated weekday name */
	    case 'A':		/* full weekday name */
	    case 'd':		/* day of the month (with leading zero) */
	    case 'j':		/* day of the year (with leading zeroes) */
	    case 'w':		/* day of the week (0-6) */
	    case 'x':		/* full date spec */
	        if (periodicity > DAILY)
		{
		    periodicity = DAILY;
		}
  	        break;
	    
	    case 'H':		/* hour (24 hour clock) */
	    case 'I':		/* hour (12 hour clock) */
	    case 'P':		/* AM/PM indicator */
	        if (periodicity > HOURLY)
		{
		    periodicity = HOURLY;
		}
		break;
		
	    case 'M':		/* minute */
	    case 'c':		/* full time and date spec */
	        periodicity = PER_MINUTE;
		break;
		
	    default:		/* ignore anything else */
	        break;
	    }
	}
    }
    return periodicity;
}




/* To determine the time of the start of the next period add just
 * enough to move into the start of the period and then determine the
 * start of the period.
 *
 * There is a potential problem if the start or end of daylight saving
 * time occurs at the start of the period.
 */
static time_t
start_of_next_period(time_t time_now, 
		     PERIODICITY periodicity)
{
    time_t	start_time;
    struct tm	*tm;
    
    switch (periodicity)
    {
    case YEARLY:
	tm = localtime(&time_now);	
	start_time = time_now + (366 - tm->tm_yday) * SECS_PER_DAY + LEAP_SECOND_ALLOWANCE;
	break;

    case MONTHLY:
	tm = localtime(&time_now);	
	start_time = time_now + (32 - tm->tm_mday) * SECS_PER_DAY + LEAP_SECOND_ALLOWANCE;
	break;

    case WEEKLY:
	start_time = time_now + 7 * SECS_PER_DAY + LEAP_SECOND_ALLOWANCE;
	break;
	
    case DAILY:
	start_time = time_now + SECS_PER_DAY + LEAP_SECOND_ALLOWANCE;
	break;
	
    case HOURLY:
	start_time = time_now + SECS_PER_HOUR + LEAP_SECOND_ALLOWANCE;
	break;

    case PER_MINUTE:
	start_time = time_now + SECS_PER_MIN + LEAP_SECOND_ALLOWANCE;
	break;

    default:
	start_time = FAR_DISTANT_FUTURE;
	break;
    }
    return start_of_this_period(start_time, periodicity);
}



/* To determine the time of the start of the period containing a given
 * time just break down the time with localtime and subtract the
 * number of seconds since the start of the period. 
 */
static time_t
start_of_this_period(time_t 		start_time, 
		     PERIODICITY 	periodicity)
{
    struct tm	*tm;
    
    switch (periodicity)
    {
    case YEARLY:
	tm = localtime(&start_time);
	start_time -= (  (SECS_PER_DAY  * tm->tm_yday)
		       + (SECS_PER_HOUR * tm->tm_hour)
		       + (SECS_PER_MIN  * tm->tm_min)
		       + tm->tm_sec);
	break;

    case MONTHLY:
	tm = localtime(&start_time);
	start_time -= (  (SECS_PER_DAY  * (tm->tm_mday - 1))
		       + (SECS_PER_HOUR * tm->tm_hour)
		       + (SECS_PER_MIN  * tm->tm_min)
		       + tm->tm_sec);
	break;

    case WEEKLY:
	tm = localtime(&start_time);
	if (weeks_start_on_mondays)
	{
	    tm->tm_wday = (6 + tm->tm_wday) % 7;
	}
	start_time -= (  (SECS_PER_DAY  * tm->tm_wday)
		       + (SECS_PER_HOUR * tm->tm_hour)
		       + (SECS_PER_MIN  * tm->tm_min)
		       + tm->tm_sec);
	break;
	
    case DAILY:
	tm = localtime(&start_time);
	start_time -= (  (SECS_PER_HOUR * tm->tm_hour)
		       + (SECS_PER_MIN  * tm->tm_min)
		       + tm->tm_sec);
	break;
	
    case HOURLY:
	tm = localtime(&start_time);
	start_time -= (SECS_PER_MIN * tm->tm_min) + tm->tm_sec;
	break;

    case PER_MINUTE:
	tm = localtime(&start_time);
	start_time -= tm->tm_sec;
	break;

    default:
	break;
    }
    return start_time;
}

Mime
View raw message