]> granicus.if.org Git - apache/blob - support/rotatelogs.c
37f11096767a493250ac01a6906364c33e2eac6f
[apache] / support / rotatelogs.c
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
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 /*
18  * Simple program to rotate Apache logs without having to kill the server.
19  *
20  * Contributed by Ben Laurie <ben algroup.co.uk>
21  *
22  * 12 Mar 1996
23  *
24  * Ported to APR by Mladen Turk <mturk mappingsoft.com>
25  *
26  * 23 Sep 2001
27  *
28  * -l option added 2004-06-11
29  *
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!
33  *
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
36  *    data.
37  *
38  * -v option added Feb, 2008. Verbose output of command line parsing.
39  */
40
41
42 #include "apr.h"
43 #include "apr_lib.h"
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"
49 #include "apr_time.h"
50 #include "apr_getopt.h"
51
52 #if APR_HAVE_STDLIB_H
53 #include <stdlib.h>
54 #endif
55 #define APR_WANT_STRFUNC
56 #include "apr_want.h"
57
58 #define BUFSIZE         65536
59 #define ERRMSGSZ        256
60
61 #ifndef MAX_PATH
62 #define MAX_PATH        1024
63 #endif
64
65 #define ROTATE_NONE     0
66 #define ROTATE_NEW      1
67 #define ROTATE_TIME     2
68 #define ROTATE_SIZE     3
69 #define ROTATE_FORCE    4
70
71 static const char *ROTATE_REASONS[] = {
72     "None",
73     "Open a new file",
74     "Time interval expired",
75     "Maximum size reached",
76     "Forced rotation",
77     NULL
78 };
79
80 typedef struct rotate_config rotate_config_t;
81
82 struct rotate_config {
83     unsigned int sRotation;
84     int tRotation;
85     int utc_offset;
86     int use_localtime;
87     int use_strftime;
88     int force_open;
89     int verbose;
90     const char *szLogRoot;
91     int truncate;
92     const char *linkfile;
93 };
94
95 typedef struct rotate_status rotate_status_t;
96
97 struct rotate_status {
98     apr_pool_t *pool;
99     apr_pool_t *pfile;
100     apr_pool_t *pfile_prev;
101     apr_file_t *nLogFD;
102     apr_file_t *nLogFDprev;
103     char filename[MAX_PATH];
104     char errbuf[ERRMSGSZ];
105     int rotateReason;
106     int tLogEnd;
107     int nMessCount;
108 };
109
110 static rotate_config_t config;
111 static rotate_status_t status;
112
113 static void usage(const char *argv0, const char *reason)
114 {
115     if (reason) {
116         fprintf(stderr, "%s\n", reason);
117     }
118     fprintf(stderr,
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",
122             argv0);
123 #ifdef OS2
124     fprintf(stderr,
125             "Add this:\n\nTransferLog \"|%s.exe /some/where 86400\"\n\n",
126             argv0);
127 #else
128     fprintf(stderr,
129             "Add this:\n\nTransferLog \"|%s /some/where 86400\"\n\n",
130             argv0);
131     fprintf(stderr,
132             "or \n\nTransferLog \"|%s /some/where 5M\"\n\n", argv0);
133 #endif
134     fprintf(stderr,
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");
146     exit(1);
147 }
148
149 /*
150  * Get the unix time with timezone corrections
151  * given in the config struct.
152  */
153 static int get_now(rotate_config_t *config)
154 {
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
160          * savings time.
161          */
162         apr_time_exp_t lt;
163         apr_time_exp_lt(&lt, tNow);
164         utc_offset = lt.tm_gmtoff;
165     }
166     return (int)apr_time_sec(tNow) + utc_offset;
167 }
168
169 /*
170  * Close a file and destroy the associated pool.
171  */
172 static void closeFile(rotate_config_t *config, apr_pool_t *pool, apr_file_t *file)
173 {
174     if (file != NULL) {
175         if (config->verbose) {
176             apr_finfo_t finfo;
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);
182                 } else {
183                     fprintf(stderr, "Closing file %s\n", finfo.fname);
184                 }
185             }
186         }
187         apr_file_close(file);
188         if (pool) {
189             apr_pool_destroy(pool);
190         }
191     }
192 }
193
194 /*
195  * Dump the configuration parsing result to STDERR.
196  */
197 static void dumpConfig (rotate_config_t *config)
198 {
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);
207 }
208
209 /*
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)
217  *
218  * When size and time constraints are both given,
219  * it suffices that one of them is fulfilled.
220  */
221 static void checkRotate(rotate_config_t *config, rotate_status_t *status)
222 {
223
224     if (status->nLogFD == NULL) {
225         status->rotateReason = ROTATE_NEW;
226     }
227     else if (config->sRotation) {
228         apr_finfo_t finfo;
229         apr_off_t current_size = -1;
230
231         if (apr_file_info_get(&finfo, APR_FINFO_SIZE, status->nLogFD) == APR_SUCCESS) {
232             current_size = finfo.size;
233         }
234
235         if (current_size > config->sRotation) {
236             status->rotateReason = ROTATE_SIZE;
237         }
238         else if (config->tRotation) {
239             if (get_now(config) >= status->tLogEnd) {
240                 status->rotateReason = ROTATE_TIME;
241             }
242         }
243     }
244     else if (config->tRotation) {
245         if (get_now(config) >= status->tLogEnd) {
246             status->rotateReason = ROTATE_TIME;
247         }
248     }
249     else {
250         fprintf(stderr, "No rotation time or size specified\n");
251         exit(2);
252     }
253
254     if (status->rotateReason != ROTATE_NONE && config->verbose) {
255         fprintf(stderr, "File rotation needed, reason: %s\n", ROTATE_REASONS[status->rotateReason]);
256     }
257
258     return;
259 }
260
261 /*
262  * Open a new log file, and if successful
263  * also close the old one.
264  *
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.
271  */
272 static void doRotate(rotate_config_t *config, rotate_status_t *status)
273 {
274
275     int now = get_now(config);
276     int tLogStart;
277     apr_status_t rv;
278
279     status->rotateReason = ROTATE_NONE;
280     status->nLogFDprev = status->nLogFD;
281     status->nLogFD = NULL;
282     status->pfile_prev = status->pfile;
283
284     if (config->tRotation) {
285         int tLogEnd;
286         tLogStart = (now / config->tRotation) * config->tRotation;
287         tLogEnd = tLogStart + config->tRotation;
288         /*
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.
292          */
293         if (tLogStart < status->tLogEnd) {
294             tLogStart = now;
295         }
296         status->tLogEnd = tLogEnd;
297     }
298     else {
299         tLogStart = now;
300     }
301
302     if (config->use_strftime) {
303         apr_time_t tNow = apr_time_from_sec(tLogStart);
304         apr_time_exp_t e;
305         apr_size_t rs;
306
307         apr_time_exp_gmt(&e, tNow);
308         apr_strftime(status->filename, &rs, sizeof(status->filename), config->szLogRoot, &e);
309     }
310     else {
311         if (config->truncate) {
312             apr_snprintf(status->filename, sizeof(status->filename), "%s", config->szLogRoot);
313         }
314         else {
315             apr_snprintf(status->filename, sizeof(status->filename), "%s.%010d", config->szLogRoot,
316                     tLogStart);
317         }
318     }
319     apr_pool_create(&status->pfile, status->pool);
320     if (config->verbose) {
321         fprintf(stderr, "Opening file %s\n", status->filename);
322     }
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) {
326         char error[120];
327
328         apr_strerror(rv, error, sizeof error);
329
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);
335             exit(2);
336         }
337         else {
338             apr_size_t nWrite;
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);
352                 exit(2);
353             }
354         }
355     }
356     else {
357         closeFile(config, status->pfile_prev, status->nLogFDprev);
358         status->nLogFDprev = NULL;
359         status->pfile_prev = NULL;
360     }
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);
366         }
367         rv = apr_file_link(status->filename, config->linkfile);
368         if (rv != APR_SUCCESS) {
369             char error[120];
370             apr_strerror(rv, error, sizeof error);
371             fprintf(stderr, "Error linking file %s to %s (%s)\n",
372                     status->filename, config->linkfile, error);
373             exit(2);
374         }
375     }
376 }
377
378 /*
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.
385  */
386 static const char *get_time_or_size(rotate_config_t *config,
387                                     const char *arg, int last) {
388     char *ptr = NULL;
389     /* Byte multiplier */
390     unsigned int mult = 1;
391     if ((ptr = strchr(arg, 'B')) != NULL) { /* Found KB size */
392         mult = 1;
393     }
394     else if ((ptr = strchr(arg, 'K')) != NULL) { /* Found KB size */
395         mult = 1024;
396     }
397     else if ((ptr = strchr(arg, 'M')) != NULL) { /* Found MB size */
398         mult = 1024 * 1024;
399     }
400     else if ((ptr = strchr(arg, 'G')) != NULL) { /* Found GB size */
401         mult = 1024 * 1024 * 1024;
402     }
403     if (ptr) { /* rotation based on file size */
404         if (config->sRotation > 0) {
405             return "Rotation size parameter allowed only once";
406         }
407         if (*(ptr+1) == '\0') {
408             config->sRotation = atoi(arg) * mult;
409         }
410         if (config->sRotation == 0) {
411             return "Invalid rotation size parameter";
412         }
413     }
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";
418         }
419         config->utc_offset = atoi(arg) * 60;
420     }
421     else { /* rotation based on elapsed time */
422         if (config->tRotation > 0) {
423             return "Rotation time parameter allowed only once";
424         }
425         config->tRotation = atoi(arg);
426         if (config->tRotation <= 0) {
427             return "Invalid rotation time parameter";
428         }
429     }
430     return NULL;
431 }
432
433 int main (int argc, const char * const argv[])
434 {
435     char buf[BUFSIZE];
436     apr_size_t nRead, nWrite;
437     apr_file_t *f_stdin;
438     apr_getopt_t *opt;
439     apr_status_t rv;
440     char c;
441     const char *opt_arg;
442     const char *err = NULL;
443
444     apr_app_initialize(&argc, &argv, NULL);
445     atexit(apr_terminate);
446
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;
453     config.verbose = 0;
454     status.pool = NULL;
455     status.pfile = NULL;
456     status.pfile_prev = NULL;
457     status.nLogFD = NULL;
458     status.nLogFDprev = NULL;
459     status.tLogEnd = 0;
460     status.rotateReason = ROTATE_NONE;
461     status.nMessCount = 0;
462
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) {
466         switch (c) {
467         case 'l':
468             config.use_localtime = 1;
469             break;
470         case 'L':
471             config.linkfile = opt_arg;
472             break;
473         case 'f':
474             config.force_open = 1;
475             break;
476         case 't':
477             config.truncate = 1;
478             break;
479         case 'v':
480             config.verbose = 1;
481             break;
482         }
483     }
484
485     if (rv != APR_EOF) {
486         usage(argv[0], NULL /* specific error message already issued */ );
487     }
488
489     /*
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.
493      */
494     if ((argc - opt->ind < 2) || (argc - opt->ind > 4) ) {
495         usage(argv[0], "Incorrect number of arguments");
496     }
497
498     config.szLogRoot = argv[opt->ind++];
499
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) {
504             usage(argv[0], err);
505         }
506     }
507
508     config.use_strftime = (strchr(config.szLogRoot, '%') != NULL);
509
510     if (apr_file_open_stdin(&f_stdin, status.pool) != APR_SUCCESS) {
511         fprintf(stderr, "Unable to open stdin\n");
512         exit(1);
513     }
514
515     /*
516      * Write out result of config parsing if verbose is set.
517      */
518     if (config.verbose) {
519         dumpConfig(&config);
520     }
521
522     /*
523      * Immediately open the logfile as we start, if we were forced
524      * to do so via '-f'.
525      */
526     if (config.force_open) {
527         doRotate(&config, &status);
528     }
529
530     for (;;) {
531         nRead = sizeof(buf);
532         rv = apr_file_read(f_stdin, buf, &nRead);
533         if (rv != APR_SUCCESS) {
534             exit(3);
535         }
536         checkRotate(&config, &status);
537         if (status.rotateReason != ROTATE_NONE) {
538             doRotate(&config, &status);
539         }
540
541         nWrite = nRead;
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
547              */
548             apr_size_t nWritten = nWrite;
549
550             nRead  = nRead - nWritten;
551             nWrite = nRead;
552             rv = apr_file_write(status.nLogFD, buf + nWritten, &nWrite);
553         }
554         if (nWrite != nRead) {
555             char strerrbuf[120];
556             apr_off_t cur_offset;
557
558             cur_offset = 0;
559             if (apr_file_seek(status.nLogFD, APR_CUR, &cur_offset) != APR_SUCCESS) {
560                 cur_offset = -1;
561             }
562             apr_strerror(rv, strerrbuf, sizeof strerrbuf);
563             status.nMessCount++;
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);
572                 exit(2);
573             }
574         }
575         else {
576             status.nMessCount++;
577         }
578     }
579     /* Of course we never, but prevent compiler warnings */
580     return 0;
581 }