log.c 17.2 KB
Newer Older
Roger Dingledine's avatar
Roger Dingledine committed
1
2
/* Copyright (c) 2001 Matej Pfajfar.
 * Copyright (c) 2001-2004, Roger Dingledine.
3
 * Copyright (c) 2004-2007, Roger Dingledine, Nick Mathewson. */
4
5
/* See LICENSE for licensing information */
/* $Id$ */
6
const char log_c_id[] = "$Id$";
Roger Dingledine's avatar
Roger Dingledine committed
7

Nick Mathewson's avatar
Nick Mathewson committed
8
9
10
/**
 * \file log.c
 * \brief Functions to send messages to log files or the console.
11
 **/
Nick Mathewson's avatar
Nick Mathewson committed
12

13
#include "orconfig.h"
14
#include <stdarg.h>
15
#include <assert.h>
16
#include <stdio.h>
17
#include <stdlib.h>
18
#include <string.h>
19
20
21
22
23
24
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#ifdef HAVE_TIME_H
#include <time.h>
#endif
25
26
27
#include "./util.h"
#include "./log.h"

28
29
#include <event.h>

30
31
#define TRUNCATED_STR "[...truncated]"
#define TRUNCATED_STR_LEN 14
32

Nick Mathewson's avatar
Nick Mathewson committed
33
/** Information for a single logfile; only used in log.c */
34
typedef struct logfile_t {
35
  struct logfile_t *next; /**< Next logfile_t in the linked list. */
36
  char *filename; /**< Filename to open. */
37
  FILE *file; /**< Stream to receive log messages. */
Nick Mathewson's avatar
Nick Mathewson committed
38
  int needs_close; /**< Boolean: true if the stream gets closed on shutdown. */
39
  int min_loglevel; /**< Lowest severity level to send to this stream. */
Nick Mathewson's avatar
Nick Mathewson committed
40
  int max_loglevel; /**< Highest severity level to send to this stream. */
41
  int is_temporary; /**< Boolean: close after initializing logging subsystem.*/
42
  int is_syslog; /**< Boolean: send messages to syslog. */
Nick Mathewson's avatar
Nick Mathewson committed
43
  log_callback callback; /**< If not NULL, send messages to this function. */
44
45
} logfile_t;

46
/** Helper: map a log severity to descriptive string. */
47
48
49
static INLINE const char *
sev_to_string(int severity)
{
50
  switch (severity) {
51
52
    case LOG_DEBUG:   return "debug";
    case LOG_INFO:    return "info";
Roger Dingledine's avatar
Roger Dingledine committed
53
    case LOG_NOTICE:  return "notice";
Roger Dingledine's avatar
Roger Dingledine committed
54
    case LOG_WARN:    return "warn";
55
    case LOG_ERR:     return "err";
56
    default:          assert(0); return "UNKNOWN";
57
58
59
  }
}

60
/** Helper: decide whether to include the function name in the log message. */
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
static INLINE int
should_log_function_name(uint32_t domain, int severity)
{
  switch (severity) {
    case LOG_DEBUG:
    case LOG_INFO:
      /* All debugging messages occur in interesting places. */
      return 1;
    case LOG_NOTICE:
    case LOG_WARN:
    case LOG_ERR:
      /* We care about places where bugs occur. */
      return (domain == LD_BUG);
    default:
      assert(0); return 0;
  }
}

79
/** Linked list of logfile_t. */
80
static logfile_t *logfiles = NULL;
81
82
83
#ifdef HAVE_SYSLOG_H
static int syslog_count = 0;
#endif
84

85
86
87
/* What's the lowest log level anybody cares about? */
int _log_global_min_severity = LOG_NOTICE;

Roger Dingledine's avatar
Roger Dingledine committed
88
static void delete_log(logfile_t *victim);
89
static void close_log(logfile_t *victim);
Roger Dingledine's avatar
Roger Dingledine committed
90

91
92
93
/** Helper: Write the standard prefix for log lines to a
 * <b>buf_len</b> character buffer in <b>buf</b>.
 */
