Remove wheel submodule - will re-add later with new URL
[dmon:dmon.git] / drlog.c
1 /*
2  * drlog.c
3  * Copyright © 2010-2011 Adrián Pérez <aperez@igalia.com>
4  * Copyright © 2008-2010 Adrián Pérez <aperez@connectical.com>
5  *
6  * Distributed under terms of the MIT license.
7  */
8
9 #include "wheel.h"
10 #include "util.h"
11 #include <time.h>
12 #include <stdio.h>
13 #include <limits.h>
14 #include <string.h>
15 #include <dirent.h>
16 #include <stdlib.h>
17 #include <signal.h>
18 #include <sys/types.h>
19 #include <sys/param.h>
20 #include <sys/stat.h>
21 #include <unistd.h>
22 #include <errno.h>
23 #include <fcntl.h>
24
25 #ifdef NO_MULTICALL
26 # define drlog_main main
27 #endif /* NO_MULTICALL */
28
29 #ifndef LOGDIR_PERMS
30 #define LOGDIR_PERMS 0750
31 #endif /* !LOGDIR_PERMS */
32
33 #ifndef LOGDIR_TSTAMP
34 #define LOGDIR_TSTAMP ".timestamp"
35 #endif /* !LOGDIR_TSTAMP */
36
37 #ifndef LOGFILE_PERMS
38 #define LOGFILE_PERMS 0640
39 #endif /* !LOGFILE_PERMS */
40
41 #ifndef LOGFILE_PREFIX
42 #define LOGFILE_PREFIX "log-"
43 #endif /* !LOGFILE_PREFIX */
44
45 #ifndef LOGFILE_CURRENT
46 #define LOGFILE_CURRENT "current"
47 #endif /* !LOGFILE_CURRENT */
48
49 #ifndef LOGFILE_DEFMAX
50 #define LOGFILE_DEFMAX 10
51 #endif /* !LOGFILE_DEFMAX */
52
53 #ifndef LOGFILE_DEFSIZE
54 #define LOGFILE_DEFSIZE (150 * 1024) /* 150 kB */
55 #endif /* !LOGFILE_DEFSIZE */
56
57 #ifndef LOGFILE_DEFTIME
58 #define LOGFILE_DEFTIME (60 * 60 * 24 * 5) /* Five days */
59 #endif /* !LOGFILE_DEFTIME */
60
61 #ifndef TSTAMP_FMT
62 #define TSTAMP_FMT "%Y-%m-%d/%H:%M:%S "
63 #endif /* !TSTAMP_FMT */
64
65 #ifndef TSTAMP_LEN
66 #define TSTAMP_LEN (5 + 3 + 3 + 3 + 3 + 3)
67 #endif /* !TSTAMP_LEN */
68
69
70 static char              *directory  = NULL;
71 static w_io_t            *out_io     = NULL;
72 static unsigned           maxfiles   = LOGFILE_DEFMAX;
73 static unsigned long long maxtime    = LOGFILE_DEFTIME;
74 static unsigned long long maxsize    = LOGFILE_DEFSIZE;
75 static unsigned long long curtime    = 0;
76 static unsigned long long cursize    = 0;
77 static wbool              timestamp  = W_NO;
78 static wbool              buffered   = W_NO;
79 static int                returncode = 0;
80 static w_buf_t            line       = W_BUF;
81 static w_buf_t            overflow   = W_BUF;
82
83
84 static int
85 rotate_log (void)
86 {
87     char old_name[MAXPATHLEN];
88     char path[MAXPATHLEN];
89     const char *name;
90     DIR *dir;
91     struct dirent *dirent;
92     unsigned foundlogs;
93     int year, mon, mday, hour, min, sec;
94     int older_year, older_mon = INT_MAX, older_mday = INT_MAX,
95             older_hour = INT_MAX, older_min = INT_MAX, older_sec = INT_MAX;
96
97 rescan:
98     foundlogs = 0;
99     *old_name = 0;
100     older_year = INT_MAX;
101
102     if ((dir = opendir (directory)) == NULL) {
103         w_io_format (w_stderr,
104                      "Unable to open directory '$s' for rotation ($E).\n",
105                      directory);
106         return -1;
107     }
108
109     while ((dirent = readdir (dir)) != NULL) {
110         name = dirent->d_name;
111         if (!strncmp (name, LOGFILE_PREFIX, sizeof (LOGFILE_PREFIX) - 1U)) {
112             if (sscanf (name, LOGFILE_PREFIX "%d-%d-%d-%d:%d:%d",
113                         &year, &mon, &mday, &hour, &min, &sec) != 6)
114                 continue;
115
116             foundlogs++;
117             if (year < older_year || (year == older_year &&
118                (mon  < older_mon  || (mon  == older_mon  &&
119                (mday < older_mday || (mday == older_mday &&
120                (hour < older_hour || (hour == older_hour &&
121                (min  < older_min  || (min  == older_min  &&
122                (sec  < older_sec))))))))))) /* Oh yeah! */
123             {
124                 older_year = year;
125                 older_mon  = mon ;
126                 older_mday = mday;
127                 older_hour = hour;
128                 older_min  = min ;
129                 older_sec  = sec ;
130                 strncpy (old_name, name, sizeof (old_name));
131                 old_name[sizeof (old_name) - 1] = 0;
132             }
133         }
134     }
135
136     closedir (dir);
137
138     if (foundlogs >= maxfiles) {
139         if (*old_name == 0) {
140             return -3;
141         }
142         if (snprintf (path, sizeof (path), "%s/%s", directory, old_name) < 0) {
143             w_io_format (w_stderr, "Path too long to unlink: $s/$s\n",
144                          directory, old_name);
145             return -4;
146         }
147         if (unlink (path) < 0) {
148             return -2;
149         }
150         foundlogs--;
151         if (foundlogs >= maxfiles) {
152             goto rescan;
153         }
154     }
155     return 0;
156 }
157
158
159 static void
160 flush_line (void)
161 {
162     time_t now = time (NULL);
163     w_buf_t out = W_BUF;
164
165     if (!out_io) {
166         /* We need to open the output file. */
167         char path[MAXPATHLEN];
168         struct stat st;
169         time_t ts;
170         w_io_t *ts_io;
171
172 testdir:
173         if (stat (directory, &st) < 0 || !S_ISDIR (st.st_mode))
174             w_die ((errno == ENOENT)
175                         ? "Output directory does not exist: $s\n"
176                         : "Output path is not a directory: $s\n",
177                    directory);
178
179         if (snprintf (path, sizeof (path), "%s/" LOGFILE_CURRENT, directory) < 0)
180             w_die ("Path name too long: $s\n", directory);
181
182         out_io = w_io_unix_open (path,
183                                  O_APPEND | O_CREAT | O_WRONLY,
184                                  LOGFILE_PERMS);
185
186         if (!out_io)
187             w_die ("Cannot open '$s': $E\n", path);
188
189         if (snprintf (path, sizeof (path), "%s/" LOGDIR_TSTAMP, directory) < 0) {
190             w_obj_unref (out_io);
191             w_die ("Path name too long: $s\n", directory);
192         }
193
194         if ((ts_io = w_io_unix_open (path, O_RDONLY, 0)) == NULL) {
195 recreate_ts:
196             ts = time (NULL);
197             ts_io = w_io_unix_open (path, O_WRONLY | O_CREAT | O_TRUNC,
198                                     LOGFILE_PERMS);
199             if (!ts_io) {
200                 w_io_format (w_stderr, "Unable to write timestamp to '$s'\n",
201                              directory);
202                 w_obj_unref (out_io);
203                 w_die (NULL);
204             }
205             w_io_format (ts_io, "$I\n", (unsigned long) ts);
206         }
207         else {
208             unsigned long long ts_;
209             w_buf_t tsb = W_BUF;
210
211             if ((w_io_read_line (ts_io, &tsb, &overflow, 0) == W_IO_ERR) ||
212                 (sscanf (w_buf_str (&tsb), "%llu", &ts_) <= 0))
213             {
214                 w_obj_unref (ts_io);
215                 w_buf_free (&tsb);
216                 goto recreate_ts;
217             }
218             ts = ts_;
219             w_buf_free (&tsb);
220         }
221         w_obj_unref (ts_io);
222         curtime = ts;
223         if (maxtime) {
224             curtime -= (curtime % maxtime);
225         }
226         cursize = (unsigned long long) lseek (W_IO_UNIX_FD (out_io), 0, SEEK_CUR);
227     }
228
229     if ((maxsize != 0) && (maxtime != 0)) {
230         if (cursize >= maxsize || (unsigned long long) now > (curtime + maxtime)) {
231             struct tm *time_gm;
232             char path[MAXPATHLEN];
233             char newpath[MAXPATHLEN];
234
235             if (!out_io)
236                 w_die ("Internal inconsistency at $s:$i\n", __FILE__, __LINE__);
237
238             if ((time_gm = gmtime(&now)) == NULL)
239                 w_die ("Unable to get current date: $E\n");
240
241             if (snprintf (newpath, sizeof (newpath), "%s/" LOGFILE_PREFIX, directory) < 0)
242                 w_die ("Path name too long: $s\n", directory);
243
244             if (strftime (newpath + strlen (newpath),
245                           sizeof (newpath) - strlen(newpath),
246                           "%Y-%m-%d-%H:%M:%S",
247                           time_gm) == 0)
248                 w_die ("Path name too long: '$s'\n", directory);
249
250             if (snprintf(path, sizeof (path), "%s/" LOGFILE_CURRENT, directory) < 0)
251                 w_die ("Path name too long: $s\n", directory);
252
253             rotate_log ();
254
255             w_obj_unref (out_io);
256             out_io = NULL;
257
258             if (rename (path, newpath) < 0 && unlink (path) < 0)
259                 w_die ("Unable to rename '$s' to '$s'\n", path, newpath);
260
261             if (snprintf (path, sizeof (path), "%s/" LOGDIR_TSTAMP, directory) < 0)
262                 w_die ("Path name too long: $s\n", directory);
263
264             unlink (path);
265             goto testdir;
266         }
267     }
268
269     if (w_buf_length (&line) == 0) {
270         w_buf_free (&line);
271         return;
272     }
273
274     if (timestamp) {
275         char timebuf[TSTAMP_LEN+1];
276         struct tm *time_gm = gmtime (&now);
277
278         if (strftime (timebuf, TSTAMP_LEN+1, TSTAMP_FMT, time_gm) == 0)
279             w_die ("Cannot format timestamp\n");
280
281         w_buf_append_mem (&out, timebuf, strlen (timebuf));
282     }
283
284     w_buf_append_buf (&out, &line);
285     w_buf_append_char (&out, '\n');
286     w_buf_free (&line);
287
288     for (;;) {
289         if (w_io_write (out_io, out.buf, w_buf_length (&out)) >= 0) {
290             if (!buffered) {
291                 w_io_flush (out_io);
292             }
293             cursize += w_buf_length (&out);
294             break;
295         }
296
297         w_io_format (w_stderr, "Cannot write to logfile: $E.\n");
298         safe_sleep (5);
299     }
300     w_buf_free (&out);
301 }
302
303
304 static void
305 close_log (void)
306 {
307     flush_line ();
308
309     for (;;) {
310         if (w_io_close (out_io)) {
311             w_obj_unref (out_io);
312             out_io = NULL;
313             break;
314         }
315
316         w_io_format (w_stderr,
317                      "Unable to close logfile at directory '$s\n",
318                      directory);
319         safe_sleep (5);
320     }
321 }
322
323
324 static void
325 roll_handler (int signum)
326 {
327     w_unused (signum);
328     close_log ();
329 }
330
331
332 static void
333 quit_handler (int signum)
334 {
335     w_unused (signum);
336
337     flush_line ();
338     w_buf_append_buf (&line, &overflow);
339     close_log ();
340     exit (returncode);
341 }
342
343
344 #define HELP_MESSAGE \
345     "Usage: rotlog [OPTIONS] logdir\n"                                 \
346     "Log standard input to a directory of timestamped files\n"         \
347     "which are automatically rotated as-needed.\n"                     \
348
349
350 static const w_opt_t drlog_options[] = {
351     { 1, 'm', "max-files", W_OPT_UINT, &maxfiles,
352         "Maximum number of log files to keep." },
353
354     { 1, 'T', "max-time", time_period_option, &maxtime,
355         "Maximum time to use a log file (suffixes: mhdwMy)." },
356
357     { 1, 's', "max-size", storage_size_option, &maxsize,
358         "Maximum size of each log file (suffixes: kmg)." },
359
360     { 1, 'b', "buffered", W_OPT_BOOL, &buffered,
361         "Buffered operation, do not flush to disk after each line." },
362
363     { 0, 't', "timestamp", W_OPT_BOOL, &timestamp,
364         "Prepend a timestamp in YYYY-MM-DD/HH:MM:SS format to each line." },
365
366     W_OPT_END
367 };
368
369
370 int drlog_main (int argc, char **argv)
371 {
372     struct sigaction sa;
373     char *env_opts = NULL;
374     unsigned consumed;
375
376     if ((env_opts = getenv ("DRLOG_OPTIONS")) != NULL)
377         replace_args_string (env_opts, &argc, &argv);
378
379     consumed = w_opt_parse (drlog_options, NULL, NULL, "logdir-path", argc, argv);
380
381     if (consumed >= (unsigned) argc)
382         w_die ("$s: No log directory path was specified.\n", argv[0]);
383
384     directory = argv[consumed];
385
386     sigemptyset (&sa.sa_mask);
387     sa.sa_flags = 0;
388
389     sa.sa_handler = roll_handler;
390     safe_sigaction ("HUP", SIGHUP, &sa);
391
392     sa.sa_handler = quit_handler;
393     safe_sigaction ("INT", SIGINT, &sa);
394
395     sa.sa_handler = quit_handler;
396     safe_sigaction ("TERM", SIGTERM, &sa);
397
398     for (;;) {
399         ssize_t ret = w_io_read_line (w_stdin, &line, &overflow, 0);
400
401         if (ret == W_IO_ERR) {
402             w_io_format (w_stderr, "Unable to read from standard input: $E.\n");
403             returncode = 1;
404             quit_handler (0);
405         }
406         if (ret == W_IO_EOF)
407             break;
408         flush_line ();
409     }
410
411     quit_handler (0);
412     return 0; /* Keep compiler happy */
413 }
414