Line data Source code
1 : /**
2 : * @file
3 : * Logging Dispatcher
4 : *
5 : * @authors
6 : * Copyright (C) 2018 Richard Russon <rich@flatcap.org>
7 : *
8 : * @copyright
9 : * This program is free software: you can redistribute it and/or modify it under
10 : * the terms of the GNU General Public License as published by the Free Software
11 : * Foundation, either version 2 of the License, or (at your option) any later
12 : * version.
13 : *
14 : * This program is distributed in the hope that it will be useful, but WITHOUT
15 : * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 : * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 : * details.
18 : *
19 : * You should have received a copy of the GNU General Public License along with
20 : * this program. If not, see <http://www.gnu.org/licenses/>.
21 : */
22 :
23 : /**
24 : * @page mutt_logging Logging Dispatcher
25 : *
26 : * Logging Dispatcher
27 : */
28 :
29 : #include "config.h"
30 : #include <errno.h>
31 : #include <stdarg.h>
32 : #include <stdbool.h>
33 : #include <stdio.h>
34 : #include <string.h>
35 : #include <time.h>
36 : #include <unistd.h>
37 : #include "logging.h"
38 : #include "date.h"
39 : #include "file.h"
40 : #include "memory.h"
41 : #include "message.h"
42 : #include "queue.h"
43 : #include "string2.h"
44 :
45 : const char *LevelAbbr = "PEWM12345N"; ///< Abbreviations of logging level names
46 :
47 : /**
48 : * MuttLogger - The log dispatcher - @ingroup logging_api
49 : *
50 : * This function pointer controls where log messages are redirected.
51 : */
52 : log_dispatcher_t MuttLogger = log_disp_terminal;
53 :
54 : FILE *LogFileFP = NULL; ///< Log file handle
55 : char *LogFileName = NULL; ///< Log file name
56 : int LogFileLevel = 0; ///< Log file level
57 : char *LogFileVersion = NULL; ///< Program version
58 :
59 : /**
60 : * LogQueue - In-memory list of log lines
61 : */
62 : static struct LogLineList LogQueue = STAILQ_HEAD_INITIALIZER(LogQueue);
63 :
64 : int LogQueueCount = 0; ///< Number of entries currently in the log queue
65 : int LogQueueMax = 0; ///< Maximum number of entries in the log queue
66 :
67 : /**
68 : * timestamp - Create a YYYY-MM-DD HH:MM:SS timestamp
69 : * @param stamp Unix time
70 : * @retval ptr Timestamp string
71 : *
72 : * If stamp is 0, then the current time will be used.
73 : *
74 : * @note This function returns a pointer to a static buffer.
75 : * Do not free it.
76 : */
77 0 : static const char *timestamp(time_t stamp)
78 : {
79 : static char buf[23] = { 0 };
80 : static time_t last = 0;
81 :
82 0 : if (stamp == 0)
83 0 : stamp = mutt_date_epoch();
84 :
85 0 : if (stamp != last)
86 : {
87 0 : mutt_date_localtime_format(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", stamp);
88 0 : last = stamp;
89 : }
90 :
91 0 : return buf;
92 : }
93 :
94 : /**
95 : * log_file_close - Close the log file
96 : * @param verbose If true, then log the event
97 : */
98 2 : void log_file_close(bool verbose)
99 : {
100 2 : if (!LogFileFP)
101 2 : return;
102 :
103 0 : fprintf(LogFileFP, "[%s] Closing log.\n", timestamp(0));
104 0 : fprintf(LogFileFP, "# vim: syntax=neomuttlog\n");
105 0 : mutt_file_fclose(&LogFileFP);
106 0 : if (verbose)
107 0 : mutt_message(_("Closed log file: %s"), LogFileName);
108 : }
109 :
110 : /**
111 : * log_file_open - Start logging to a file
112 : * @param verbose If true, then log the event
113 : * @retval 0 Success
114 : * @retval -1 Error, see errno
115 : *
116 : * Before opening a log file, call log_file_set_version(), log_file_set_level()
117 : * and log_file_set_filename().
118 : */
119 2 : int log_file_open(bool verbose)
120 : {
121 2 : if (!LogFileName)
122 2 : return -1;
123 :
124 0 : if (LogFileFP)
125 0 : log_file_close(false);
126 :
127 0 : if (LogFileLevel < LL_DEBUG1)
128 0 : return -1;
129 :
130 0 : LogFileFP = mutt_file_fopen(LogFileName, "a+");
131 0 : if (!LogFileFP)
132 0 : return -1;
133 0 : setvbuf(LogFileFP, NULL, _IOLBF, 0);
134 :
135 0 : fprintf(LogFileFP, "[%s] NeoMutt%s debugging at level %d\n", timestamp(0),
136 0 : NONULL(LogFileVersion), LogFileLevel);
137 0 : if (verbose)
138 0 : mutt_message(_("Debugging at level %d to file '%s'"), LogFileLevel, LogFileName);
139 0 : return 0;
140 : }
141 :
142 : /**
143 : * log_file_set_filename - Set the filename for the log
144 : * @param file Name to use
145 : * @param verbose If true, then log the event
146 : * @retval 0 Success, file opened
147 : * @retval -1 Error, see errno
148 : */
149 2 : int log_file_set_filename(const char *file, bool verbose)
150 : {
151 2 : if (!file)
152 2 : return -1;
153 :
154 : /* also handles both being NULL */
155 0 : if (mutt_str_equal(LogFileName, file))
156 0 : return 0;
157 :
158 0 : mutt_str_replace(&LogFileName, file);
159 :
160 0 : if (!LogFileName)
161 0 : log_file_close(verbose);
162 :
163 0 : return log_file_open(verbose);
164 : }
165 :
166 : /**
167 : * log_file_set_level - Set the logging level
168 : * @param level Logging level
169 : * @param verbose If true, then log the event
170 : * @retval 0 Success
171 : * @retval -1 Error, level is out of range
172 : *
173 : * The level should be: LL_MESSAGE <= level < LL_MAX.
174 : */
175 2 : int log_file_set_level(enum LogLevel level, bool verbose)
176 : {
177 2 : if ((level < LL_MESSAGE) || (level >= LL_MAX))
178 2 : return -1;
179 :
180 0 : if (level == LogFileLevel)
181 0 : return 0;
182 :
183 0 : LogFileLevel = level;
184 :
185 0 : if (level == LL_MESSAGE)
186 : {
187 0 : log_file_close(verbose);
188 : }
189 0 : else if (LogFileFP)
190 : {
191 0 : if (verbose)
192 0 : mutt_message(_("Logging at level %d to file '%s'"), LogFileLevel, LogFileName);
193 0 : fprintf(LogFileFP, "[%s] NeoMutt%s debugging at level %d\n", timestamp(0),
194 0 : NONULL(LogFileVersion), LogFileLevel);
195 : }
196 : else
197 : {
198 0 : log_file_open(verbose);
199 : }
200 :
201 0 : if (LogFileLevel >= LL_DEBUG5)
202 : {
203 0 : fprintf(LogFileFP,
204 : "\n"
205 : "WARNING:\n"
206 : " Logging at this level can reveal personal information.\n"
207 : " Review the log carefully before posting in bug reports.\n"
208 : "\n");
209 : }
210 :
211 0 : return 0;
212 : }
213 :
214 : /**
215 : * log_file_set_version - Set the program's version number
216 : * @param version Version number
217 : *
218 : * The string will be appended directly to 'NeoMutt', so it should begin with a
219 : * hyphen.
220 : */
221 2 : void log_file_set_version(const char *version)
222 : {
223 2 : mutt_str_replace(&LogFileVersion, version);
224 2 : }
225 :
226 : /**
227 : * log_file_running - Is the log file running?
228 : * @retval true The log file is running
229 : */
230 2 : bool log_file_running(void)
231 : {
232 2 : return LogFileFP;
233 : }
234 :
235 : /**
236 : * log_disp_file - Save a log line to a file - Implements ::log_dispatcher_t - @ingroup logging_api
237 : *
238 : * This log dispatcher saves a line of text to a file. The format is:
239 : * * `[TIMESTAMP]<LEVEL> FUNCTION() FORMATTED-MESSAGE`
240 : *
241 : * The caller must first set #LogFileName and #LogFileLevel, then call
242 : * log_file_open(). Any logging above #LogFileLevel will be ignored.
243 : *
244 : * If stamp is 0, then the current time will be used.
245 : */
246 4454 : int log_disp_file(time_t stamp, const char *file, int line,
247 : const char *function, enum LogLevel level, ...)
248 : {
249 4454 : if (!LogFileFP || (level < LL_PERROR) || (level > LogFileLevel))
250 4454 : return 0;
251 :
252 0 : int ret = 0;
253 0 : int err = errno;
254 :
255 0 : if (!function)
256 0 : function = "UNKNOWN";
257 :
258 0 : ret += fprintf(LogFileFP, "[%s]<%c> %s() ", timestamp(stamp),
259 0 : LevelAbbr[level + 3], function);
260 :
261 : va_list ap;
262 0 : va_start(ap, level);
263 0 : const char *fmt = va_arg(ap, const char *);
264 0 : ret += vfprintf(LogFileFP, fmt, ap);
265 0 : va_end(ap);
266 :
267 0 : if (level == LL_PERROR)
268 : {
269 0 : fprintf(LogFileFP, ": %s\n", strerror(err));
270 : }
271 0 : else if (level <= LL_MESSAGE)
272 : {
273 0 : fputs("\n", LogFileFP);
274 0 : ret++;
275 : }
276 :
277 0 : return ret;
278 : }
279 :
280 : /**
281 : * log_queue_add - Add a LogLine to the queue
282 : * @param ll LogLine to add
283 : * @retval num Entries in the queue
284 : *
285 : * If #LogQueueMax is non-zero, the queue will be limited to this many items.
286 : */
287 6 : int log_queue_add(struct LogLine *ll)
288 : {
289 6 : if (!ll)
290 2 : return -1;
291 :
292 4 : STAILQ_INSERT_TAIL(&LogQueue, ll, entries);
293 :
294 4 : if ((LogQueueMax > 0) && (LogQueueCount >= LogQueueMax))
295 : {
296 0 : ll = STAILQ_FIRST(&LogQueue);
297 0 : STAILQ_REMOVE_HEAD(&LogQueue, entries);
298 0 : FREE(&ll->message);
299 0 : FREE(&ll);
300 : }
301 : else
302 : {
303 4 : LogQueueCount++;
304 : }
305 4 : return LogQueueCount;
306 : }
307 :
308 : /**
309 : * log_queue_set_max_size - Set a upper limit for the queue length
310 : * @param size New maximum queue length
311 : *
312 : * @note size of 0 means unlimited
313 : */
314 2 : void log_queue_set_max_size(int size)
315 : {
316 2 : if (size < 0)
317 2 : size = 0;
318 2 : LogQueueMax = size;
319 2 : }
320 :
321 : /**
322 : * log_queue_empty - Free the contents of the queue
323 : *
324 : * Free any log lines in the queue.
325 : */
326 4 : void log_queue_empty(void)
327 : {
328 4 : struct LogLine *ll = NULL;
329 4 : struct LogLine *tmp = NULL;
330 :
331 8 : STAILQ_FOREACH_SAFE(ll, &LogQueue, entries, tmp)
332 : {
333 4 : STAILQ_REMOVE(&LogQueue, ll, LogLine, entries);
334 4 : FREE(&ll->message);
335 4 : FREE(&ll);
336 : }
337 :
338 4 : LogQueueCount = 0;
339 4 : }
340 :
341 : /**
342 : * log_queue_flush - Replay the log queue
343 : * @param disp Log dispatcher - Implements ::log_dispatcher_t
344 : *
345 : * Pass all of the log entries in the queue to the log dispatcher provided.
346 : * The queue will be emptied afterwards.
347 : */
348 2 : void log_queue_flush(log_dispatcher_t disp)
349 : {
350 2 : struct LogLine *ll = NULL;
351 2 : STAILQ_FOREACH(ll, &LogQueue, entries)
352 : {
353 0 : disp(ll->time, ll->file, ll->line, ll->function, ll->level, "%s", ll->message);
354 : }
355 :
356 2 : log_queue_empty();
357 2 : }
358 :
359 : /**
360 : * log_queue_save - Save the contents of the queue to a temporary file
361 : * @param fp Open file handle
362 : * @retval num Lines written to the file
363 : *
364 : * The queue is written to a temporary file. The format is:
365 : * * `[HH:MM:SS]<LEVEL> FORMATTED-MESSAGE`
366 : *
367 : * @note The caller should delete the file
368 : */
369 2 : int log_queue_save(FILE *fp)
370 : {
371 2 : if (!fp)
372 2 : return 0;
373 :
374 : char buf[32];
375 0 : int count = 0;
376 0 : struct LogLine *ll = NULL;
377 0 : STAILQ_FOREACH(ll, &LogQueue, entries)
378 : {
379 0 : mutt_date_localtime_format(buf, sizeof(buf), "%H:%M:%S", ll->time);
380 0 : fprintf(fp, "[%s]<%c> %s", buf, LevelAbbr[ll->level + 3], ll->message);
381 0 : if (ll->level <= LL_MESSAGE)
382 0 : fputs("\n", fp);
383 0 : count++;
384 : }
385 :
386 0 : return count;
387 : }
388 :
389 : /**
390 : * log_disp_queue - Save a log line to an internal queue - Implements ::log_dispatcher_t - @ingroup logging_api
391 : *
392 : * This log dispatcher saves a line of text to a queue.
393 : * The format string and parameters are expanded and the other parameters are
394 : * stored as they are.
395 : *
396 : * @sa log_queue_set_max_size(), log_queue_flush(), log_queue_empty()
397 : *
398 : * @warning Log lines are limited to 1024 bytes.
399 : */
400 4 : int log_disp_queue(time_t stamp, const char *file, int line,
401 : const char *function, enum LogLevel level, ...)
402 : {
403 4 : char buf[1024] = { 0 };
404 4 : int err = errno;
405 :
406 : va_list ap;
407 4 : va_start(ap, level);
408 4 : const char *fmt = va_arg(ap, const char *);
409 4 : int ret = vsnprintf(buf, sizeof(buf), fmt, ap);
410 4 : va_end(ap);
411 :
412 4 : if (level == LL_PERROR)
413 : {
414 0 : if ((ret >= 0) && (ret < sizeof(buf)))
415 0 : ret += snprintf(buf + ret, sizeof(buf) - ret, ": %s", strerror(err));
416 0 : level = LL_ERROR;
417 : }
418 :
419 4 : struct LogLine *ll = mutt_mem_calloc(1, sizeof(*ll));
420 4 : ll->time = (stamp != 0) ? stamp : mutt_date_epoch();
421 4 : ll->file = file;
422 4 : ll->line = line;
423 4 : ll->function = function;
424 4 : ll->level = level;
425 4 : ll->message = mutt_str_dup(buf);
426 :
427 4 : log_queue_add(ll);
428 :
429 4 : return ret;
430 : }
431 :
432 : /**
433 : * log_disp_terminal - Save a log line to the terminal - Implements ::log_dispatcher_t - @ingroup logging_api
434 : *
435 : * This log dispatcher saves a line of text to the terminal.
436 : * The format is:
437 : * * `[TIMESTAMP]<LEVEL> FUNCTION() FORMATTED-MESSAGE`
438 : *
439 : * @note The output will be coloured using ANSI escape sequences,
440 : * unless the output is redirected.
441 : */
442 4450 : int log_disp_terminal(time_t stamp, const char *file, int line,
443 : const char *function, enum LogLevel level, ...)
444 : {
445 : char buf[1024];
446 :
447 : va_list ap;
448 4450 : va_start(ap, level);
449 4450 : const char *fmt = va_arg(ap, const char *);
450 4450 : int ret = vsnprintf(buf, sizeof(buf), fmt, ap);
451 4450 : va_end(ap);
452 :
453 4450 : log_disp_file(stamp, file, line, function, level, "%s", buf);
454 :
455 4450 : if ((level < LL_PERROR) || (level > LL_MESSAGE))
456 4446 : return 0;
457 :
458 4 : FILE *fp = (level < LL_MESSAGE) ? stderr : stdout;
459 4 : int err = errno;
460 4 : int colour = 0;
461 4 : bool tty = (isatty(fileno(fp)) == 1);
462 :
463 4 : if (tty)
464 : {
465 4 : switch (level)
466 : {
467 0 : case LL_PERROR:
468 : case LL_ERROR:
469 0 : colour = 31;
470 0 : break;
471 0 : case LL_WARNING:
472 0 : colour = 33;
473 0 : break;
474 4 : case LL_MESSAGE:
475 : // colour = 36;
476 4 : break;
477 0 : default:
478 0 : break;
479 : }
480 : }
481 :
482 4 : if (colour > 0)
483 0 : ret += fprintf(fp, "\033[1;%dm", colour); // Escape
484 :
485 4 : fputs(buf, fp);
486 :
487 4 : if (level == LL_PERROR)
488 0 : ret += fprintf(fp, ": %s", strerror(err));
489 :
490 4 : if (colour > 0)
491 0 : ret += fprintf(fp, "\033[0m"); // Escape
492 :
493 4 : ret += fprintf(fp, "\n");
494 :
495 4 : return ret;
496 : }
497 :
498 : /**
499 : * log_disp_null - Discard log lines - Implements ::log_dispatcher_t - @ingroup logging_api
500 : */
501 22 : int log_disp_null(time_t stamp, const char *file, int line,
502 : const char *function, enum LogLevel level, ...)
503 : {
504 22 : return 0;
505 : }
|