Roger Dingledine's avatar
Roger Dingledine committed
94
static INLINE size_t
95
_log_prefix(char *buf, size_t buf_len, int severity)
Roger Dingledine's avatar
Roger Dingledine committed
96
{
97
  time_t t;
98
  struct timeval now;
99
  struct tm tm;
100
  size_t n;
101
  int r;
102

103
  tor_gettimeofday(&now);
104
  t = (time_t)now.tv_sec;
105

106
  n = strftime(buf, buf_len, "%b %d %H:%M:%S", tor_localtime_r(&t, &tm));
Roger Dingledine's avatar
Roger Dingledine committed
107
  r = tor_snprintf(buf+n, buf_len-n, ".%.3ld [%s] ",
108
                   (long)now.tv_usec / 1000, sev_to_string(severity));
109
110
111
112
  if (r<0)
    return buf_len-1;
  else
    return n+r;
113
114
115
}

/** If lf refers to an actual file that we have just opened, and the file
116
117
118
119
 * contains no data, log an "opening new logfile" message at the top.
 *
 * Return -1 if the log is broken and needs to be deleted, else return 0.
 */
120
121
static int
log_tor_version(logfile_t *lf, int reset)
122
123
124
{
  char buf[256];
  size_t n;
125
  int is_new;
126
127
128

  if (!lf->needs_close)
    /* If it doesn't get closed, it isn't really a file. */
129
    return 0;
130
131
  if (lf->is_temporary)
    /* If it's temporary, it isn't really a file. */
132
    return 0;
133
#ifdef HAVE_FTELLO
134
135
  is_new = (ftello(lf->file) == 0);
#else
136
  is_new = (ftell(lf->file) == 0);
137
#endif
138
139
140
  if (reset && !is_new)
    /* We are resetting, but we aren't at the start of the file; no
     * need to log again. */
141
    return 0;
142
  n = _log_prefix(buf, sizeof(buf), LOG_NOTICE);
143
144
  tor_snprintf(buf+n, sizeof(buf)-n,
               "Tor %s opening %slog file.\n", VERSION, is_new?"new ":"");
145
  if (fputs(buf, lf->file) == EOF ||
146
      fflush(lf->file) == EOF) /* error */
147
148
    return -1; /* failed */
  return 0;
149
150
151
152
}

/** Helper: Format a log message into a fixed-sized buffer. (This is
 * factored out of <b>logv</b> so that we never format a message more
153
154
 * than once.)  Return a pointer to the first character of the message
 * portion of the formatted string.
155
 */
156
157
static INLINE char *
format_msg(char *buf, size_t buf_len,
158
           uint32_t domain, int severity, const char *funcname,
159
           const char *format, va_list ap)
160
161
{
  size_t n;
162
  int r;
163
  char *end_of_prefix;
164
165

  tor_assert(buf_len >= 2); /* prevent integer underflow */
166
167
  buf_len -= 2; /* subtract 2 characters so we have room for \n\0 */

168
  n = _log_prefix(buf, buf_len, severity);
169
  end_of_prefix = buf+n;
170

171
  if (funcname && should_log_function_name(domain, severity)) {
172
173
174
175
176
    r = tor_snprintf(buf+n, buf_len-n, "%s(): ", funcname);
    if (r<0)
      n = strlen(buf);
    else
      n += r;
177
  }
178

179
180
181
182
183
  if (domain == LD_BUG && buf_len-n > 6) {
    memcpy(buf+n, "Bug: ", 6);
    n += 5;
  }

184
  r = tor_vsnprintf(buf+n,buf_len-n,format,ap);
185
  if (r < 0) {
186
187
188
    /* The message was too long; overwrite the end of the buffer with
     * "[...truncated]" */
    if (buf_len >= TRUNCATED_STR_LEN) {
189
190
191
192
      int offset = buf_len-TRUNCATED_STR_LEN;
      /* We have an extra 2 characters after buf_len to hold the \n\0,
       * so it's safe to add 1 to the size here. */
      strlcpy(buf+offset, TRUNCATED_STR, buf_len-offset+1);
193
194
195
196
    }
    /* Set 'n' to the end of the buffer, where we'll be writing \n\0.
     * Since we already subtracted 2 from buf_len, this is safe.*/
    n = buf_len;
197
198
  } else {
    n += r;
199
  }
200
201
  buf[n]='\n';
  buf[n+1]='\0';
202
  return end_of_prefix;
203
204
}

