src/log.c: Avoid recursive logging.
[srvx.git] / src / log.c
1 /* log.c - Diagnostic and error logging
2  * Copyright 2000-2004 srvx Development Team
3  *
4  * This file is part of srvx.
5  *
6  * srvx is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with srvx; if not, write to the Free Software Foundation,
18  * Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
19  */
20
21 #include "conf.h"
22 #include "log.h"
23 #include "helpfile.h" /* send_message, message_register, etc */
24 #include "nickserv.h"
25
26 struct logDestination;
27
28 struct logDest_vtable {
29     const char *type_name;
30     struct logDestination* (*open)(const char *args);
31     void (*reopen)(struct logDestination *self);
32     void (*close)(struct logDestination *self);
33     void (*log_audit)(struct logDestination *self, struct log_type *type, struct logEntry *entry);
34     void (*log_replay)(struct logDestination *self, struct log_type *type, int is_write, const char *line);
35     void (*log_module)(struct logDestination *self, struct log_type *type, enum log_severity sev, const char *message);
36 };
37
38 struct logDestination {
39     struct logDest_vtable *vtbl;
40     char *name;
41     int refcnt;
42 };
43
44 DECLARE_LIST(logList, struct logDestination*);
45
46 struct log_type {
47     char *name;
48     struct logList logs[LOG_NUM_SEVERITIES];
49     struct logEntry *log_oldest;
50     struct logEntry *log_newest;
51     unsigned int log_count;
52     unsigned int max_age;
53     unsigned int max_count;
54     unsigned int depth;
55     unsigned int default_set : 1;
56 };
57
58 static const char *log_severity_names[] = {
59     "replay",   /* 0 */
60     "debug",
61     "command",
62     "info",
63     "override",
64     "staff",    /* 5 */
65     "warning",
66     "error",
67     "fatal",
68     0
69 };
70
71 static struct dict *log_dest_types;
72 static struct dict *log_dests;
73 static struct dict *log_types;
74 static struct log_type *log_default;
75 static int log_inited, log_debugged;
76
77 DEFINE_LIST(logList, struct logDestination*)
78 static void log_format_audit(struct logEntry *entry);
79 static const struct message_entry msgtab[] = {
80     { "MSG_INVALID_FACILITY", "$b%s$b is an invalid log facility." },
81     { "MSG_INVALID_SEVERITY", "$b%s$b is an invalid severity level." },
82     { NULL, NULL }
83 };
84
85 static struct logDestination *
86 log_open(const char *name)
87 {
88     struct logDest_vtable *vtbl;
89     struct logDestination *ld;
90     char *sep;
91     char type_name[32];
92
93     if ((ld = dict_find(log_dests, name, NULL))) {
94         ld->refcnt++;
95         return ld;
96     }
97     if ((sep = strchr(name, ':'))) {
98         memcpy(type_name, name, sep-name);
99         type_name[sep-name] = 0;
100     } else {
101         strcpy(type_name, name);
102     }
103     if (!(vtbl = dict_find(log_dest_types, type_name, NULL))) {
104         log_module(MAIN_LOG, LOG_ERROR, "Invalid log type for log '%s'.", name);
105         return 0;
106     }
107     if (!(ld = vtbl->open(sep ? sep+1 : 0)))
108         return 0;
109     ld->name = strdup(name);
110     dict_insert(log_dests, ld->name, ld);
111     ld->refcnt = 1;
112     return ld;
113 }
114
115 static void
116 logList_open(struct logList *ll, struct record_data *rd)
117 {
118     struct logDestination *ld;
119     unsigned int ii;
120
121     if (!ll->size)
122         logList_init(ll);
123     switch (rd->type) {
124     case RECDB_QSTRING:
125         if ((ld = log_open(rd->d.qstring)))
126             logList_append(ll, ld);
127         break;
128     case RECDB_STRING_LIST:
129         for (ii=0; ii<rd->d.slist->used; ++ii) {
130             if ((ld = log_open(rd->d.slist->list[ii])))
131                 logList_append(ll, ld);
132         }
133         break;
134     default:
135         break;
136     }
137 }
138
139 static void
140 logList_join(struct logList *target, const struct logList *source)
141 {
142     unsigned int ii, jj, kk;
143
144     if (!source->used)
145         return;
146     jj = target->used;
147     target->used += source->used;
148     target->size += source->used;
149     target->list = realloc(target->list, target->size * sizeof(target->list[0]));
150     for (ii = 0; ii < source->used; ++ii, ++jj) {
151         int dup;
152         for (dup = 0, kk = 0; kk < jj; kk++) {
153             if (target->list[kk] == source->list[ii]) {
154                 dup = 1;
155                 break;
156             }
157         }
158         if (dup) {
159             jj--;
160             target->used--;
161             continue;
162         }
163         target->list[jj] = source->list[ii];
164         target->list[jj]->refcnt++;
165     }
166 }
167
168 static void
169 logList_close(struct logList *ll)
170 {
171     unsigned int ii;
172     for (ii=0; ii<ll->used; ++ii) {
173         if (!--ll->list[ii]->refcnt) {
174             struct logDestination *ld = ll->list[ii];
175             ld->vtbl->close(ld);
176         }
177     }
178     logList_clean(ll);
179 }
180
181 static void
182 close_logs(void)
183 {
184     dict_iterator_t it;
185     struct log_type *lt;
186     enum log_severity ls;
187
188     for (it = dict_first(log_types); it; it = iter_next(it)) {
189         lt = iter_data(it);
190         for (ls = 0; ls < LOG_NUM_SEVERITIES; ls++) {
191             logList_close(&lt->logs[ls]);
192
193             lt->logs[ls].size = 0;
194             lt->logs[ls].used = 0;
195             lt->logs[ls].list = 0;
196         }
197     }
198 }
199
200 static void
201 log_type_free_oldest(struct log_type *lt)
202 {
203     struct logEntry *next;
204
205     if (!lt->log_oldest)
206         return;
207     next = lt->log_oldest->next;
208     free(lt->log_oldest->default_desc);
209     free(lt->log_oldest);
210     lt->log_oldest = next;
211     lt->log_count--;
212 }
213
214 static void
215 log_type_free(void *ptr)
216 {
217     struct log_type *lt = ptr;
218
219     while (lt->log_oldest)
220         log_type_free_oldest(lt);
221     free(lt);
222 }
223
224 static void
225 cleanup_logs(void)
226 {
227
228     close_logs();
229     dict_delete(log_types);
230     dict_delete(log_dests);
231     dict_delete(log_dest_types);
232 }
233
234 static enum log_severity
235 find_severity(const char *text)
236 {
237     enum log_severity ls;
238     for (ls = 0; ls < LOG_NUM_SEVERITIES; ++ls)
239         if (!ircncasecmp(text, log_severity_names[ls], strlen(log_severity_names[ls])))
240             return ls;
241     return LOG_NUM_SEVERITIES;
242 }
243
244 /* Log keys are based on syslog.conf syntax:
245  *   KEY := LOGSET '.' SEVSET
246  *   LOGSET := LOGLIT | LOGLIT ',' LOGSET
247  *   LOGLIT := a registered log type
248  *   SEVSET := '*' | SEVLIT | '<' SEVLIT | '<=' SEVLIT | '>' SEVLIT | '>=' SEVLIT | SEVLIT ',' SEVSET
249  *   SEVLIT := one of log_severity_names
250  * A KEY contains the Cartesian product of the logs in its LOGSET
251  * and the severities in its SEVSET.
252  */
253
254 static void
255 log_parse_logset(char *buffer, struct string_list *slist)
256 {
257     slist->used = 0;
258     while (buffer) {
259         char *cont = strchr(buffer, ',');
260         if (cont)
261             *cont++ = 0;
262         string_list_append(slist, strdup(buffer));
263         buffer = cont;
264     }
265 }
266
267 static void
268 log_parse_sevset(char *buffer, char targets[LOG_NUM_SEVERITIES])
269 {
270     memset(targets, 0, LOG_NUM_SEVERITIES);
271     while (buffer) {
272         char *cont;
273         enum log_severity bound;
274         int first;
275
276         cont = strchr(buffer, ',');
277         if (cont)
278             *cont++ = 0;
279         if (buffer[0] == '*' && buffer[1] == 0) {
280             for (bound = 0; bound < LOG_NUM_SEVERITIES; bound++) {
281                 /* make people explicitly specify replay targets */
282                 if (bound != LOG_REPLAY)
283                     targets[bound] = 1;
284             }
285         } else if (buffer[0] == '<') {
286             if (buffer[1] == '=')
287                 bound = find_severity(buffer+2) + 1;
288             else
289                 bound = find_severity(buffer+1);
290             for (first = 1; bound > 0; bound--) {
291                 /* make people explicitly specify replay targets */
292                 if (bound != LOG_REPLAY || first) {
293                     targets[bound] = 1;
294                     first = 0;
295                 }
296             }
297         } else if (buffer[0] == '>') {
298             if (buffer[1] == '=')
299                 bound = find_severity(buffer+2);
300             else
301                 bound = find_severity(buffer+1) + 1;
302             for (first = 1; bound < LOG_NUM_SEVERITIES; bound++) {
303                 /* make people explicitly specify replay targets */
304                 if (bound != LOG_REPLAY || first) {
305                     targets[bound] = 1;
306                     first = 0;
307                 }
308             }
309         } else {
310             if (buffer[0] == '=')
311                 buffer++;
312             bound = find_severity(buffer);
313             targets[bound] = 1;
314         }
315         buffer = cont;
316     }
317 }
318
319 static void
320 log_parse_cross(const char *buffer, struct string_list *types, char sevset[LOG_NUM_SEVERITIES])
321 {
322     char *dup, *sep;
323
324     dup = strdup(buffer);
325     sep = strchr(dup, '.');
326     *sep++ = 0;
327     log_parse_logset(dup, types);
328     log_parse_sevset(sep, sevset);
329     free(dup);
330 }
331
332 static void
333 log_parse_options(struct log_type *type, struct dict *conf)
334 {
335     const char *opt;
336     opt = database_get_data(conf, "max_age", RECDB_QSTRING);
337     if (opt)
338         type->max_age = ParseInterval(opt);
339     opt = database_get_data(conf, "max_count", RECDB_QSTRING);
340     if (opt)
341         type->max_count = strtoul(opt, NULL, 10);
342 }
343
344 static void
345 log_conf_read(void)
346 {
347     struct record_data *rd, *rd2;
348     dict_iterator_t it;
349     const char *sep;
350     struct log_type *type;
351     enum log_severity sev;
352     unsigned int ii;
353
354     close_logs();
355     dict_delete(log_dests);
356
357     log_dests = dict_new();
358     dict_set_free_keys(log_dests, free);
359
360     rd = conf_get_node("logs");
361     if (rd && (rd->type == RECDB_OBJECT)) {
362         for (it = dict_first(rd->d.object); it; it = iter_next(it)) {
363             if ((sep = strchr(iter_key(it), '.'))) {
364                 struct logList logList;
365                 char sevset[LOG_NUM_SEVERITIES];
366                 struct string_list *slist;
367
368                 /* It looks like a <type>.<severity> record.  Try to parse it. */
369                 slist = alloc_string_list(4);
370                 log_parse_cross(iter_key(it), slist, sevset);
371                 logList.size = 0;
372                 logList_open(&logList, iter_data(it));
373                 for (ii = 0; ii < slist->used; ++ii) {
374                     type = log_register_type(slist->list[ii], NULL);
375                     for (sev = 0; sev < LOG_NUM_SEVERITIES; ++sev) {
376                         if (!sevset[sev])
377                             continue;
378                         logList_join(&type->logs[sev], &logList);
379                     }
380                 }
381                 logList_close(&logList);
382                 free_string_list(slist);
383             } else if ((rd2 = iter_data(it))
384                        && (rd2->type == RECDB_OBJECT)
385                        && (type = log_register_type(iter_key(it), NULL))) {
386                 log_parse_options(type, rd2->d.object);
387             } else {
388                 log_module(MAIN_LOG, LOG_ERROR, "Unknown logs subkey '%s'.", iter_key(it));
389             }
390         }
391     }
392     if (log_debugged)
393         log_debug();
394 }
395
396 void
397 log_debug(void)
398 {
399     enum log_severity sev;
400     struct logDestination *log_stdout;
401     struct logList target;
402
403     log_stdout = log_open("std:out");
404     logList_init(&target);
405     logList_append(&target, log_stdout);
406
407     for (sev = 0; sev < LOG_NUM_SEVERITIES; ++sev)
408         logList_join(&log_default->logs[sev], &target);
409
410     logList_close(&target);
411     log_debugged = 1;
412 }
413
414 void
415 log_reopen(void)
416 {
417     dict_iterator_t it;
418     for (it = dict_first(log_dests); it; it = iter_next(it)) {
419         struct logDestination *ld = iter_data(it);
420         ld->vtbl->reopen(ld);
421     }
422 }
423
424 struct log_type *
425 log_register_type(const char *name, const char *default_log)
426 {
427     struct log_type *type;
428     struct logDestination *dest;
429     enum log_severity sev;
430
431     if (!(type = dict_find(log_types, name, NULL))) {
432         type = calloc(1, sizeof(*type));
433         type->name = strdup(name);
434         type->max_age = 600;
435         type->max_count = 1024;
436         dict_insert(log_types, type->name, type);
437     }
438     if (default_log && !type->default_set) {
439         /* If any severity level was unspecified in the config, use the default. */
440         dest = NULL;
441         for (sev = 0; sev < LOG_NUM_SEVERITIES; ++sev) {
442             if (sev == LOG_REPLAY)
443                 continue; /* never default LOG_REPLAY */
444             if (!type->logs[sev].size) {
445                 logList_init(&type->logs[sev]);
446                 if (!dest) {
447                     if (!(dest = log_open(default_log)))
448                         break;
449                     dest->refcnt--;
450                 }
451                 logList_append(&type->logs[sev], dest);
452                 dest->refcnt++;
453             }
454         }
455         type->default_set = 1;
456     }
457     return type;
458 }
459
460 /* logging functions */
461
462 void
463 log_audit(struct log_type *type, enum log_severity sev, struct userNode *user, struct userNode *bot, const char *channel_name, unsigned int flags, const char *command)
464 {
465     struct logEntry *entry;
466     unsigned int size, ii;
467     char *str_next;
468
469     /* First make sure severity is appropriate */
470     if ((sev != LOG_COMMAND) && (sev != LOG_OVERRIDE) && (sev != LOG_STAFF)) {
471         log_module(MAIN_LOG, LOG_ERROR, "Illegal audit severity %d", sev);
472         return;
473     }
474     /* Allocate and fill in the log entry */
475     size = sizeof(*entry) + strlen(user->nick) + strlen(command) + 2;
476     if (user->handle_info)
477         size += strlen(user->handle_info->handle) + 1;
478     if (channel_name)
479         size += strlen(channel_name) + 1;
480     if (flags & AUDIT_HOSTMASK)
481         size += strlen(user->ident) + strlen(user->hostname) + 2;
482     entry = calloc(1, size);
483     str_next = (char*)(entry + 1);
484     entry->time = now;
485     entry->slvl = sev;
486     entry->bot = bot;
487     if (channel_name) {
488         size = strlen(channel_name) + 1;
489         entry->channel_name = memcpy(str_next, channel_name, size);
490         str_next += size;
491     }
492     if (true) {
493         size = strlen(user->nick) + 1;
494         entry->user_nick = memcpy(str_next, user->nick, size);
495         str_next += size;
496     }
497     if (user->handle_info) {
498         size = strlen(user->handle_info->handle) + 1;
499         entry->user_account = memcpy(str_next, user->handle_info->handle, size);
500         str_next += size;
501     }
502     if (flags & AUDIT_HOSTMASK) {
503         size = sprintf(str_next, "%s@%s", user->ident, user->hostname) + 1;
504         entry->user_hostmask = str_next;
505         str_next += size;
506     } else {
507         entry->user_hostmask = 0;
508     }
509     if (true) {
510         size = strlen(command) + 1;
511         entry->command = memcpy(str_next, command, size);
512         str_next += size;
513     }
514
515     /* fill in the default text for the event */
516     log_format_audit(entry);
517
518     /* insert into the linked list */
519     entry->next = 0;
520     entry->prev = type->log_newest;
521     if (type->log_newest)
522         type->log_newest->next = entry;
523     else
524         type->log_oldest = entry;
525     type->log_newest = entry;
526     type->log_count++;
527
528     /* remove old elements from the linked list */
529     while (type->log_count > type->max_count)
530         log_type_free_oldest(type);
531     while (type->log_oldest && (type->log_oldest->time + type->max_age < now))
532         log_type_free_oldest(type);
533     if (type->log_oldest)
534         type->log_oldest->prev = 0;
535     else
536         type->log_newest = 0;
537
538     /* call the destination logs */
539     for (ii=0; ii<type->logs[sev].used; ++ii) {
540         struct logDestination *ld = type->logs[sev].list[ii];
541         ld->vtbl->log_audit(ld, type, entry);
542     }
543     for (ii=0; ii<log_default->logs[sev].used; ++ii) {
544         struct logDestination *ld = log_default->logs[sev].list[ii];
545         ld->vtbl->log_audit(ld, type, entry);
546     }
547 }
548
549 void
550 log_replay(struct log_type *type, int is_write, const char *line)
551 {
552     unsigned int ii;
553
554     for (ii=0; ii<type->logs[LOG_REPLAY].used; ++ii) {
555         struct logDestination *ld = type->logs[LOG_REPLAY].list[ii];
556         ld->vtbl->log_replay(ld, type, is_write, line);
557     }
558     for (ii=0; ii<log_default->logs[LOG_REPLAY].used; ++ii) {
559         struct logDestination *ld = log_default->logs[LOG_REPLAY].list[ii];
560         ld->vtbl->log_replay(ld, type, is_write, line);
561     }
562 }
563
564 void
565 log_module(struct log_type *type, enum log_severity sev, const char *format, ...)
566 {
567     char msgbuf[1024];
568     unsigned int ii;
569     va_list args;
570
571     if (!type)
572         return;
573     if (type->depth)
574         return;
575     ++type->depth;
576     if (sev > LOG_FATAL) {
577         log_module(MAIN_LOG, LOG_ERROR, "Illegal log_module severity %d", sev);
578         return;
579     }
580     va_start(args, format);
581     vsnprintf(msgbuf, sizeof(msgbuf), format, args);
582     va_end(args);
583     if (log_inited) {
584         for (ii=0; ii<type->logs[sev].used; ++ii) {
585             struct logDestination *ld = type->logs[sev].list[ii];
586             ld->vtbl->log_module(ld, type, sev, msgbuf);
587         }
588         for (ii=0; ii<log_default->logs[sev].used; ++ii) {
589             struct logDestination *ld = log_default->logs[sev].list[ii];
590             ld->vtbl->log_module(ld, type, sev, msgbuf);
591         }
592     } else {
593         /* Special behavior before we start full operation */
594         fprintf(stderr, "%s: %s\n", log_severity_names[sev], msgbuf);
595     }
596     --type->depth;
597     if (sev == LOG_FATAL) {
598         assert(0 && "fatal message logged");
599         _exit(1);
600     }
601 }
602
603 /* audit log searching */
604
605 struct logSearch *
606 log_discrim_create(struct userNode *service, struct userNode *user, unsigned int argc, char *argv[])
607 {
608     unsigned int ii;
609     struct logSearch *discrim;
610
611     /* Assume all criteria require arguments. */
612     if((argc - 1) % 2)
613     {
614         send_message(user, service, "MSG_MISSING_PARAMS", argv[0]);
615         return NULL;
616     }
617
618     discrim = malloc(sizeof(struct logSearch));
619     memset(discrim, 0, sizeof(*discrim));
620     discrim->limit = 25;
621     discrim->max_time = INT_MAX;
622     discrim->severities = ~0;
623
624     for (ii=1; ii<argc-1; ii++) {
625         if (!irccasecmp(argv[ii], "bot")) {
626             struct userNode *bot = GetUserH(argv[++ii]);
627             if (!bot) {
628                 send_message(user, service, "MSG_NICK_UNKNOWN", argv[ii]);
629                 goto fail;
630             } else if (!IsLocal(bot)) {
631                 send_message(user, service, "MSG_NOT_A_SERVICE", argv[ii]);
632                 goto fail;
633             }
634             discrim->masks.bot = bot;
635         } else if (!irccasecmp(argv[ii], "channel")) {
636             discrim->masks.channel_name = argv[++ii];
637         } else if (!irccasecmp(argv[ii], "nick")) {
638             discrim->masks.user_nick = argv[++ii];
639         } else if (!irccasecmp(argv[ii], "account")) {
640             discrim->masks.user_account = argv[++ii];
641         } else if (!irccasecmp(argv[ii], "hostmask")) {
642             discrim->masks.user_hostmask = argv[++ii];
643         } else if (!irccasecmp(argv[ii], "command")) {
644             discrim->masks.command = argv[++ii];
645         } else if (!irccasecmp(argv[ii], "age")) {
646             const char *cmp = argv[++ii];
647             if (cmp[0] == '<') {
648                 if (cmp[1] == '=')
649                     discrim->min_time = now - ParseInterval(cmp+2);
650                 else
651                     discrim->min_time = now - (ParseInterval(cmp+1) - 1);
652             } else if (cmp[0] == '>') {
653                 if (cmp[1] == '=')
654                     discrim->max_time = now - ParseInterval(cmp+2);
655                 else
656                     discrim->max_time = now - (ParseInterval(cmp+1) - 1);
657             } else {
658                 discrim->min_time = now - ParseInterval(cmp);
659             }
660         } else if (!irccasecmp(argv[ii], "limit")) {
661             discrim->limit = strtoul(argv[++ii], NULL, 10);
662         } else if (!irccasecmp(argv[ii], "level")) {
663             char *severity = argv[++ii];
664             discrim->severities = 0;
665             while (1) {
666                 enum log_severity sev = find_severity(severity);
667                 if (sev == LOG_NUM_SEVERITIES) {
668                     send_message(user, service, "MSG_INVALID_SEVERITY", severity);
669                     goto fail;
670                 }
671                 discrim->severities |= 1 << sev;
672                 severity = strchr(severity, ',');
673                 if (!severity)
674                     break;
675                 severity++;
676             }
677         } else if (!irccasecmp(argv[ii], "type")) {
678             if (!(discrim->type = dict_find(log_types, argv[++ii], NULL))) {
679                 send_message(user, service, "MSG_INVALID_FACILITY", argv[ii]);
680                 goto fail;
681             }
682         } else {
683             send_message(user, service, "MSG_INVALID_CRITERIA", argv[ii]);
684             goto fail;
685         }
686     }
687
688     return discrim;
689   fail:
690     free(discrim);
691     return NULL;
692 }
693
694 static int
695 entry_match(struct logSearch *discrim, struct logEntry *entry)
696 {
697     if ((entry->time < discrim->min_time)
698         || (entry->time > discrim->max_time)
699         || !(discrim->severities & (1 << entry->slvl))
700         || (discrim->masks.bot && (discrim->masks.bot != entry->bot))
701         /* don't do glob matching, so that !events #a*b does not match #acb */
702         || (discrim->masks.channel_name
703             && (!entry->channel_name
704                 || irccasecmp(entry->channel_name, discrim->masks.channel_name)))
705         || (discrim->masks.user_nick
706             && !match_ircglob(entry->user_nick, discrim->masks.user_nick))
707         || (discrim->masks.user_account
708             && (!entry->user_account
709                 || !match_ircglob(entry->user_account, discrim->masks.user_account)))
710         || (discrim->masks.user_hostmask
711             && entry->user_hostmask
712             && !match_ircglob(entry->user_hostmask, discrim->masks.user_hostmask))
713         || (discrim->masks.command
714             && !match_ircglob(entry->command, discrim->masks.command))) {
715         return 0;
716     }
717     return 1;
718 }
719
720 void
721 log_report_entry(struct logEntry *match, void *extra)
722 {
723     struct logReport *rpt = extra;
724     send_message_type(4, rpt->user, rpt->reporter, "%s", match->default_desc);
725 }
726
727 unsigned int
728 log_entry_search(struct logSearch *discrim, entry_search_func esf, void *data)
729 {
730     unsigned int matched = 0;
731
732     if (discrim->type) {
733         static volatile struct logEntry *last;
734         struct logEntry *entry;
735
736         for (entry = discrim->type->log_oldest, last = NULL;
737              entry;
738              last = entry, entry = entry->next) {
739             verify(entry);
740             if (entry_match(discrim, entry)) {
741                 esf(entry, data);
742                 if (++matched >= discrim->limit)
743                     break;
744             }
745         }
746     } else {
747         dict_iterator_t it;
748
749         for (it = dict_first(log_types); it; it = iter_next(it)) {
750             discrim->type = iter_data(it);
751             matched += log_entry_search(discrim, esf, data);
752         }
753     }
754
755     return matched;
756 }
757
758 /* generic helper functions */
759
760 static void
761 log_format_timestamp(unsigned long when, struct string_buffer *sbuf)
762 {
763     struct tm local;
764     time_t feh;
765     feh = when;
766     localtime_r(&feh, &local);
767     if (sbuf->size < 24) {
768         sbuf->size = 24;
769         free(sbuf->list);
770         sbuf->list = calloc(1, 24);
771     }
772     sbuf->used = sprintf(sbuf->list, "[%02d:%02d:%02d %02d/%02d/%04d]", local.tm_hour, local.tm_min, local.tm_sec, local.tm_mon+1, local.tm_mday, local.tm_year+1900);
773 }
774
775 static void
776 log_format_audit(struct logEntry *entry)
777 {
778     struct string_buffer sbuf;
779     memset(&sbuf, 0, sizeof(sbuf));
780     log_format_timestamp(entry->time, &sbuf);
781     string_buffer_append_string(&sbuf, " (");
782     string_buffer_append_string(&sbuf, entry->bot->nick);
783     if (entry->channel_name) {
784         string_buffer_append(&sbuf, ':');
785         string_buffer_append_string(&sbuf, entry->channel_name);
786     }
787     string_buffer_append_string(&sbuf, ") [");
788     string_buffer_append_string(&sbuf, entry->user_nick);
789     if (entry->user_hostmask) {
790         string_buffer_append(&sbuf, '!');
791         string_buffer_append_string(&sbuf, entry->user_hostmask);
792     }
793     if (entry->user_account) {
794         string_buffer_append(&sbuf, ':');
795         string_buffer_append_string(&sbuf, entry->user_account);
796     }
797     string_buffer_append_string(&sbuf, "]: ");
798     string_buffer_append_string(&sbuf, entry->command);
799     entry->default_desc = strdup(sbuf.list);
800     free(sbuf.list);
801 }
802
803 /* shared stub log operations act as a noop */
804
805 static void
806 ldNop_reopen(UNUSED_ARG(struct logDestination *self_)) {
807     /* no operation necessary */
808 }
809
810 static void
811 ldNop_replay(UNUSED_ARG(struct logDestination *self_), UNUSED_ARG(struct log_type *type), UNUSED_ARG(int is_write), UNUSED_ARG(const char *line)) {
812     /* no operation necessary */
813 }
814
815 /* file: log type */
816
817 struct logDest_file {
818     struct logDestination base;
819     char *fname;
820     FILE *output;
821 };
822 static struct logDest_vtable ldFile_vtbl;
823
824 static struct logDestination *
825 ldFile_open(const char *args) {
826     struct logDest_file *ld;
827     ld = calloc(1, sizeof(*ld));
828     ld->base.vtbl = &ldFile_vtbl;
829     ld->fname = strdup(args);
830     ld->output = fopen(ld->fname, "a");
831     return &ld->base;
832 }
833
834 static void
835 ldFile_reopen(struct logDestination *self_) {
836     struct logDest_file *self = (struct logDest_file*)self_;
837     fclose(self->output);
838     self->output = fopen(self->fname, "a");
839 }
840
841 static void
842 ldFile_close(struct logDestination *self_) {
843     struct logDest_file *self = (struct logDest_file*)self_;
844     fclose(self->output);
845     free(self->fname);
846     free(self);
847 }
848
849 static void
850 ldFile_audit(struct logDestination *self_, UNUSED_ARG(struct log_type *type), struct logEntry *entry) {
851     struct logDest_file *self = (struct logDest_file*)self_;
852     fputs(entry->default_desc, self->output);
853     fputc('\n', self->output);
854     fflush(self->output);
855 }
856
857 static void
858 ldFile_replay(struct logDestination *self_, UNUSED_ARG(struct log_type *type), int is_write, const char *line) {
859     struct logDest_file *self = (struct logDest_file*)self_;
860     struct string_buffer sbuf;
861     memset(&sbuf, 0, sizeof(sbuf));
862     log_format_timestamp(now, &sbuf);
863     string_buffer_append_string(&sbuf, is_write ? "W: " : "   ");
864     string_buffer_append_string(&sbuf, line);
865     fputs(sbuf.list, self->output);
866     fputc('\n', self->output);
867     free(sbuf.list);
868     fflush(self->output);
869 }
870
871 static void
872 ldFile_module(struct logDestination *self_, struct log_type *type, enum log_severity sev, const char *message) {
873     struct logDest_file *self = (struct logDest_file*)self_;
874     struct string_buffer sbuf;
875     memset(&sbuf, 0, sizeof(sbuf));
876     log_format_timestamp(now, &sbuf);
877     fprintf(self->output, "%s (%s:%s) %s\n", sbuf.list, type->name, log_severity_names[sev], message);
878     free(sbuf.list);
879     fflush(self->output);
880 }
881
882 static struct logDest_vtable ldFile_vtbl = {
883     "file",
884     ldFile_open,
885     ldFile_reopen,
886     ldFile_close,
887     ldFile_audit,
888     ldFile_replay,
889     ldFile_module
890 };
891
892 /* std: log type */
893
894 static struct logDest_vtable ldStd_vtbl;
895
896 static struct logDestination *
897 ldStd_open(const char *args) {
898     struct logDest_file *ld;
899     ld = calloc(1, sizeof(*ld));
900     ld->base.vtbl = &ldStd_vtbl;
901     ld->fname = strdup(args);
902
903     /* Print to stderr if given "err" and default to stdout otherwise. */
904     if (atoi(args))
905         ld->output = fdopen(atoi(args), "a");
906     else if (!strcasecmp(args, "err"))
907         ld->output = stdout;
908     else
909         ld->output = stderr;
910
911     return &ld->base;
912 }
913
914 static void
915 ldStd_close(struct logDestination *self_) {
916     struct logDest_file *self = (struct logDest_file*)self_;
917     free(self->fname);
918     free(self);
919 }
920
921 static void
922 ldStd_replay(struct logDestination *self_, UNUSED_ARG(struct log_type *type), int is_write, const char *line) {
923     struct logDest_file *self = (struct logDest_file*)self_;
924     fprintf(self->output, "%s%s\n", is_write ? "W: " : "   ", line);
925 }
926
927 static void
928 ldStd_module(struct logDestination *self_, UNUSED_ARG(struct log_type *type), enum log_severity sev, const char *message) {
929     struct logDest_file *self = (struct logDest_file*)self_;
930     fprintf(self->output, "%s: %s\n", log_severity_names[sev], message);
931 }
932
933 static struct logDest_vtable ldStd_vtbl = {
934     "std",
935     ldStd_open,
936     ldNop_reopen,
937     ldStd_close,
938     ldFile_audit,
939     ldStd_replay,
940     ldStd_module
941 };
942
943 /* irc: log type */
944
945 struct logDest_irc {
946     struct logDestination base;
947     char *target;
948 };
949 static struct logDest_vtable ldIrc_vtbl;
950
951 static struct logDestination *
952 ldIrc_open(const char *args) {
953     struct logDest_irc *ld;
954     ld = calloc(1, sizeof(*ld));
955     ld->base.vtbl = &ldIrc_vtbl;
956     ld->target = strdup(args);
957     return &ld->base;
958 }
959
960 static void
961 ldIrc_close(struct logDestination *self_) {
962     struct logDest_irc *self = (struct logDest_irc*)self_;
963     free(self->target);
964     free(self);
965 }
966
967 static void
968 ldIrc_audit(struct logDestination *self_, UNUSED_ARG(struct log_type *type), struct logEntry *entry) {
969     struct logDest_irc *self = (struct logDest_irc*)self_;
970
971     if (entry->channel_name) {
972         send_target_message(4, self->target, entry->bot, "(%s", strchr(strchr(entry->default_desc, ' '), ':')+1);
973     } else {
974         send_target_message(4, self->target, entry->bot, "%s", strchr(entry->default_desc, ')')+2);
975     }
976 }
977
978 static void
979 ldIrc_module(struct logDestination *self_, struct log_type *type, enum log_severity sev, const char *message) {
980     struct logDest_irc *self = (struct logDest_irc*)self_;
981     extern struct userNode *opserv;
982
983     send_target_message(4, self->target, opserv, "%s %s: %s\n", type->name, log_severity_names[sev], message);
984 }
985
986 static struct logDest_vtable ldIrc_vtbl = {
987     "irc",
988     ldIrc_open,
989     ldNop_reopen,
990     ldIrc_close,
991     ldIrc_audit,
992     ldNop_replay, /* totally ignore this - it would be a recipe for disaster */
993     ldIrc_module
994 };
995
996 void
997 log_init(void)
998 {
999     log_types = dict_new();
1000     dict_set_free_keys(log_types, free);
1001     dict_set_free_data(log_types, log_type_free);
1002     log_dest_types = dict_new();
1003     /* register log types */
1004     dict_insert(log_dest_types, ldFile_vtbl.type_name, &ldFile_vtbl);
1005     dict_insert(log_dest_types, ldStd_vtbl.type_name, &ldStd_vtbl);
1006     dict_insert(log_dest_types, ldIrc_vtbl.type_name, &ldIrc_vtbl);
1007     conf_register_reload(log_conf_read);
1008     log_default = log_register_type("*", NULL);
1009     reg_exit_func(cleanup_logs);
1010     message_register_table(msgtab);
1011     log_inited = 1;
1012 }