1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements. See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 * Simple program to rotate Apache logs without having to kill the server.
20 * Contributed by Ben Laurie <ben algroup.co.uk>
24 * Ported to APR by Mladen Turk <mturk mappingsoft.com>
28 * -l option added 2004-06-11
30 * -l causes the use of local time rather than GMT as the base for the
31 * interval. NB: Using -l in an environment which changes the GMT offset
32 * (such as for BST or DST) can lead to unpredictable results!
34 * -f option added Feb, 2008. This causes rotatelog to open/create
35 * the logfile as soon as it's started, not as soon as it sees
38 * -v option added Feb, 2008. Verbose output of command line parsing.
44 #include "apr_strings.h"
45 #include "apr_errno.h"
46 #include "apr_file_io.h"
47 #include "apr_file_info.h"
48 #include "apr_general.h"
50 #include "apr_getopt.h"
55 #define APR_WANT_STRFUNC
69 #define ROTATE_FORCE 4
71 static const char *ROTATE_REASONS[] = {
74 "Time interval expired",
75 "Maximum size reached",
80 typedef struct rotate_config rotate_config_t;
82 struct rotate_config {
83 unsigned int sRotation;
90 const char *szLogRoot;
95 typedef struct rotate_status rotate_status_t;
97 struct rotate_status {
100 apr_pool_t *pfile_prev;
102 apr_file_t *nLogFDprev;
103 char filename[MAX_PATH];
104 char errbuf[ERRMSGSZ];
110 static rotate_config_t config;
111 static rotate_status_t status;
113 static void usage(const char *argv0, const char *reason)
116 fprintf(stderr, "%s\n", reason);
119 "Usage: %s [-v] [-l] [-L linkname] [-f] [-t] <logfile> "
120 "{<rotation time in seconds>|<rotation size>(B|K|M|G)} "
121 "[offset minutes from UTC]\n\n",
125 "Add this:\n\nTransferLog \"|%s.exe /some/where 86400\"\n\n",
129 "Add this:\n\nTransferLog \"|%s /some/where 86400\"\n\n",
132 "or \n\nTransferLog \"|%s /some/where 5M\"\n\n", argv0);
135 "to httpd.conf. The generated name will be /some/where.nnnn "
136 "where nnnn is the\nsystem time at which the log nominally "
137 "starts (N.B. if using a rotation time,\nthe time will always "
138 "be a multiple of the rotation time, so you can synchronize\n"
139 "cron scripts with it). At the end of each rotation time or "
140 "when the file size\nis reached a new log is started. If the "
141 "-t option is specified, the specified\nfile will be truncated "
142 "instead of rotated, and is useful where tail is used to\n"
143 "process logs in real time. If the -L option is specified, "
144 "a hard link will be\nmade from the current log file to the "
145 "specified filename.\n");
150 * Get the unix time with timezone corrections
151 * given in the config struct.
153 static int get_now(rotate_config_t *config)
155 apr_time_t tNow = apr_time_now();
156 int utc_offset = config->utc_offset;
157 if (config->use_localtime) {
158 /* Check for our UTC offset before using it, since it might
159 * change if there's a switch between standard and daylight
163 apr_time_exp_lt(<, tNow);
164 utc_offset = lt.tm_gmtoff;
166 return (int)apr_time_sec(tNow) + utc_offset;
170 * Close a file and destroy the associated pool.
172 static void closeFile(rotate_config_t *config, apr_pool_t *pool, apr_file_t *file)
175 if (config->verbose) {
177 apr_int32_t wanted = APR_FINFO_NAME;
178 apr_status_t rv = apr_file_info_get(&finfo, wanted, file);
179 if ((rv == APR_SUCCESS) || (rv == APR_INCOMPLETE)) {
180 if (finfo.valid & APR_FINFO_NAME) {
181 fprintf(stderr, "Closing file %s (%s)\n", finfo.name, finfo.fname);
183 fprintf(stderr, "Closing file %s\n", finfo.fname);
187 apr_file_close(file);
189 apr_pool_destroy(pool);
195 * Dump the configuration parsing result to STDERR.
197 static void dumpConfig (rotate_config_t *config)
199 fprintf(stderr, "Rotation time interval: %12d\n", config->tRotation);
200 fprintf(stderr, "Rotation size interval: %12d\n", config->sRotation);
201 fprintf(stderr, "Rotation time UTC offset: %12d\n", config->utc_offset);
202 fprintf(stderr, "Rotation based on localtime: %12s\n", config->use_localtime ? "yes" : "no");
203 fprintf(stderr, "Rotation file date pattern: %12s\n", config->use_strftime ? "yes" : "no");
204 fprintf(stderr, "Rotation file forced open: %12s\n", config->force_open ? "yes" : "no");
205 fprintf(stderr, "Rotation verbose: %12s\n", config->verbose ? "yes" : "no");
206 fprintf(stderr, "Rotation file name: %21s\n", config->szLogRoot);
210 * Check whether we need to rotate.
211 * Possible reasons are:
212 * - No log file open (ROTATE_NEW)
213 * - User forces us to rotate (ROTATE_FORCE)
214 * - Our log file size is already bigger than the
215 * allowed maximum (ROTATE_SIZE)
216 * - The next log time interval expired (ROTATE_TIME)
218 * When size and time constraints are both given,
219 * it suffices that one of them is fulfilled.
221 static void checkRotate(rotate_config_t *config, rotate_status_t *status)
224 if (status->nLogFD == NULL) {
225 status->rotateReason = ROTATE_NEW;
227 else if (config->sRotation) {
229 apr_off_t current_size = -1;
231 if (apr_file_info_get(&finfo, APR_FINFO_SIZE, status->nLogFD) == APR_SUCCESS) {
232 current_size = finfo.size;
235 if (current_size > config->sRotation) {
236 status->rotateReason = ROTATE_SIZE;
238 else if (config->tRotation) {
239 if (get_now(config) >= status->tLogEnd) {
240 status->rotateReason = ROTATE_TIME;
244 else if (config->tRotation) {
245 if (get_now(config) >= status->tLogEnd) {
246 status->rotateReason = ROTATE_TIME;
250 fprintf(stderr, "No rotation time or size specified\n");
254 if (status->rotateReason != ROTATE_NONE && config->verbose) {
255 fprintf(stderr, "File rotation needed, reason: %s\n", ROTATE_REASONS[status->rotateReason]);
262 * Open a new log file, and if successful
263 * also close the old one.
265 * The timestamp for the calculation of the file
266 * name of the new log file will be the actual millisecond
267 * timestamp, except when a regular rotation based on a time
268 * interval is configured and the previous interval
269 * is over. Then the timestamp is the starting time
270 * of the actual interval.
272 static void doRotate(rotate_config_t *config, rotate_status_t *status)
275 int now = get_now(config);
279 status->rotateReason = ROTATE_NONE;
280 status->nLogFDprev = status->nLogFD;
281 status->nLogFD = NULL;
282 status->pfile_prev = status->pfile;
284 if (config->tRotation) {
286 tLogStart = (now / config->tRotation) * config->tRotation;
287 tLogEnd = tLogStart + config->tRotation;
289 * Check if rotation was forced and the last rotation
290 * interval is not yet over. Use the value of now instead
291 * of the time interval boundary for the file name then.
293 if (tLogStart < status->tLogEnd) {
296 status->tLogEnd = tLogEnd;
302 if (config->use_strftime) {
303 apr_time_t tNow = apr_time_from_sec(tLogStart);
307 apr_time_exp_gmt(&e, tNow);
308 apr_strftime(status->filename, &rs, sizeof(status->filename), config->szLogRoot, &e);
311 if (config->truncate) {
312 apr_snprintf(status->filename, sizeof(status->filename), "%s", config->szLogRoot);
315 apr_snprintf(status->filename, sizeof(status->filename), "%s.%010d", config->szLogRoot,
319 apr_pool_create(&status->pfile, status->pool);
320 if (config->verbose) {
321 fprintf(stderr, "Opening file %s\n", status->filename);
323 rv = apr_file_open(&status->nLogFD, status->filename, APR_WRITE | APR_CREATE | APR_APPEND
324 | (config->truncate ? APR_TRUNCATE : 0), APR_OS_DEFAULT, status->pfile);
325 if (rv != APR_SUCCESS) {
328 apr_strerror(rv, error, sizeof error);
330 /* Uh-oh. Failed to open the new log file. Try to clear
331 * the previous log file, note the lost log entries,
332 * and keep on truckin'. */
333 if (status->nLogFDprev == NULL) {
334 fprintf(stderr, "Could not open log file '%s' (%s)\n", status->filename, error);
339 status->nLogFD = status->nLogFDprev;
340 apr_pool_destroy(status->pfile);
341 status->pfile = status->pfile_prev;
342 /* Try to keep this error message constant length
343 * in case it occurs several times. */
344 apr_snprintf(status->errbuf, sizeof status->errbuf,
345 "Resetting log file due to error opening "
346 "new log file, %10d messages lost: %-25.25s\n",
347 status->nMessCount, error);
348 nWrite = strlen(status->errbuf);
349 apr_file_trunc(status->nLogFD, 0);
350 if (apr_file_write(status->nLogFD, status->errbuf, &nWrite) != APR_SUCCESS) {
351 fprintf(stderr, "Error writing to the file %s\n", status->filename);
357 closeFile(config, status->pfile_prev, status->nLogFDprev);
358 status->nLogFDprev = NULL;
359 status->pfile_prev = NULL;
361 status->nMessCount = 0;
362 if (config->linkfile) {
363 apr_file_remove(config->linkfile, status->pfile);
364 if (config->verbose) {
365 fprintf(stderr,"Linking %s to %s\n", status->filename, config->linkfile);
367 rv = apr_file_link(status->filename, config->linkfile);
368 if (rv != APR_SUCCESS) {
370 apr_strerror(rv, error, sizeof error);
371 fprintf(stderr, "Error linking file %s to %s (%s)\n",
372 status->filename, config->linkfile, error);
379 * Get a size or time param from a string.
380 * Parameter 'last' indicates, whether the
381 * argument is the last commadnline argument.
382 * UTC offset is only allowed as a last argument
383 * in order to make is distinguishable from the
384 * rotation interval time.
386 static const char *get_time_or_size(rotate_config_t *config,
387 const char *arg, int last) {
389 /* Byte multiplier */
390 unsigned int mult = 1;
391 if ((ptr = strchr(arg, 'B')) != NULL) { /* Found KB size */
394 else if ((ptr = strchr(arg, 'K')) != NULL) { /* Found KB size */
397 else if ((ptr = strchr(arg, 'M')) != NULL) { /* Found MB size */
400 else if ((ptr = strchr(arg, 'G')) != NULL) { /* Found GB size */
401 mult = 1024 * 1024 * 1024;
403 if (ptr) { /* rotation based on file size */
404 if (config->sRotation > 0) {
405 return "Rotation size parameter allowed only once";
407 if (*(ptr+1) == '\0') {
408 config->sRotation = atoi(arg) * mult;
410 if (config->sRotation == 0) {
411 return "Invalid rotation size parameter";
414 else if ((config->sRotation > 0 || config->tRotation > 0) && last) {
415 /* rotation based on elapsed time */
416 if (config->use_localtime) {
417 return "UTC offset parameter is not valid with -l";
419 config->utc_offset = atoi(arg) * 60;
421 else { /* rotation based on elapsed time */
422 if (config->tRotation > 0) {
423 return "Rotation time parameter allowed only once";
425 config->tRotation = atoi(arg);
426 if (config->tRotation <= 0) {
427 return "Invalid rotation time parameter";
433 int main (int argc, const char * const argv[])
436 apr_size_t nRead, nWrite;
442 const char *err = NULL;
444 apr_app_initialize(&argc, &argv, NULL);
445 atexit(apr_terminate);
447 config.sRotation = 0;
448 config.tRotation = 0;
449 config.utc_offset = 0;
450 config.use_localtime = 0;
451 config.use_strftime = 0;
452 config.force_open = 0;
456 status.pfile_prev = NULL;
457 status.nLogFD = NULL;
458 status.nLogFDprev = NULL;
460 status.rotateReason = ROTATE_NONE;
461 status.nMessCount = 0;
463 apr_pool_create(&status.pool, NULL);
464 apr_getopt_init(&opt, status.pool, argc, argv);
465 while ((rv = apr_getopt(opt, "lL:ftv", &c, &opt_arg)) == APR_SUCCESS) {
468 config.use_localtime = 1;
471 config.linkfile = opt_arg;
474 config.force_open = 1;
486 usage(argv[0], NULL /* specific error message already issued */ );
490 * After the initial flags we need 2 to 4 arguments,
491 * the file name, either the rotation interval time or size
492 * or both of them, and optionally the UTC offset.
494 if ((argc - opt->ind < 2) || (argc - opt->ind > 4) ) {
495 usage(argv[0], "Incorrect number of arguments");
498 config.szLogRoot = argv[opt->ind++];
500 /* Read in the remaining flags, namely time, size and UTC offset. */
501 for(; opt->ind < argc; opt->ind++) {
502 if ((err = get_time_or_size(&config, argv[opt->ind],
503 opt->ind < argc - 1 ? 0 : 1)) != NULL) {
508 config.use_strftime = (strchr(config.szLogRoot, '%') != NULL);
510 if (apr_file_open_stdin(&f_stdin, status.pool) != APR_SUCCESS) {
511 fprintf(stderr, "Unable to open stdin\n");
516 * Write out result of config parsing if verbose is set.
518 if (config.verbose) {
523 * Immediately open the logfile as we start, if we were forced
526 if (config.force_open) {
527 doRotate(&config, &status);
532 rv = apr_file_read(f_stdin, buf, &nRead);
533 if (rv != APR_SUCCESS) {
536 checkRotate(&config, &status);
537 if (status.rotateReason != ROTATE_NONE) {
538 doRotate(&config, &status);
542 rv = apr_file_write(status.nLogFD, buf, &nWrite);
543 if (rv == APR_SUCCESS && nWrite != nRead) {
544 /* buffer partially written, which for rotatelogs means we encountered
545 * an error such as out of space or quota or some other limit reached;
546 * try to write the rest so we get the real error code
548 apr_size_t nWritten = nWrite;
550 nRead = nRead - nWritten;
552 rv = apr_file_write(status.nLogFD, buf + nWritten, &nWrite);
554 if (nWrite != nRead) {
556 apr_off_t cur_offset;
559 if (apr_file_seek(status.nLogFD, APR_CUR, &cur_offset) != APR_SUCCESS) {
562 apr_strerror(rv, strerrbuf, sizeof strerrbuf);
564 apr_snprintf(status.errbuf, sizeof status.errbuf,
565 "Error %d writing to log file at offset %" APR_OFF_T_FMT ". "
566 "%10d messages lost (%s)\n",
567 rv, cur_offset, status.nMessCount, strerrbuf);
568 nWrite = strlen(status.errbuf);
569 apr_file_trunc(status.nLogFD, 0);
570 if (apr_file_write(status.nLogFD, status.errbuf, &nWrite) != APR_SUCCESS) {
571 fprintf(stderr, "Error writing to the file %s\n", status.filename);
579 /* Of course we never, but prevent compiler warnings */