Nick Mathewson's avatar
Nick Mathewson committed
205
206
/** Helper: sends a message to the appropriate logfiles, at loglevel
 * <b>severity</b>.  If provided, <b>funcname</b> is prepended to the
207
 * message.  The actual message is derived as from tor_snprintf(format,ap).
Nick Mathewson's avatar
Nick Mathewson committed
208
 */
209
static void
210
logv(int severity, uint32_t domain, const char *funcname, const char *format,
211
     va_list ap)
212
{
213
  char buf[10024];
214
215
  int formatted = 0;
  logfile_t *lf;
216
  char *end_of_prefix=NULL;
217

218
  assert(format);
219
  lf = logfiles;
220
  while (lf) {
221
    if (severity > lf->min_loglevel || severity < lf->max_loglevel) {
222
      lf = lf->next;
223
      continue;
224
    }
225
    if (! (lf->file || lf->is_syslog || lf->callback)) {
226
      lf = lf->next;
227
      continue;
228
    }
229

230
    if (!formatted) {
231
      end_of_prefix =
232
        format_msg(buf, sizeof(buf), domain, severity, funcname, format, ap);
233
234
      formatted = 1;
    }
235
236
    if (lf->is_syslog) {
#ifdef HAVE_SYSLOG_H
237
238
      /* XXXX Some syslog implementations have scary limits on the length of
       * what you can pass them.  Can/should we detect this? */
239
240
241
242
      syslog(severity, "%s", end_of_prefix);
#endif
      lf = lf->next;
      continue;
243
    } else if (lf->callback) {
244
      lf->callback(severity, domain, end_of_prefix);
245
246
      lf = lf->next;
      continue;
247
    }
248
    if (fputs(buf, lf->file) == EOF ||
249
        fflush(lf->file) == EOF) { /* error */
250
251
252
      /* don't log the error! Blow away this log entry and continue. */
      logfile_t *victim = lf;
      lf = victim->next;
Roger Dingledine's avatar
Roger Dingledine committed
253
      delete_log(victim);
254
255
    } else {
      lf = lf->next;
Roger Dingledine's avatar
Roger Dingledine committed
256
    }
257
  }
258
}
259

Nick Mathewson's avatar
Nick Mathewson committed
260
/** Output a message to the log. */
261
void
262
_log(int severity, uint32_t domain, const char *format, ...)
263
264
265
{
  va_list ap;
  va_start(ap,format);
266
  logv(severity, domain, NULL, format, ap);
267
  va_end(ap);
Roger Dingledine's avatar
Roger Dingledine committed
268
}
269

270
/** Output a message to the log, prefixed with a function name <b>fn</b>. */
271
#ifdef __GNUC__
272
void
273
_log_fn(int severity, uint32_t domain, const char *fn, const char *format, ...)
274
275
276
{
  va_list ap;
  va_start(ap,format);
277
  logv(severity, domain, fn, format, ap);
278
279
  va_end(ap);
}
280
281
#else
const char *_log_fn_function_name=NULL;
282
void
283
_log_fn(int severity, uint32_t domain, const char *format, ...)
284
285
286
{
  va_list ap;
  va_start(ap,format);
287
288
289
290
291
  logv(severity, domain, _log_fn_function_name, format, ap);
  va_end(ap);
  _log_fn_function_name = NULL;
}
void
292
_log_debug(uint32_t domain, const char *format, ...)
293
294
295
{
  va_list ap;
  va_start(ap,format);
296
  logv(LOG_DEBUG, domain, _log_fn_function_name, format, ap);
297
298
299
300
  va_end(ap);
  _log_fn_function_name = NULL;
}
void
301
_log_info(uint32_t domain, const char *format, ...)
302
303
304
{
  va_list ap;
  va_start(ap,format);
305
  logv(LOG_INFO, domain, _log_fn_function_name, format, ap);
306
307
308
309
  va_end(ap);
  _log_fn_function_name = NULL;
}
void
310
_log_notice(uint32_t domain, const char *format, ...)
311
312
313
{
  va_list ap;
  va_start(ap,format);
314
  logv(LOG_NOTICE, domain, _log_fn_function_name, format, ap);
315
316
317
318
  va_end(ap);
  _log_fn_function_name = NULL;
}
void
319
_log_warn(uint32_t domain, const char *format, ...)
320
321
322
{
  va_list ap;
  va_start(ap,format);
323
  logv(LOG_WARN, domain, _log_fn_function_name, format, ap);
324
325
326
327
  va_end(ap);
  _log_fn_function_name = NULL;
}
void
328
_log_err(uint32_t domain, const char *format, ...)
329
330
331
{
  va_list ap;
  va_start(ap,format);
332
  logv(LOG_ERR, domain, _log_fn_function_name, format, ap);
333
334
335
336
  va_end(ap);
  _log_fn_function_name = NULL;
}
#endif
337

