]> granicus.if.org Git - apache/blob - support/rotatelogs.c
2cf2549e27922a92b7191d44e0e40ba3c04c97f4
[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 };
92
93 typedef struct rotate_status rotate_status_t;
94
95 struct rotate_status {
96     apr_pool_t *pool;
97     apr_pool_t *pfile;
98     apr_pool_t *pfile_prev;
99     apr_file_t *nLogFD;
100     apr_file_t *nLogFDprev;
101     char filename[MAX_PATH];
102     char errbuf[ERRMSGSZ];
103     int rotateReason;
104     int tLogEnd;
105     int nMessCount;
106 };
107
108 static rotate_config_t config;
109 static rotate_status_t status;
110
111 static void usage(const char *argv0, const char *reason)
112 {
113     if (reason) {
114         fprintf(stderr, "%s\n", reason);
115     }
116     fprintf(stderr,
117             "Usage: %s [-v] [-l] [-f] <logfile> "
118             "{<rotation time in seconds>|<rotation size>(B|K|M|G)} "
119             "[offset minutes from UTC]\n\n",
120             argv0);
121 #ifdef OS2
122     fprintf(stderr,
123             "Add this:\n\nTransferLog \"|%s.exe /some/where 86400\"\n\n",
124             argv0);
125 #else
126     fprintf(stderr,
127             "Add this:\n\nTransferLog \"|%s /some/where 86400\"\n\n",
128             argv0);
129     fprintf(stderr,
130             "or \n\nTransferLog \"|%s /some/where 5M\"\n\n", argv0);
131 #endif
132     fprintf(stderr,
133             "to httpd.conf. The generated name will be /some/where.nnnn "
134             "where nnnn is the\nsystem time at which the log nominally "
135             "starts (N.B. if using a rotation time,\nthe time will always "
136             "be a multiple of the rotation time, so you can synchronize\n"
137             "cron scripts with it). At the end of each rotation time or "
138             "when the file size\nis reached a new log is started.\n");
139     exit(1);
140 }
141
142 /*
143  * Get the unix time with timezone corrections
144  * given in the config struct.
145  */
146 static int get_now(rotate_config_t *config)
147 {
148     apr_time_t tNow = apr_time_now();
149     int utc_offset = config->utc_offset;
150     if (config->use_localtime) {
151         /* Check for our UTC offset before using it, since it might
152          * change if there's a switch between standard and daylight
153          * savings time.
154          */
155         apr_time_exp_t lt;
156         apr_time_exp_lt(&lt, tNow);
157         utc_offset = lt.tm_gmtoff;
158     }
159     return (int)apr_time_sec(tNow) + utc_offset;
160 }
161
162 /*
163  * Close a file and destroy the associated pool.
164  */
165 static void closeFile(rotate_config_t *config, apr_pool_t *pool, apr_file_t *file)
166 {
167     if (file != NULL) {
168         if (config->verbose) {
169             apr_finfo_t finfo;
170             apr_int32_t wanted = APR_FINFO_NAME;
171             if (apr_file_info_get(&finfo, wanted, file) == APR_SUCCESS) {
172                 fprintf(stderr, "Closing file %s (%s)\n", finfo.name, finfo.fname);
173             }
174         }
175         apr_file_close(file);
176         if (pool) {
177             apr_pool_destroy(pool);
178         }
179     }
180 }
181
182 /*
183  * Dump the configuration parsing result to STDERR.
184  */
185 static void dumpConfig (rotate_config_t *config)
186 {
187     fprintf(stderr, "Rotation time interval:      %12d\n", config->tRotation);
188     fprintf(stderr, "Rotation size interval:      %12d\n", config->sRotation);
189     fprintf(stderr, "Rotation time UTC offset:    %12d\n", config->utc_offset);
190     fprintf(stderr, "Rotation based on localtime: %12s\n", config->use_localtime ? "yes" : "no");
191     fprintf(stderr, "Rotation file date pattern:  %12s\n", config->use_strftime ? "yes" : "no");
192     fprintf(stderr, "Rotation file forced open:   %12s\n", config->force_open ? "yes" : "no");
193     fprintf(stderr, "Rotation verbose:            %12s\n", config->verbose ? "yes" : "no");
194     fprintf(stderr, "Rotation file name: %21s\n", config->szLogRoot);
195 }
196
197 /*
198  * Check whether we need to rotate.
199  * Possible reasons are:
200  * - No log file open (ROTATE_NEW)
201  * - User forces us to rotate (ROTATE_FORCE)
202  * - Our log file size is already bigger than the
203  *   allowed maximum (ROTATE_SIZE)
204  * - The next log time interval expired (ROTATE_TIME)
205  *
206  * When size and time constraints are both given,
207  * it suffices that one of them is fulfilled.
208  */
209 static void checkRotate(rotate_config_t *config, rotate_status_t *status)
210 {
211
212     if (status->nLogFD == NULL) {
213         status->rotateReason = ROTATE_NEW;
214     }
215     else if (config->sRotation) {
216         apr_finfo_t finfo;
217         apr_off_t current_size = -1;
218
219         if (apr_file_info_get(&finfo, APR_FINFO_SIZE, status->nLogFD) == APR_SUCCESS) {
220             current_size = finfo.size;
221         }
222
223         if (current_size > config->sRotation) {
224             status->rotateReason = ROTATE_SIZE;
225         }
226         else if (config->tRotation) {
227             if (get_now(config) >= status->tLogEnd) {
228                 status->rotateReason = ROTATE_TIME;
229             }
230         }
231     }
232     else if (config->tRotation) {
233         if (get_now(config) >= status->tLogEnd) {
234             status->rotateReason = ROTATE_TIME;
235         }
236     }
237     else {
238         fprintf(stderr, "No rotation time or size specified\n");
239         exit(2);
240     }
241
242     if (status->rotateReason != ROTATE_NONE && config->verbose) {
243         fprintf(stderr, "File rotation needed, reason: %s\n", ROTATE_REASONS[status->rotateReason]);
244     }
245
246     return;
247 }
248
249 /*
250  * Open a new log file, and if successful
251  * also close the old one.
252  *
253  * The timestamp for the calculation of the file
254  * name of the new log file will be the actual millisecond
255  * timestamp, except when a regular rotation based on a time
256  * interval is configured and the previous interval
257  * is over. Then the timestamp is the starting time
258  * of the actual interval.
259  */
260 static void doRotate(rotate_config_t *config, rotate_status_t *status)
261 {
262
263     int now = get_now(config);
264     int tLogStart;
265     apr_status_t rv;
266
267     status->rotateReason = ROTATE_NONE;
268     status->nLogFDprev = status->nLogFD;
269     status->nLogFD = NULL;
270     status->pfile_prev = status->pfile;
271
272     if (config->tRotation) {
273         int tLogEnd;
274         tLogStart = (now / config->tRotation) * config->tRotation;
275         tLogEnd = tLogStart + config->tRotation;
276         /*
277          * Check if rotation was forced and the last rotation
278          * interval is not yet over. Use the value of now instead
279          * of the time interval boundary for the file name then.
280          */
281         if (tLogStart < status->tLogEnd) {
282             tLogStart = now;
283         }
284         status->tLogEnd = tLogEnd;
285     }
286     else {
287         tLogStart = now;
288     }
289
290     if (config->use_strftime) {
291         apr_time_t tNow = apr_time_from_sec(tLogStart);
292         apr_time_exp_t e;
293         apr_size_t rs;
294
295         apr_time_exp_gmt(&e, tNow);
296         apr_strftime(status->filename, &rs, sizeof(status->filename), config->szLogRoot, &e);
297     }
298     else {
299         sprintf(status->filename, "%s.%010d", config->szLogRoot, tLogStart);
300     }
301     apr_pool_create(&status->pfile, status->pool);
302     if (config->verbose) {
303         fprintf(stderr, "Opening file %s\n", status->filename);
304     }
305     rv = apr_file_open(&status->nLogFD, status->filename, APR_WRITE | APR_CREATE | APR_APPEND,
306                        APR_OS_DEFAULT, status->pfile);
307     if (rv != APR_SUCCESS) {
308         char error[120];
309
310         apr_strerror(rv, error, sizeof error);
311
312         /* Uh-oh. Failed to open the new log file. Try to clear
313          * the previous log file, note the lost log entries,
314          * and keep on truckin'. */
315         if (status->nLogFDprev == NULL) {
316             fprintf(stderr, "Could not open log file '%s' (%s)\n", status->filename, error);
317             exit(2);
318         }
319         else {
320             apr_size_t nWrite;
321             status->nLogFD = status->nLogFDprev;
322             apr_pool_destroy(status->pfile);
323             status->pfile = status->pfile_prev;
324             /* Try to keep this error message constant length
325              * in case it occurs several times. */
326             apr_snprintf(status->errbuf, sizeof status->errbuf,
327                          "Resetting log file due to error opening "
328                          "new log file, %10d messages lost: %-25.25s\n",
329                          status->nMessCount, error);
330             nWrite = strlen(status->errbuf);
331             apr_file_trunc(status->nLogFD, 0);
332             if (apr_file_write(status->nLogFD, status->errbuf, &nWrite) != APR_SUCCESS) {
333                 fprintf(stderr, "Error writing to the file %s\n", status->filename);
334                 exit(2);
335             }
336         }
337     }
338     else {
339         closeFile(config, status->pfile_prev, status->nLogFDprev);
340         status->nLogFDprev = NULL;
341         status->pfile_prev = NULL;
342     }
343     status->nMessCount = 0;
344 }
345
346 /*
347  * Get a size or time param from a string.
348  * Parameter 'last' indicates, whether the
349  * argument is the last commadnline argument.
350  * UTC offset is only allowed as a last argument
351  * in order to make is distinguishable from the
352  * rotation interval time.
353  */
354 static const char *get_time_or_size(rotate_config_t *config,
355                                     const char *arg, int last) {
356     char *ptr = NULL;
357     /* Byte multiplier */
358     unsigned int mult = 1;
359     if ((ptr = strchr(arg, 'B')) != NULL) { /* Found KB size */
360         mult = 1;
361     }
362     else if ((ptr = strchr(arg, 'K')) != NULL) { /* Found KB size */
363         mult = 1024;
364     }
365     else if ((ptr = strchr(arg, 'M')) != NULL) { /* Found MB size */
366         mult = 1024 * 1024;
367     }
368     else if ((ptr = strchr(arg, 'G')) != NULL) { /* Found GB size */
369         mult = 1024 * 1024 * 1024;
370     }
371     if (ptr) { /* rotation based on file size */
372         if (config->sRotation > 0) {
373             return "Rotation size parameter allowed only once";
374         }
375         if (*(ptr+1) == '\0') {
376             config->sRotation = atoi(arg) * mult;
377         }
378         if (config->sRotation == 0) {
379             return "Invalid rotation size parameter";
380         }
381     }
382     else if ((config->sRotation > 0 || config->tRotation > 0) && last) {
383         /* rotation based on elapsed time */
384         if (config->use_localtime) {
385             return "UTC offset parameter is not valid with -l";
386         }
387         config->utc_offset = atoi(arg) * 60;
388     }
389     else { /* rotation based on elapsed time */
390         if (config->tRotation > 0) {
391             return "Rotation time parameter allowed only once";
392         }
393         config->tRotation = atoi(arg);
394         if (config->tRotation <= 0) {
395             return "Invalid rotation time parameter";
396         }
397     }
398     return NULL;
399 }
400
401 int main (int argc, const char * const argv[])
402 {
403     char buf[BUFSIZE];
404     apr_size_t nRead, nWrite;
405     apr_file_t *f_stdin;
406     apr_getopt_t *opt;
407     apr_status_t rv;
408     char c;
409     const char *optarg;
410     const char *err = NULL;
411
412     apr_app_initialize(&argc, &argv, NULL);
413     atexit(apr_terminate);
414
415     config.sRotation = 0;
416     config.tRotation = 0;
417     config.utc_offset = 0;
418     config.use_localtime = 0;
419     config.use_strftime = 0;
420     config.force_open = 0;
421     config.verbose = 0;
422     status.pool = NULL;
423     status.pfile = NULL;
424     status.pfile_prev = NULL;
425     status.nLogFD = NULL;
426     status.nLogFDprev = NULL;
427     status.tLogEnd = 0;
428     status.rotateReason = ROTATE_NONE;
429     status.nMessCount = 0;
430
431     apr_pool_create(&status.pool, NULL);
432     apr_getopt_init(&opt, status.pool, argc, argv);
433     while ((rv = apr_getopt(opt, "lfv", &c, &optarg)) == APR_SUCCESS) {
434         switch (c) {
435         case 'l':
436             config.use_localtime = 1;
437             break;
438         case 'f':
439             config.force_open = 1;
440             break;
441         case 'v':
442             config.verbose = 1;
443             break;
444         }
445     }
446
447     if (rv != APR_EOF) {
448         usage(argv[0], NULL /* specific error message already issued */ );
449     }
450
451     /*
452      * After the initial flags we need 2 to 4 arguments,
453      * the file name, either the rotation interval time or size
454      * or both of them, and optionally the UTC offset.
455      */
456     if ((argc - opt->ind < 2) || (argc - opt->ind > 4) ) {
457         usage(argv[0], "Incorrect number of arguments");
458     }
459
460     config.szLogRoot = argv[opt->ind++];
461
462     /* Read in the remaining flags, namely time, size and UTC offset. */
463     for(; opt->ind < argc; opt->ind++) {
464         if ((err = get_time_or_size(&config, argv[opt->ind],
465                                     opt->ind < argc - 1 ? 0 : 1)) != NULL) {
466             usage(argv[0], err);
467         }
468     }
469
470     config.use_strftime = (strchr(config.szLogRoot, '%') != NULL);
471
472     if (apr_file_open_stdin(&f_stdin, status.pool) != APR_SUCCESS) {
473         fprintf(stderr, "Unable to open stdin\n");
474         exit(1);
475     }
476
477     /*
478      * Write out result of config parsing if verbose is set.
479      */
480     if (config.verbose) {
481         dumpConfig(&config);
482     }
483
484     /*
485      * Immediately open the logfile as we start, if we were forced
486      * to do so via '-f'.
487      */
488     if (config.force_open) {
489         doRotate(&config, &status);
490     }
491
492     for (;;) {
493         nRead = sizeof(buf);
494         rv = apr_file_read(f_stdin, buf, &nRead);
495         if (rv != APR_SUCCESS) {
496             exit(3);
497         }
498         checkRotate(&config, &status);
499         if (status.rotateReason != ROTATE_NONE) {
500             doRotate(&config, &status);
501         }
502
503         nWrite = nRead;
504         rv = apr_file_write(status.nLogFD, buf, &nWrite);
505         if (rv == APR_SUCCESS && nWrite != nRead) {
506             /* buffer partially written, which for rotatelogs means we encountered
507              * an error such as out of space or quota or some other limit reached;
508              * try to write the rest so we get the real error code
509              */
510             apr_size_t nWritten = nWrite;
511
512             nRead  = nRead - nWritten;
513             nWrite = nRead;
514             rv = apr_file_write(status.nLogFD, buf + nWritten, &nWrite);
515         }
516         if (nWrite != nRead) {
517             char strerrbuf[120];
518             apr_off_t cur_offset;
519
520             cur_offset = 0;
521             if (apr_file_seek(status.nLogFD, APR_CUR, &cur_offset) != APR_SUCCESS) {
522                 cur_offset = -1;
523             }
524             apr_strerror(rv, strerrbuf, sizeof strerrbuf);
525             status.nMessCount++;
526             apr_snprintf(status.errbuf, sizeof status.errbuf,
527                          "Error %d writing to log file at offset %" APR_OFF_T_FMT ". "
528                          "%10d messages lost (%s)\n",
529                          rv, cur_offset, status.nMessCount, strerrbuf);
530             nWrite = strlen(status.errbuf);
531             apr_file_trunc(status.nLogFD, 0);
532             if (apr_file_write(status.nLogFD, status.errbuf, &nWrite) != APR_SUCCESS) {
533                 fprintf(stderr, "Error writing to the file %s\n", status.filename);
534                 exit(2);
535             }
536         }
537         else {
538             status.nMessCount++;
539         }
540     }
541     /* Of course we never, but prevent compiler warnings */
542     return 0;
543 }