1 /**
2  * Utilities for the quality of service module mod_qos.
3  *
4  * qsrotate.c: Log rotation tool.
5  *
6  * See http://mod-qos.sourceforge.net/ for further
7  * details.
8  *
9  * Copyright (C) 2020 Pascal Buchbinder
10  *
11  * Licensed to the Apache Software Foundation (ASF) under one or more
12  * contributor license agreements.  See the NOTICE file distributed with
13  * this work for additional information regarding copyright ownership.
14  * The ASF licenses this file to You under the Apache License, Version 2.0
15  * (the "License"); you may not use this file except in compliance with
16  * the License.  You may obtain a copy of the License at
17  *
18  *     http://www.apache.org/licenses/LICENSE-2.0
19  *
20  * Unless required by applicable law or agreed to in writing, software
21  * distributed under the License is distributed on an "AS IS" BASIS,
22  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23  * See the License for the specific language governing permissions and
24  * limitations under the License.
25  *
26  */
27 
28 static const char revision[] = "$Id: qsrotate.c 2595 2020-01-03 06:19:53Z pbuchbinder $";
29 
30 #include <stdio.h>
31 #include <string.h>
32 
33 #include <errno.h>
34 #include <fcntl.h>
35 #include <dirent.h>
36 
37 #include <stdlib.h>
38 #include <unistd.h>
39 
40 #include <pthread.h>
41 
42 #include <time.h>
43 #include <zlib.h>
44 
45 #include <signal.h>
46 #include <sys/types.h>
47 #include <sys/wait.h>
48 #include <sys/stat.h>
49 
50 #include "qs_util.h"
51 
52 #define HUGE_STR       1024
53 
54 //yyyy-mm-dd<sp>hh-mm-ss<sp>
55 #define TME_STR_LEN    20
56 
57 /* global variables used by main and support thread */
58 static int m_force_rotation = 0;
59 static time_t m_tLogEnd = 0;
60 static time_t m_tRotation = 86400; /* default are 24h */
61 static int m_nLogFD = -1;
62 static int m_generations = -1;
63 static mode_t m_mode = 0660;
64 static char *m_file_name = NULL;
65 static long m_messages = 0;
66 static char *m_cmd = NULL;
67 static int m_compress = 0;
68 static int m_stdout = 0;
69 static int m_timestamp = 0;
70 static char time_string[TME_STR_LEN];
71 static long m_counter = 0;
72 static long m_limit = 2147483648 - (128 * 1024);
73 static int m_offset = 0;
74 static int m_offset_enabled = 0;
75 
usage(char * cmd,int man)76 static void usage(char *cmd, int man) {
77   if(man) {
78     //.TH [name of program] [section number] [center footer] [left footer] [center header]
79     printf(".TH %s 1 \"%s\" \"mod_qos utilities %s\" \"%s man page\"\n", qs_CMD(cmd), man_date,
80 	   man_version, cmd);
81   }
82   printf("\n");
83   if(man) {
84     printf(".SH NAME\n");
85   }
86   qs_man_print(man, "%s - a log rotation tool (similar to Apache's rotatelogs).\n", cmd);
87   printf("\n");
88   if(man) {
89     printf(".SH SYNOPSIS\n");
90   }
91   qs_man_print(man, "%s%s -o <file> [-s <sec> [-t <hours>]] [-b <bytes>] [-f] [-z] [-g <num>] [-u <name>] [-m <mask>] [-p] [-d]\n", man ? "" : "Usage: ", cmd);
92   printf("\n");
93   if(man) {
94     printf(".SH DESCRIPTION\n");
95   } else {
96     printf("Summary\n");
97   }
98   qs_man_print(man, "%s reads from stdin (piped log) and writes the data to the provided\n", cmd);
99   qs_man_print(man, "file rotating the file after the specified time.\n");
100   printf("\n");
101   if(man) {
102     printf(".SH OPTIONS\n");
103   } else {
104     printf("Options\n");
105   }
106   if(man) printf(".TP\n");
107   qs_man_print(man, "  -o <file>\n");
108   if(man) printf("\n");
109   qs_man_print(man, "     Output log file to write the data to (use an absolute path).\n");
110   if(man) printf("\n.TP\n");
111   qs_man_print(man, "  -s <sec>\n");
112   if(man) printf("\n");
113   qs_man_print(man, "     Rotation interval in seconds, default are 86400 seconds.\n");
114   if(man) printf("\n.TP\n");
115   qs_man_print(man, "  -t <hours>\n");
116   if(man) printf("\n");
117   qs_man_print(man, "     Offset to UTC (enables also DST support), default is 0.\n");
118   if(man) printf("\n.TP\n");
119   qs_man_print(man, "  -b <bytes>\n");
120   if(man) printf("\n");
121   qs_man_print(man, "     File size limitation (default/max. are %ld bytes, min. are 1048576 bytes).\n", m_limit);
122   if(man) printf("\n.TP\n");
123   qs_man_print(man, "  -f\n");
124   if(man) printf("\n");
125   qs_man_print(man, "     Forced log rotation at the specified interval even no data is written.\n");
126   if(man) printf("\n.TP\n");
127   qs_man_print(man, "  -z\n");
128   if(man) printf("\n");
129   qs_man_print(man, "     Compress (gzip) the rotated file.\n");
130   if(man) printf("\n.TP\n");
131   qs_man_print(man, "  -g <num>\n");
132   if(man) printf("\n");
133   qs_man_print(man, "     Generations (number of files to keep).\n");
134   if(man) printf("\n.TP\n");
135   qs_man_print(man, "  -u <name>\n");
136   if(man) printf("\n");
137   qs_man_print(man, "     Become another user, e.g. www-data.\n");
138   qs_man_print(man, "  -m <mask>\n");
139   if(man) printf("\n");
140   qs_man_print(man, "     File permission which is either 600, 640, 660 (default) or 664.\n");
141   if(man) printf("\n.TP\n");
142   qs_man_print(man, "  -p\n");
143   if(man) printf("\n");
144   qs_man_print(man, "     Writes data also to stdout (for piped logging).\n");
145   qs_man_print(man, "  -d\n");
146   if(man) printf("\n");
147   qs_man_print(man, "     Line-by-line data reading prefixing every line with a timestamp.\n");
148   printf("\n");
149   if(man) {
150     printf(".SH EXAMPLE\n");
151   } else {
152     printf("Example:\n");
153   }
154   qs_man_println(man, "  TransferLog \"|/usr/bin/%s -f -z -g 3 -o /var/log/apache/access.log -s 86400\"\n", cmd);
155   printf("\n");
156   qs_man_print(man, "The name of the rotated file will be /dest/filee.YYYYmmddHHMMSS\n");
157   qs_man_print(man, "where YYYYmmddHHMMSS is the system time at which the data has been\n");
158   qs_man_print(man, "rotated.\n");
159   printf("\n");
160   if(man) {
161     printf(".SH NOTE\n");
162   } else {
163     printf("Notes:\n");
164   }
165   qs_man_println(man, " - Each %s instance must use an individual file.\n", cmd);
166   qs_man_println(man, " - You may trigger a file rotation manually by sending the signal USR1\n");
167   qs_man_print(man, "   to the process.\n");
168   printf("\n");
169   if(man) {
170     printf(".SH SEE ALSO\n");
171     printf("qsdt(1), qsexec(1), qsfilter2(1), qsgeo(1), qsgrep(1), qshead(1), qslog(1), qslogger(1), qsre(1), qsrespeed(1), qspng(1), qssign(1), qstail(1)\n");
172     printf(".SH AUTHOR\n");
173     printf("Pascal Buchbinder, http://mod-qos.sourceforge.net/\n");
174   } else {
175     printf("See http://mod-qos.sourceforge.net/ for further details.\n");
176   }
177   if(man) {
178     exit(0);
179   } else {
180     exit(1);
181   }
182 }
183 
get_now()184 static time_t get_now() {
185   time_t now = time(NULL);
186   if(m_offset_enabled) {
187     struct tm lcl = *localtime(&now);
188     if(lcl.tm_isdst) {
189       now += 3600;
190     }
191     now += m_offset;
192   }
193   return now;
194 }
195 
openFile(const char * cmd,const char * file_name)196 static int openFile(const char *cmd, const char *file_name) {
197   int m_nLogFD = open(file_name, O_WRONLY | O_CREAT | O_APPEND, m_mode);
198   /* error while opening log file */
199   if(m_nLogFD < 0) {
200     fprintf(stderr,"[%s]: ERROR, failed to open file <%s>\n", cmd, file_name);
201   }
202   return m_nLogFD;
203 }
204 
205 /**
206  * Compress method called by a child process (forked)
207  * used to compress the rotated file.
208  *
209  * @param cmd Command name (used when logging errors)
210  * @param arch Path to the file to compress. File gets renamed to <arch>.gz
211  */
compressThread(const char * cmd,const char * arch)212 static void compressThread(const char *cmd, const char *arch) {
213   gzFile *outfp;
214   int infp;
215   char dest[HUGE_STR+20];
216   char buf[HUGE_STR];
217   int len;
218   snprintf(dest, sizeof(dest), "%s.gz", arch);
219   /* low prio */
220   if(nice(10) == -1) {
221     fprintf(stderr, "[%s]: WARNING, failed to change nice value: %s\n", cmd, strerror(errno));
222   }
223   if((infp = open(arch, O_RDONLY)) == -1) {
224     /* failed to open file, can't compress it */
225     fprintf(stderr,"[%s]: ERROR, could not open file for compression <%s>\n", cmd, arch);
226     return;
227   }
228   if((outfp = gzopen(dest,"wb")) == NULL) {
229     fprintf(stderr,"[%s]: ERROR, could not open file for compression <%s>\n", cmd, dest);
230     close(infp);
231     return;
232   }
233   chmod(dest, m_mode);
234   while((len = read(infp, buf, sizeof(buf))) > 0) {
235     gzwrite(outfp, buf, len);
236   }
237   gzclose(outfp);
238   close(infp);
239   /* done, delete the old file */
240   unlink(arch);
241 }
242 
sigchild(int signo)243 void sigchild(int signo) {
244   pid_t pid;
245   int stat;
246   while((pid=waitpid(-1,&stat,WNOHANG)) > 0) {
247   }
248 }
249 
writeTimestamp()250 void writeTimestamp() {
251   time_t tm = time(NULL);
252   struct tm *ptr = localtime(&tm);
253   strftime(time_string, TME_STR_LEN, "%Y-%m-%d %H:%M:%S ", ptr);
254   write(m_nLogFD, time_string, TME_STR_LEN);
255 }
256 
257 /**
258  * Rotates a file
259  *
260  * @param cmd Command name to be used in log messages
261  * @param now
262  * @param file_name Name of the file to rotate (rename)
263  * @param messages Number of lines/buffers which had been read
264  */
rotate(const char * cmd,time_t now,const char * file_name,long * messages)265 static void rotate(const char *cmd, time_t now,
266 		   const char *file_name, long *messages) {
267   int rc;
268   char arch[HUGE_STR+20];
269   char tmb[20];
270   struct tm *ptr = localtime(&now);
271   strftime(tmb, sizeof(tmb), "%Y%m%d%H%M%S", ptr);
272   snprintf(arch, sizeof(arch), "%s.%s", file_name, tmb);
273 
274   /* set next rotation time */
275   m_tLogEnd = ((now / m_tRotation) * m_tRotation) + m_tRotation;
276   // reset byte counter
277   m_counter = 0;
278 
279   /* rename current file */
280   if(m_nLogFD >= 0) {
281     close(m_nLogFD);
282     rename(file_name, arch);
283   }
284 
285   /* open new file */
286   m_nLogFD = openFile(cmd, file_name);
287   if(m_nLogFD < 0) {
288     /* opening a new file has failed!
289        try to reopen and clear the last file */
290     char msg[HUGE_STR];
291     snprintf(msg, sizeof(msg), "ERROR while writing to file, %ld messages lost\n", *messages);
292     fprintf(stderr,"[%s]: ERROR, while writing to file <%s>\n", cmd, file_name);
293     rename(arch,  file_name);
294     m_nLogFD = openFile(cmd, file_name);
295     if(m_nLogFD > 0) {
296       rc = ftruncate(m_nLogFD, 0);
297       rc = write(m_nLogFD, msg, strlen(msg));
298     }
299   } else {
300     *messages = 0;
301     if(m_compress || (m_generations != -1)) {
302       signal(SIGCHLD,sigchild);
303       if(fork() == 0) {
304 	if(m_compress) {
305 	  compressThread(cmd, arch);
306 	}
307 	if(m_generations != -1) {
308 	  qs_deleteOldFiles(file_name, m_generations);
309 	}
310 	exit(0);
311       }
312     }
313   }
314 }
315 
316 /**
317  * Separate thread which initiates file rotation even no
318  * log data is written.
319  *
320  * @param argv (not used)
321  */
forcedRotationThread(void * argv)322 static void *forcedRotationThread(void *argv) {
323   time_t now;
324   time_t n;
325   while(1) {
326     qs_csLock();
327     now = get_now();
328     if(now > m_tLogEnd) {
329       rotate(m_cmd, now, m_file_name, &m_messages);
330     }
331     qs_csUnLock();
332     now = get_now();
333     n = 1 + m_tLogEnd - now;
334     sleep(n);
335   }
336   return NULL;
337 }
338 
handle_signal1(int signal)339 void handle_signal1(int signal) {
340   rotate(m_cmd, get_now(), m_file_name, &m_messages);
341   return;
342 }
343 
main(int argc,char ** argv)344 int main(int argc, char **argv) {
345   char *username = NULL;
346   int rc;
347   char *buf;
348   int nRead, nWrite;
349   time_t now;
350   struct stat st;
351   long sizeLimit = 0;
352 
353   pthread_attr_t *tha = NULL;
354   pthread_t tid;
355   struct sigaction sa;
356 
357   char *cmd = strrchr(argv[0], '/');
358 
359   sa.sa_handler = &handle_signal1;
360   sa.sa_flags = SA_RESTART;
361 
362   if(cmd == NULL) {
363     cmd = argv[0];
364   } else {
365     cmd++;
366   }
367   m_cmd = calloc(1, strlen(cmd)+1);
368   strcpy(m_cmd, cmd); // copy as we can't pass it when forking
369 
370   while(argc >= 1) {
371     if(strcmp(*argv,"-o") == 0) {
372       if (--argc >= 1) {
373 	m_file_name = *(++argv);
374       }
375     } else if(strcmp(*argv,"-u") == 0) {
376       if (--argc >= 1) {
377 	username = *(++argv);
378       }
379     } else if(strcmp(*argv,"-s") == 0) {
380       if (--argc >= 1) {
381 	m_tRotation = atoi(*(++argv));
382       }
383     } else if(strcmp(*argv,"-t") == 0) {
384       if (--argc >= 1) {
385 	m_offset = atoi(*(++argv));
386 	m_offset = m_offset * 3600;
387 	m_offset_enabled = 1;
388       }
389     } else if(strcmp(*argv,"-g") == 0) {
390       if (--argc >= 1) {
391 	m_generations = atoi(*(++argv));
392       }
393     } else if(strcmp(*argv,"-b") == 0) {
394       if (--argc >= 1) {
395 	sizeLimit = atol(*(++argv));
396       }
397     } else if(strcmp(*argv,"-m") == 0) {
398       if (--argc >= 1) {
399 	int mode = atoi(*(++argv));
400 	if(mode == 600) {
401 	  m_mode = 0600;
402 	} else if(mode == 640) {
403 	  m_mode = 0640;
404 	} else if(mode == 660) {
405 	  m_mode = 0660;
406 	} else if(mode == 664) {
407 	  m_mode = 0664;
408 	}
409       }
410     } else if(strcmp(*argv,"-z") == 0) {
411       m_compress = 1;
412     } else if(strcmp(*argv,"-p") == 0) {
413       m_stdout = 1;
414     } else if(strcmp(*argv,"-d") == 0) {
415       m_timestamp = 1;
416       memset(time_string, 32, TME_STR_LEN);
417     } else if(strcmp(*argv,"-f") == 0) {
418       m_force_rotation = 1;
419     } else if(strcmp(*argv,"-h") == 0) {
420       usage(m_cmd, 0);
421     } else if(strcmp(*argv,"--help") == 0) {
422       usage(m_cmd, 0);
423     } else if(strcmp(*argv,"-?") == 0) {
424       usage(m_cmd, 0);
425     } else if(strcmp(*argv,"--man") == 0) {
426       usage(m_cmd, 1);
427     }
428 
429     argc--;
430     argv++;
431   }
432 
433   if(m_file_name == NULL) usage(m_cmd, 0);
434   if(sizeLimit > 0 && sizeLimit < m_limit && sizeLimit >= (1024 * 1024)) {
435     m_limit = sizeLimit;
436   } else if(sizeLimit > 0 && sizeLimit < (1024 * 1024)) {
437     m_limit = 1024 * 1024;
438   }
439 
440   if(stat(m_file_name, &st) == 0) {
441     m_counter = st.st_size;
442   }
443 
444   sigaction(SIGUSR1, &sa, NULL);
445   qs_setuid(username, m_cmd);
446 
447   /* set next rotation time */
448   now = get_now();
449   m_tLogEnd = ((now / m_tRotation) * m_tRotation) + m_tRotation;
450   /* open file */
451   m_nLogFD = openFile(m_cmd, m_file_name);
452   if(m_nLogFD < 0) {
453     /* startup did not success */
454     exit(2);
455   }
456 
457   if(m_force_rotation) {
458     qs_csInitLock();
459     pthread_create(&tid, tha, forcedRotationThread, NULL);
460   }
461 
462   buf = calloc(1, MAX_LINE_BUFFER+1);
463   for(;;) {
464     if(m_timestamp) {
465       // low perf line-by-line read
466       if(fgets(buf, MAX_LINE_BUFFER, stdin) == NULL) {
467 	exit(3);
468       } else {
469 	nRead = strlen(buf);
470 	if(m_force_rotation) {
471 	qs_csLock();                       // >@CTR1
472       }
473 	m_counter += (nRead + TME_STR_LEN);
474 	now = get_now();
475 	writeTimestamp();
476 	nWrite = write(m_nLogFD, buf, nRead);
477       }
478     } else {
479       // normal/fast buffer read/process
480       nRead = read(0, buf, MAX_LINE_BUFFER);
481       if(nRead == 0) exit(3);
482       if(nRead < 0) if(errno != EINTR) exit(4);
483       if(m_force_rotation) {
484 	qs_csLock();                         // >@CTR1
485       }
486       m_counter += nRead;
487       now = get_now();
488       /* write data if we have a file handle (else continue but drop log data,
489 	 re-try to open the file at next rotation time) */
490       if(m_nLogFD >= 0) {
491 	do {
492 	  nWrite = write(m_nLogFD, buf, nRead);
493 	  if(m_stdout) {
494 	    printf("%.*s", nRead, buf);
495 	  }
496 	} while (nWrite < 0 && errno == EINTR);
497       }
498       m_messages++;
499       if(nWrite != nRead) {
500 	if(m_nLogFD >= 0) {
501 	  char msg[HUGE_STR];
502 	  snprintf(msg, sizeof(msg), "ERROR while writing to file, %ld messages lost\n", m_messages);
503 	  /* error while writing data, try to delete the old file and continue ... */
504 	  rc = ftruncate(m_nLogFD, 0);
505 	  rc = write(m_nLogFD, msg, strlen(msg));
506 	  m_messages = 0;
507 	}
508       }
509     }
510     // end buffer or line read
511     if((now > m_tLogEnd) || (m_counter > m_limit)) {
512       /* rotate! */
513       rotate(m_cmd, now, m_file_name, &m_messages);
514     }
515     if(m_force_rotation) {
516       qs_csUnLock();                         // <@CTR1
517     }
518   }
519   memset(buf, 0, MAX_LINE_BUFFER);
520   free(buf);
521   return 0;
522 }
523