338
/** Close all open log files. */
339
340
void
close_logs(void)
341
{
342
343
344
345
346
347
  logfile_t *victim, *next;
  next = logfiles;
  logfiles = NULL;
  while (next) {
    victim = next;
    next = next->next;
348
    close_log(victim);
349
350
    tor_free(victim->filename);
    tor_free(victim);
351
352
  }
}
353

Roger Dingledine's avatar
Roger Dingledine committed
354
/** Remove and free the log entry <b>victim</b> from the linked-list
355
356
357
358
359
360
 * logfiles (it is probably present, but it might not be due to thread
 * racing issues). After this function is called, the caller shouldn't
 * refer to <b>victim</b> anymore.
 *
 * Long-term, we need to do something about races in the log subsystem
 * in general. See bug 222 for more details.
Roger Dingledine's avatar
Roger Dingledine committed
361
 */
362
363
364
static void
delete_log(logfile_t *victim)
{
Roger Dingledine's avatar
Roger Dingledine committed
365
  logfile_t *tmpl;
366
  if (victim == logfiles)
Roger Dingledine's avatar
Roger Dingledine committed
367
368
    logfiles = victim->next;
  else {
369
    for (tmpl = logfiles; tmpl && tmpl->next != victim; tmpl=tmpl->next) ;
370
371
372
373
//    tor_assert(tmpl);
//    tor_assert(tmpl->next == victim);
    if (!tmpl)
      return;
Roger Dingledine's avatar
Roger Dingledine committed
374
375
376
377
378
379
    tmpl->next = victim->next;
  }
  tor_free(victim->filename);
  tor_free(victim);
}

380
381
/** Helper: release system resources (but not memory) held by a single
 * logfile_t. */
382
383
static void
close_log(logfile_t *victim)
384
385
386
387
388
{
  if (victim->needs_close && victim->file) {
    fclose(victim->file);
  } else if (victim->is_syslog) {
#ifdef HAVE_SYSLOG_H
389
    if (--syslog_count == 0) {
390
391
      /* There are no other syslogs; close the logging facility. */
      closelog();
392
    }
393
394
395
396
#endif
  }
}

Nick Mathewson's avatar
Nick Mathewson committed
397
398
/** Add a log handler to send all messages of severity <b>loglevel</b>
 * or higher to <b>stream</b>. */
399
void
400
401
add_stream_log(int loglevelMin, int loglevelMax,
               const char *name, FILE *stream)
402
403
{
  logfile_t *lf;
404
  lf = tor_malloc_zero(sizeof(logfile_t));
405
  lf->filename = tor_strdup(name);
406
  lf->min_loglevel = loglevelMin;
407
  lf->max_loglevel = loglevelMax;
408
409
410
  lf->file = stream;
  lf->next = logfiles;
  logfiles = lf;
411
412

  _log_global_min_severity = get_min_log_level();
413
414
}

415
416
417
/** Add a log handler to receive messages during startup (before the real
 * logs are initialized).
 */
418
419
void
add_temp_log(void)
420
{
421
  add_stream_log(LOG_NOTICE, LOG_ERR, "<temp>", stdout);
422
  logfiles->is_temporary = 1;
423
424

  _log_global_min_severity = get_min_log_level();
425
426
}

427
428
429
430
431
/**
 * Add a log handler to send messages of severity between
 * <b>logLevelmin</b> and <b>logLevelMax</b> to the function
 * <b>cb</b>.
 */
432
433
int
add_callback_log(int loglevelMin, int loglevelMax, log_callback cb)
434
435
436
{
  logfile_t *lf;
  lf = tor_malloc_zero(sizeof(logfile_t));
437
  lf->min_loglevel = loglevelMin;
438
  lf->max_loglevel = loglevelMax;
Nick Mathewson's avatar
Nick Mathewson committed
439
  lf->filename = tor_strdup("<callback>");
440
441
442
  lf->callback = cb;
  lf->next = logfiles;
  logfiles = lf;
443
444

  _log_global_min_severity = get_min_log_level();
445
446
447
  return 0;
}

