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.
19 #include "apr_strings.h"
20 #include "apr_errno.h"
21 #include "apr_file_io.h"
22 #include "apr_file_info.h"
23 #include "apr_general.h"
25 #include "apr_getopt.h"
26 #include "apr_thread_proc.h"
27 #include "apr_signal.h"
28 #if APR_FILES_AS_SOCKETS
35 #define APR_WANT_STRFUNC
44 #define ROTATE_FORCE 4
46 static const char *const ROTATE_REASONS[] = {
49 "Time interval expired",
50 "Maximum size reached",
55 typedef struct rotate_config rotate_config_t;
57 struct rotate_config {
58 unsigned int sRotation;
69 const char *postrotate_prog;
70 #if APR_FILES_AS_SOCKETS
77 typedef struct rotate_status rotate_status_t;
79 /* "adjusted_time_t" is used to store Unix time (seconds since epoch)
80 * which has been adjusted for some timezone fudge factor. It should
81 * be used for storing the return values from get_now(). A typedef is
82 * used since this type is similar to time_t, but different. */
83 typedef long adjusted_time_t;
85 /* Structure to contain relevant logfile state: fd, pool and
90 char name[APR_PATH_MAX];
93 struct rotate_status {
94 struct logfile current; /* current logfile. */
95 apr_pool_t *pool; /* top-level pool */
97 adjusted_time_t tLogEnd;
102 static rotate_config_t config;
103 static rotate_status_t status;
105 static void usage(const char *argv0, const char *reason)
108 fprintf(stderr, "%s\n", reason);
111 #if APR_FILES_AS_SOCKETS
112 "Usage: %s [-v] [-l] [-L linkname] [-p prog] [-f] [-D] [-t] [-e] [-c] [-n number] <logfile> "
114 "Usage: %s [-v] [-l] [-L linkname] [-p prog] [-f] [-D] [-t] [-e] [-n number] <logfile> "
116 "{<rotation time in seconds>|<rotation size>(B|K|M|G)} "
117 "[offset minutes from UTC]\n\n",
121 "Add this:\n\nTransferLog \"|%s.exe /some/where 86400\"\n\n",
125 "Add this:\n\nTransferLog \"|%s /some/where 86400\"\n\n",
128 "or \n\nTransferLog \"|%s /some/where 5M\"\n\n", argv0);
131 "to httpd.conf. By default, the generated name will be\n"
132 "<logfile>.nnnn where nnnn is the system time at which the log\n"
133 "nominally starts (N.B. if using a rotation time, the time will\n"
134 "always be a multiple of the rotation time, so you can synchronize\n"
135 "cron scripts with it). If <logfile> contains strftime conversion\n"
136 "specifications, those will be used instead. At the end of each\n"
137 "rotation time or when the file size is reached a new log is\n"
141 " -v Verbose operation. Messages are written to stderr.\n"
142 " -l Base rotation on local time instead of UTC.\n"
143 " -L path Create hard link from current log to specified path.\n"
144 " -p prog Run specified program after opening a new log file. See below.\n"
145 " -f Force opening of log on program start.\n"
146 " -D Create parent directories of log file.\n"
147 " -t Truncate logfile instead of rotating, tail friendly.\n"
148 " -e Echo log to stdout for further processing.\n"
149 #if APR_FILES_AS_SOCKETS
150 " -c Create log even if it is empty.\n"
152 " -n num Rotate file by adding suffixes '.0', '.1', ..., '.(num-1)'.\n"
154 "The program for '-p' is invoked as \"[prog] <curfile> [<prevfile>]\"\n"
155 "where <curfile> is the filename of the newly opened logfile, and\n"
156 "<prevfile>, if given, is the filename of the previously used logfile.\n"
161 /* This function returns the current Unix time (time_t) adjusted for
162 * any configured or derived local time offset. The offset applied is
163 * returned via *offset. */
164 static adjusted_time_t get_now(rotate_config_t *config, apr_int32_t *offset)
166 apr_time_t tNow = apr_time_now();
167 apr_int32_t utc_offset;
169 if (config->use_localtime) {
170 /* Check for our UTC offset before using it, since it might
171 * change if there's a switch between standard and daylight
175 apr_time_exp_lt(<, tNow);
176 utc_offset = lt.tm_gmtoff;
179 utc_offset = config->utc_offset;
183 *offset = utc_offset;
185 return apr_time_sec(tNow) + utc_offset;
189 * Close a file and destroy the associated pool.
191 static void close_logfile(rotate_config_t *config, struct logfile *logfile)
193 if (config->verbose) {
194 fprintf(stderr, "Closing file %s\n", logfile->name);
196 apr_file_close(logfile->fd);
197 apr_pool_destroy(logfile->pool);
201 * Dump the configuration parsing result to STDERR.
203 static void dumpConfig (rotate_config_t *config)
205 fprintf(stderr, "Rotation time interval: %12d\n", config->tRotation);
206 fprintf(stderr, "Rotation size interval: %12d\n", config->sRotation);
207 fprintf(stderr, "Rotation time UTC offset: %12d\n", config->utc_offset);
208 fprintf(stderr, "Rotation based on localtime: %12s\n", config->use_localtime ? "yes" : "no");
209 fprintf(stderr, "Rotation file date pattern: %12s\n", config->use_strftime ? "yes" : "no");
210 fprintf(stderr, "Rotation file forced open: %12s\n", config->force_open ? "yes" : "no");
211 fprintf(stderr, "Create parent directories: %12s\n", config->create_path ? "yes" : "no");
212 fprintf(stderr, "Rotation verbose: %12s\n", config->verbose ? "yes" : "no");
213 #if APR_FILES_AS_SOCKETS
214 fprintf(stderr, "Rotation create empty logs: %12s\n", config->create_empty ? "yes" : "no");
216 fprintf(stderr, "Rotation file name: %21s\n", config->szLogRoot);
217 fprintf(stderr, "Post-rotation prog: %21s\n", config->postrotate_prog ? config->postrotate_prog : "not used");
221 * Check whether we need to rotate.
222 * Possible reasons are:
223 * - No log file open (ROTATE_NEW)
224 * - User forces us to rotate (ROTATE_FORCE)
225 * - Our log file size is already bigger than the
226 * allowed maximum (ROTATE_SIZE)
227 * - The next log time interval expired (ROTATE_TIME)
229 * When size and time constraints are both given,
230 * it suffices that one of them is fulfilled.
232 static void checkRotate(rotate_config_t *config, rotate_status_t *status)
234 if (status->current.fd == NULL) {
235 status->rotateReason = ROTATE_NEW;
237 else if (config->sRotation) {
239 apr_off_t current_size = -1;
241 if (apr_file_info_get(&finfo, APR_FINFO_SIZE, status->current.fd) == APR_SUCCESS) {
242 current_size = finfo.size;
245 if (current_size > config->sRotation) {
246 status->rotateReason = ROTATE_SIZE;
248 else if (config->tRotation) {
249 if (get_now(config, NULL) >= status->tLogEnd) {
250 status->rotateReason = ROTATE_TIME;
254 else if (config->tRotation) {
255 if (get_now(config, NULL) >= status->tLogEnd) {
256 status->rotateReason = ROTATE_TIME;
260 fprintf(stderr, "No rotation time or size specified\n");
264 if (status->rotateReason != ROTATE_NONE && config->verbose) {
265 fprintf(stderr, "File rotation needed, reason: %s\n", ROTATE_REASONS[status->rotateReason]);
270 * Handle post-rotate processing.
272 static void post_rotate(apr_pool_t *pool, struct logfile *newlog,
273 rotate_config_t *config, rotate_status_t *status)
276 apr_procattr_t *pattr;
280 /* Handle link file, if configured. */
281 if (config->linkfile) {
282 apr_file_remove(config->linkfile, newlog->pool);
283 if (config->verbose) {
284 fprintf(stderr, "Linking %s to %s\n", newlog->name, config->linkfile);
286 rv = apr_file_link(newlog->name, config->linkfile);
287 if (rv != APR_SUCCESS) {
288 char *error = apr_psprintf(pool, "Error linking file %s to %s (%pm)\n",
289 newlog->name, config->linkfile, &rv);
290 fputs(error, stderr);
295 if (!config->postrotate_prog) {
296 /* Nothing more to do. */
300 /* Collect any zombies from a previous run, but don't wait. */
301 while (apr_proc_wait_all_procs(&proc, NULL, NULL, APR_NOWAIT, pool) == APR_CHILD_DONE)
304 if ((rv = apr_procattr_create(&pattr, pool)) != APR_SUCCESS) {
305 char *error = apr_psprintf(pool, "post_rotate: apr_procattr_create failed " \
306 "for '%s': %pm\n", config->postrotate_prog, &rv);
307 fputs(error, stderr);
311 rv = apr_procattr_error_check_set(pattr, 1);
312 if (rv == APR_SUCCESS)
313 rv = apr_procattr_cmdtype_set(pattr, APR_PROGRAM_ENV);
315 if (rv != APR_SUCCESS) {
316 char *error = apr_psprintf(pool, "post_rotate: could not set up process " \
317 "attributes for '%s': %pm\n", config->postrotate_prog,
319 fputs(error, stderr);
323 argv[0] = config->postrotate_prog;
324 argv[1] = newlog->name;
325 if (status->current.fd) {
326 argv[2] = status->current.name;
334 fprintf(stderr, "Calling post-rotate program: %s\n", argv[0]);
336 rv = apr_proc_create(&proc, argv[0], argv, NULL, pattr, pool);
337 if (rv != APR_SUCCESS) {
338 char *error = apr_psprintf(pool, "Could not spawn post-rotate process " \
339 "'%s': %pm\n", config->postrotate_prog, &rv);
340 fputs(error, stderr);
345 /* After a error, truncate the current file and write out an error
346 * message, which must be contained in message. The process is
347 * terminated on failure. */
348 static void truncate_and_write_error(rotate_status_t *status, const char *message)
350 apr_size_t buflen = strlen(message);
352 if (apr_file_trunc(status->current.fd, 0) != APR_SUCCESS) {
353 fprintf(stderr, "Error truncating the file %s\n", status->current.name);
356 if (apr_file_write_full(status->current.fd, message, buflen, NULL) != APR_SUCCESS) {
357 fprintf(stderr, "Error writing error (%s) to the file %s\n",
358 message, status->current.name);
364 * Open a new log file, and if successful
365 * also close the old one.
367 * The timestamp for the calculation of the file
368 * name of the new log file will be the actual millisecond
369 * timestamp, except when a regular rotation based on a time
370 * interval is configured and the previous interval
371 * is over. Then the timestamp is the starting time
372 * of the actual interval.
374 static void doRotate(rotate_config_t *config, rotate_status_t *status)
377 adjusted_time_t now, tLogStart;
379 struct logfile newlog;
382 /* Retrieve local-time-adjusted-Unix-time. */
383 now = get_now(config, &offset);
385 status->rotateReason = ROTATE_NONE;
387 if (config->tRotation) {
388 adjusted_time_t tLogEnd;
390 tLogStart = (now / config->tRotation) * config->tRotation;
391 tLogEnd = tLogStart + config->tRotation;
393 * Check if rotation was forced and the last rotation
394 * interval is not yet over. Use the value of now instead
395 * of the time interval boundary for the file name then.
397 if (tLogStart < status->tLogEnd) {
400 status->tLogEnd = tLogEnd;
406 if (config->use_strftime) {
407 apr_time_t tNow = apr_time_from_sec(tLogStart);
411 /* Explode the local-time-adjusted-Unix-time into a struct tm,
412 * first *reversing* local-time-adjustment applied by
413 * get_now() if we are using localtime. */
414 if (config->use_localtime)
415 apr_time_exp_lt(&e, tNow - apr_time_from_sec(offset));
417 apr_time_exp_gmt(&e, tNow);
418 apr_strftime(newlog.name, &rs, sizeof(newlog.name), config->szLogRoot, &e);
421 if (config->truncate) {
422 apr_snprintf(newlog.name, sizeof(newlog.name), "%s", config->szLogRoot);
424 else if (config->num_files > 0) {
425 if (status->fileNum == -1 || status->fileNum == (config->num_files - 1)) {
427 apr_snprintf(newlog.name, sizeof(newlog.name), "%s", config->szLogRoot);
430 thisLogNum = status->fileNum + 1;
431 apr_snprintf(newlog.name, sizeof(newlog.name), "%s.%d", config->szLogRoot, thisLogNum);
435 apr_snprintf(newlog.name, sizeof(newlog.name), "%s.%010ld", config->szLogRoot,
439 apr_pool_create(&newlog.pool, status->pool);
440 if (config->create_path) {
441 char *ptr = strrchr(newlog.name, '/');
442 if (ptr && ptr > newlog.name) {
443 char *path = apr_pstrmemdup(newlog.pool, newlog.name, ptr - newlog.name);
444 if (config->verbose) {
445 fprintf(stderr, "Creating directory tree %s\n", path);
447 rv = apr_dir_make_recursive(path, APR_FPROT_OS_DEFAULT, newlog.pool);
448 if (rv != APR_SUCCESS) {
449 char *error = apr_psprintf(newlog.pool,
450 "Could not create directory '%s' (%pm)\n",
452 fputs(error, stderr);
457 if (config->verbose) {
458 fprintf(stderr, "Opening file %s\n", newlog.name);
460 rv = apr_file_open(&newlog.fd, newlog.name, APR_WRITE | APR_CREATE | APR_APPEND
461 | (config->truncate || (config->num_files > 0 && status->current.fd) ? APR_TRUNCATE : 0),
462 APR_OS_DEFAULT, newlog.pool);
463 if (rv == APR_SUCCESS) {
464 /* Handle post-rotate processing. */
465 post_rotate(newlog.pool, &newlog, config, status);
467 status->fileNum = thisLogNum;
468 /* Close out old (previously 'current') logfile, if any. */
469 if (status->current.fd) {
470 close_logfile(config, &status->current);
473 /* New log file is now 'current'. */
474 status->current = newlog;
477 char *error = apr_psprintf(newlog.pool, "%pm", &rv);
480 /* Uh-oh. Failed to open the new log file. Try to clear
481 * the previous log file, note the lost log entries,
482 * and keep on truckin'. */
483 if (status->current.fd == NULL) {
484 fprintf(stderr, "Could not open log file '%s' (%s)\n", newlog.name, error);
488 /* Try to keep this error message constant length
489 * in case it occurs several times. */
490 message = apr_psprintf(newlog.pool,
491 "Resetting log file due to error opening "
492 "new log file, %10d messages lost: %-25.25s\n",
493 status->nMessCount, error);
495 truncate_and_write_error(status, message);
497 /* Throw away new state; it isn't going to be used. */
498 apr_pool_destroy(newlog.pool);
501 status->nMessCount = 0;
505 * Get a size or time param from a string.
506 * Parameter 'last' indicates, whether the
507 * argument is the last commadnline argument.
508 * UTC offset is only allowed as a last argument
509 * in order to make is distinguishable from the
510 * rotation interval time.
512 static const char *get_time_or_size(rotate_config_t *config,
513 const char *arg, int last) {
515 /* Byte multiplier */
516 unsigned int mult = 1;
517 if ((ptr = strchr(arg, 'B')) != NULL) { /* Found KB size */
520 else if ((ptr = strchr(arg, 'K')) != NULL) { /* Found KB size */
523 else if ((ptr = strchr(arg, 'M')) != NULL) { /* Found MB size */
526 else if ((ptr = strchr(arg, 'G')) != NULL) { /* Found GB size */
527 mult = 1024 * 1024 * 1024;
529 if (ptr) { /* rotation based on file size */
530 if (config->sRotation > 0) {
531 return "Rotation size parameter allowed only once";
533 if (*(ptr+1) == '\0') {
534 config->sRotation = atoi(arg) * mult;
536 if (config->sRotation == 0) {
537 return "Invalid rotation size parameter";
540 else if ((config->sRotation > 0 || config->tRotation > 0) && last) {
541 /* rotation based on elapsed time */
542 if (config->use_localtime) {
543 return "UTC offset parameter is not valid with -l";
545 config->utc_offset = atoi(arg) * 60;
547 else { /* rotation based on elapsed time */
548 if (config->tRotation > 0) {
549 return "Rotation time parameter allowed only once";
551 config->tRotation = atoi(arg);
552 if (config->tRotation <= 0) {
553 return "Invalid rotation time parameter";
559 int main (int argc, const char * const argv[])
562 apr_size_t nRead, nWrite;
564 apr_file_t *f_stdout;
569 const char *err = NULL;
570 #if APR_FILES_AS_SOCKETS
571 apr_pollfd_t pollfd = { 0 };
572 apr_status_t pollret = APR_SUCCESS;
576 apr_app_initialize(&argc, &argv, NULL);
577 atexit(apr_terminate);
579 memset(&config, 0, sizeof config);
580 memset(&status, 0, sizeof status);
581 status.rotateReason = ROTATE_NONE;
583 apr_pool_create(&status.pool, NULL);
584 apr_getopt_init(&opt, status.pool, argc, argv);
585 #if APR_FILES_AS_SOCKETS
586 while ((rv = apr_getopt(opt, "lL:p:fDtvecn:", &c, &opt_arg)) == APR_SUCCESS) {
588 while ((rv = apr_getopt(opt, "lL:p:fDtven:", &c, &opt_arg)) == APR_SUCCESS) {
592 config.use_localtime = 1;
595 config.linkfile = opt_arg;
598 config.postrotate_prog = opt_arg;
600 /* Prevent creation of zombies (on modern Unix systems). */
601 apr_signal(SIGCHLD, SIG_IGN);
605 config.force_open = 1;
608 config.create_path = 1;
619 #if APR_FILES_AS_SOCKETS
621 config.create_empty = 1;
625 config.num_files = atoi(opt_arg);
632 usage(argv[0], NULL /* specific error message already issued */ );
636 * After the initial flags we need 2 to 4 arguments,
637 * the file name, either the rotation interval time or size
638 * or both of them, and optionally the UTC offset.
640 if ((argc - opt->ind < 2) || (argc - opt->ind > 4) ) {
641 usage(argv[0], "Incorrect number of arguments");
644 rv = apr_filepath_merge(&config.szLogRoot, "", argv[opt->ind++],
645 APR_FILEPATH_TRUENAME, status.pool);
646 if (rv != APR_SUCCESS && rv != APR_EPATHWILD) {
647 usage(argv[0], "Invalid filename given");
650 /* Read in the remaining flags, namely time, size and UTC offset. */
651 for(; opt->ind < argc; opt->ind++) {
652 if ((err = get_time_or_size(&config, argv[opt->ind],
653 opt->ind < argc - 1 ? 0 : 1)) != NULL) {
658 config.use_strftime = (strchr(config.szLogRoot, '%') != NULL);
660 if (config.use_strftime && config.num_files > 0) {
661 fprintf(stderr, "Cannot use -n with %% in filename\n");
665 if (status.fileNum == -1 && config.num_files < 1) {
666 fprintf(stderr, "Invalid -n argument\n");
670 if (apr_file_open_stdin(&f_stdin, status.pool) != APR_SUCCESS) {
671 fprintf(stderr, "Unable to open stdin\n");
675 if (apr_file_open_stdout(&f_stdout, status.pool) != APR_SUCCESS) {
676 fprintf(stderr, "Unable to open stdout\n");
681 * Write out result of config parsing if verbose is set.
683 if (config.verbose) {
687 #if APR_FILES_AS_SOCKETS
688 if (config.create_empty && config.tRotation) {
689 pollfd.p = status.pool;
690 pollfd.desc_type = APR_POLL_FILE;
691 pollfd.reqevents = APR_POLLIN;
692 pollfd.desc.f = f_stdin;
697 * Immediately open the logfile as we start, if we were forced
700 if (config.force_open) {
701 doRotate(&config, &status);
706 #if APR_FILES_AS_SOCKETS
707 if (config.create_empty && config.tRotation) {
708 polltimeout = status.tLogEnd ? status.tLogEnd - get_now(&config, NULL) : config.tRotation;
709 if (polltimeout <= 0) {
710 pollret = APR_TIMEUP;
713 pollret = apr_poll(&pollfd, 1, &pollret, apr_time_from_sec(polltimeout));
716 if (pollret == APR_SUCCESS) {
717 rv = apr_file_read(f_stdin, buf, &nRead);
718 if (APR_STATUS_IS_EOF(rv)) {
721 else if (rv != APR_SUCCESS) {
725 else if (pollret == APR_TIMEUP) {
730 fprintf(stderr, "Unable to poll stdin\n");
733 #else /* APR_FILES_AS_SOCKETS */
734 rv = apr_file_read(f_stdin, buf, &nRead);
735 if (APR_STATUS_IS_EOF(rv)) {
738 else if (rv != APR_SUCCESS) {
741 #endif /* APR_FILES_AS_SOCKETS */
742 checkRotate(&config, &status);
743 if (status.rotateReason != ROTATE_NONE) {
744 doRotate(&config, &status);
748 rv = apr_file_write_full(status.current.fd, buf, nWrite, &nWrite);
749 if (nWrite != nRead) {
750 apr_off_t cur_offset;
755 if (apr_file_seek(status.current.fd, APR_CUR, &cur_offset) != APR_SUCCESS) {
759 apr_pool_create(&pool, status.pool);
760 error = apr_psprintf(pool, "Error %d writing to log file at offset %"
761 APR_OFF_T_FMT ". %10d messages lost (%pm)\n",
762 rv, cur_offset, status.nMessCount, &rv);
764 truncate_and_write_error(&status, error);
765 apr_pool_destroy(pool);
771 if (apr_file_write_full(f_stdout, buf, nRead, NULL)) {
772 fprintf(stderr, "Unable to write to stdout\n");
778 return 0; /* reached only at stdin EOF. */