448
449
/** Adjust the configured severity of any logs whose callback function is
 * <b>cb</b>. */
450
451
452
void
change_callback_log_severity(int loglevelMin, int loglevelMax,
                             log_callback cb)
453
454
455
456
{
  logfile_t *lf;
  for (lf = logfiles; lf; lf = lf->next) {
    if (lf->callback == cb) {
457
      lf->min_loglevel = loglevelMin;
458
459
460
      lf->max_loglevel = loglevelMax;
    }
  }
461
462

  _log_global_min_severity = get_min_log_level();
463
464
}

465
/** Close any log handlers added by add_temp_log or marked by mark_logs_temp */
466
467
void
close_temp_logs(void)
468
{
469
470
471
472
  logfile_t *lf, **p;
  for (p = &logfiles; *p; ) {
    if ((*p)->is_temporary) {
      lf = *p;
473
      /* we use *p here to handle the edge case of the head of the list */
474
475
476
477
      *p = (*p)->next;
      close_log(lf);
      tor_free(lf->filename);
      tor_free(lf);
478
    } else {
479
      p = &((*p)->next);
480
481
    }
  }
482
483

  _log_global_min_severity = get_min_log_level();
484
485
}

486
487
488
489
490
491
492
493
494
495
496
/** Make all currently temporary logs (set to be closed by close_temp_logs)
 * live again, and close all non-temporary logs. */
void
rollback_log_changes(void)
{
  logfile_t *lf;
  for (lf = logfiles; lf; lf = lf->next)
    lf->is_temporary = ! lf->is_temporary;
  close_temp_logs();
}

497
/** Configure all log handles to be closed by close_temp_logs */
498
499
void
mark_logs_temp(void)
500
501
502
503
504
505
{
  logfile_t *lf;
  for (lf = logfiles; lf; lf = lf->next)
    lf->is_temporary = 1;
}

Nick Mathewson's avatar
Nick Mathewson committed
506
507
508
/**
 * Add a log handler to send messages to <b>filename</b>. If opening
 * the logfile fails, -1 is returned and errno is set appropriately
509
 * (by fopen).
510
 */
511
512
int
add_file_log(int loglevelMin, int loglevelMax, const char *filename)
513
514
515
{
  FILE *f;
  f = fopen(filename, "a");
516
  if (!f) return -1;
517
  add_stream_log(loglevelMin, loglevelMax, filename, f);
518
  logfiles->needs_close = 1;
519
  if (log_tor_version(logfiles, 0) < 0) {
520
    delete_log(logfiles);
521
  }
522
  _log_global_min_severity = get_min_log_level();
523
  return 0;
524
}
525

526
527
528
529
#ifdef HAVE_SYSLOG_H
/**
 * Add a log handler to send messages to they system log facility.
 */
530
531
int
add_syslog_log(int loglevelMin, int loglevelMax)
532
533
534
{
  logfile_t *lf;
  if (syslog_count++ == 0)
Roger Dingledine's avatar
Roger Dingledine committed
535
    /* This is the first syslog. */
536
    openlog("Tor", LOG_PID | LOG_NDELAY, LOGFACILITY);
537
538

  lf = tor_malloc_zero(sizeof(logfile_t));
539
  lf->min_loglevel = loglevelMin;
540
541
542
543
544
  lf->filename = tor_strdup("<syslog>");
  lf->max_loglevel = loglevelMax;
  lf->is_syslog = 1;
  lf->next = logfiles;
  logfiles = lf;
545
546

  _log_global_min_severity = get_min_log_level();
547
548
549
550
  return 0;
}
#endif

551
552
/** If <b>level</b> is a valid log severity, return the corresponding
 * numeric value.  Otherwise, return -1. */
553
554
555
int
parse_log_level(const char *level)
{
556
557
  if (!strcasecmp(level, "err"))
    return LOG_ERR;
558
559
560
  if (!strcasecmp(level, "warn"))
    return LOG_WARN;
  if (!strcasecmp(level, "notice"))
561
    return LOG_NOTICE;
562
  if (!strcasecmp(level, "info"))
563
    return LOG_INFO;
564
  if (!strcasecmp(level, "debug"))
565
    return LOG_DEBUG;
566
  return -1;
567
568
}

569
/** Return the string equivalent of a given log level. */
570
571
const char *
log_level_to_string(int level)
572
573
574
575
{
  return sev_to_string(level);
}

576
/** Return the least severe log level that any current log is interested in. */
577
578
int
get_min_log_level(void)
579
580
581
582
{
  logfile_t *lf;
  int min = LOG_ERR;
  for (lf = logfiles; lf; lf = lf->next) {
583
584
    if (lf->min_loglevel > min)
      min = lf->min_loglevel;
585
586
587
588
  }
  return min;
}

589
/** Switch all logs to output at most verbose level. */
590
591
void
switch_logs_debug(void)
592
593
594
{
  logfile_t *lf;
  for (lf = logfiles; lf; lf=lf->next) {
595
    lf->min_loglevel = LOG_DEBUG;
596
597
598
  }
}

599
#ifdef HAVE_EVENT_SET_LOG_CALLBACK
600
/** A string which, if it appears in a libevent log, should be ignored. */
601
static const char *suppress_msg = NULL;
602
603
/** Callback function passed to event_set_log() so we can intercept
 * log messages from libevent. */
604
605
static void
libevent_logging_callback(int severity, const char *msg)
606
{
607
608
  char buf[1024];
  size_t n;
609
610
  if (suppress_msg && strstr(msg, suppress_msg))
    return;
611
612
613
614
  n = strlcpy(buf, msg, sizeof(buf));
  if (n && n < sizeof(buf) && buf[n-1] == '\n') {
    buf[n-1] = '\0';
  }
615
616
  switch (severity) {
    case _EVENT_LOG_DEBUG:
617
      log(LOG_DEBUG, LD_NET, "Message from libevent: %s", buf);
618
619
      break;
    case _EVENT_LOG_MSG:
620
      log(LOG_INFO, LD_NET, "Message from libevent: %s", buf);
621
622
      break;
    case _EVENT_LOG_WARN:
623
      log(LOG_WARN, LD_GENERAL, "Warning from libevent: %s", buf);
624
625
      break;
    case _EVENT_LOG_ERR:
626
      log(LOG_ERR, LD_GENERAL, "Error from libevent: %s", buf);
627
628
      break;
    default:
629
630
      log(LOG_WARN, LD_GENERAL, "Message [%d] from libevent: %s",
          severity, buf);
631
632
633
      break;
  }
}
634
/** Set hook to intercept log messages from libevent. */
635
636
void
configure_libevent_logging(void)
637
638
639
{
  event_set_log_callback(libevent_logging_callback);
}
Roger Dingledine's avatar
Roger Dingledine committed
640
/** Ignore any libevent log message that contains <b>msg</b>. */
641
642
void
suppress_libevent_log_msg(const char *msg)
643
644
645
{
  suppress_msg = msg;
}
646
#else
647
648
649
650
651
652
653
void
configure_libevent_logging(void)
{
}
void
suppress_libevent_log_msg(const char *msg)
{
654
  (void)msg;
655
}
656
#endif
657

658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
#if 0
static void
dump_log_info(logfile_t *lf)
{
  const char *tp;

  if (lf->filename) {
    printf("=== log into \"%s\" (%s-%s) (%stemporary)\n", lf->filename,
           sev_to_string(lf->min_loglevel),
           sev_to_string(lf->max_loglevel),
           lf->is_temporary?"":"not ");
  } else if (lf->is_syslog) {
    printf("=== syslog (%s-%s) (%stemporary)\n",
           sev_to_string(lf->min_loglevel),
           sev_to_string(lf->max_loglevel),
           lf->is_temporary?"":"not ");
  } else {
    printf("=== log (%s-%s) (%stemporary)\n",
           sev_to_string(lf->min_loglevel),
           sev_to_string(lf->max_loglevel),
           lf->is_temporary?"":"not ");
  }
}

void
describe_logs(void)
{
  logfile_t *lf;
  printf("==== BEGIN LOGS ====\n");
  for (lf = logfiles; lf; lf = lf->next)
    dump_log_info(lf);
  printf("==== END LOGS ====\n");
}
#endif