Use ACL instead of AXE for utilities
[gnucomo.git] / src / gcm_input / logrunner.cpp
1 /*
2  * logrunner.c
3  * (c) Peter Roozemaal, feb 2003
4  *
5  * $Id: logrunner.cpp,v 1.6 2007-12-10 16:12:37 arjen Exp $
6  *
7  * 1) compile,
8  * 2) Add 'logfile' elements to the gnucomo configuration file
9  *    defining the name and type of each logfile to scan.
10  *    'filter' elements can be addes to pre-filter lines from the log.
11  *
12  *    example:
13  *       <logfile>
14  *          <name>/var/log/httpd/access_log</name>
15  *          <type>apache access log</type>
16  *       </logfile>
17  *       <logfile>
18  *          <name>/var/log/messages</name>
19  *          <type>system log</type>
20  *          <filter>open_scanner</filter>
21  *          <filter>session closed</filter>
22  *       </logfile>
23  *
24  * 3) run executable and feed the output to gcm_input
25  */
26 #include <sys/types.h>
27 #include <sys/stat.h>
28 #include <sys/socket.h>
29 #include <netinet/in.h>
30 #include <arpa/inet.h>
31 #include <unistd.h>
32 #include <errno.h>
33 #include <fcntl.h>
34 #include <netdb.h>
35 #include <stdio.h>
36 #include <signal.h>
37 #include <stdlib.h>
38 #include <string.h>
39
40 #include <fstream>
41 #include <list>
42
43 #include <String.h>
44
45 #include "gnucomo_config.h"
46
47 extern String XML_Entities(String s);
48
49 static const char* const usage =
50    "Usage: logrunner [<options>] [<hostname>]\n\n"
51    "<hostname> is the gnucomo data collection server\n"
52    "Options are:\n"
53    " -1: one-shot; stop after a single pass over one of the configured logfiles\n"
54    " -c <name>: specify alternative configuration name, default is \'gnucomo\'\n"
55    " -l <file>: Read input from logfile <file>.\n"
56    " -p <number>: set IP port number to connect to\n"
57    " -s <file>: Use <file> as the status file instead of the default.\n"
58    " -v Verbose output\n"
59    "If no hostname is specified, logrunner sends output to stdout\n";
60
61 class LogFile {
62    String   name;
63    String   type;
64    String   fromhost;
65
66    int     fd;
67    ino_t   inode;
68    off_t   position;
69
70    std::list<regex> filter;
71
72    void copy_data();
73
74 public:
75    LogFile(String n, String t, String fh)
76    {
77       name = n;
78       type = t;
79       fromhost = fh;
80       inode     = 0;
81       position  = 0;
82       fd        = -1;
83    }
84
85    LogFile(const LogFile &lf)
86    {
87       name     = lf.name;
88       type     = lf.type;
89       fromhost = lf.fromhost;
90
91       fd       = lf.fd;
92       inode    = lf.inode;
93       position = lf.position;
94       filter   = lf.filter;
95    }
96
97    String pathname()
98    {
99       return name;
100    }
101
102    void update_status(ino_t i, off_t p)
103    {
104       inode     = i;
105       position  = p;
106    }
107
108    String status()
109    {
110       return name + " " + String(inode) + " " + String(position);
111    }
112
113    void add_filter(String filter_expression)
114    {
115       filter.push_back(filter_expression);
116    }
117
118    void do_file();
119 };
120
121 static std::list<LogFile>     logs_to_run;
122
123 String confname = "gnucomo";
124 static char *statusname;
125 static char *newstatusname;
126 static char default_statusname[] = "/var/lib/logrunner.status";
127 static char default_newstatusname[] = "/var/lib/logrunner.status.new";
128
129 static const char *hostname = NULL;
130 static int port = 2996;         /* random magic number */
131 static int out_stream = 1;
132
133 static int oneshot = 0;
134 static int verbose = 0;
135
136 static int something_has_changed = 0;
137    
138 static volatile sig_atomic_t sig_seen = 0;
139
140 static void sighandler(int sig)
141 {
142    sig_seen = sig;
143 }
144
145 static void set_signal_handler(void)
146 {
147    /* These aren't all; but it's a nice set to start with */
148    signal(SIGHUP, sighandler);
149    signal(SIGINT, sighandler);
150    signal(SIGQUIT, sighandler);
151    signal(SIGABRT, sighandler);
152    signal(SIGPIPE, sighandler);
153    signal(SIGTERM, sighandler);
154 }
155
156
157 void open_output()
158 {
159    struct hostent* hostptr;
160    struct sockaddr_in addr;
161    unsigned int namelen = sizeof(addr);
162
163    if ( hostname )
164    {
165       hostptr = gethostbyname(hostname);
166       if ( !hostptr )
167       {
168          fprintf(stderr, "logrunner: FATAL: cannot resolve %s\n", hostname);
169          exit(2);
170       }
171       out_stream = socket(PF_INET, SOCK_STREAM, 0);
172       if ( out_stream < 0 )
173       {
174          fprintf(stderr, "logrunner: FATAL: Socket creation failed\n");
175          exit(2);
176       }
177       addr.sin_family = AF_INET;
178       addr.sin_addr.s_addr = *((long*)(hostptr->h_addr));
179       addr.sin_port = htons(port);
180       if ( connect(out_stream, (struct sockaddr*) &addr, namelen) < 0 )
181       {
182          fprintf(stderr, "logrunner: FATAL: connect to %s failed\n", hostname);
183          exit(2);
184       }
185    }
186    else
187    {
188       out_stream = 1;
189    }
190 }
191
192 void xsend(const char* str)
193 {
194    write(out_stream, str, strlen(str));
195 }
196
197 void xml_header(String type)
198 {
199    struct hostent *host;
200    char buffer[256];
201    *buffer = 0;
202    gethostname(buffer, sizeof(buffer));
203
204    //  Try to obtain the official name of the host (FQDN)
205  
206    host = gethostbyname(buffer);
207    if (host != NULL)
208    {
209       strcpy(buffer, host->h_name);
210    }
211
212    xsend("<?xml version='1.0'?>\n");
213    xsend("<gcmt:message xmlns:gcmt=\"http://gnucomo.org/transport/\">\n");
214    xsend("<gcmt:header>\n<gcmt:hostname>");
215    xsend(buffer);
216    xsend("</gcmt:hostname>\n<gcmt:messagetype>");
217    xsend(type);
218    xsend("</gcmt:messagetype>\n</gcmt:header>\n");
219    xsend("<gcmt:data><gcmt:log>");
220 }
221
222 void xml_footer(void)
223 {
224    xsend("</gcmt:log></gcmt:data>\n</gcmt:message>\n");
225 }
226
227 void output(const char* str)
228 {
229    std::cerr << str;
230 }
231
232 void output_error(const char* str)
233 {
234    std::cerr << str << " errno=" << errno << "\n";
235 }
236
237 void LogFile::do_file()
238 {
239    struct stat statinfo;
240    char buffer[80];
241
242    /* inode check */
243    if ( stat(name, &statinfo) )
244    {
245       std::cerr << "!!! logfile: stat failed: " << name << ", errno=" << errno << "\n";
246    }
247    else
248    {
249       if ( statinfo.st_ino != inode )
250       {
251          if ( fd >= 0 )
252          {
253             copy_data();
254             close(fd);
255             fd = -1;
256          }
257          if (verbose)
258          {
259             std::cerr << "@@@ logfile: logrotate detected: " << name << "\n";
260          }
261          inode = statinfo.st_ino;
262          position = 0;
263       }
264    }
265    if ( fd < 0 )
266    {
267       /* attempt to open the file */
268       fd = open(name, O_RDONLY);
269       if ( fd < 0 )
270       {
271          std::cerr << "!!! logfile: open failed: " << name << ", " << strerror(errno) << "\n";
272          return;
273       }
274       lseek(fd, position, SEEK_SET);
275
276       if (verbose)
277       {
278          std::cerr << "*** logfile: opened: " << name;
279          std::cerr << "\n*** logfile: resumed read from position ";
280          std::cerr << position << "\n";
281          std::cerr << "This logfile has " << filter.size() << " filters.\n";
282       }
283    }
284
285    copy_data();
286 }
287
288 void LogFile::copy_data()
289 {
290    char buffer[4096];
291    int ndata;
292    String logline("");
293
294    /* read data and dump to output */
295    ndata = read(fd, buffer, sizeof(buffer)-1);
296    if ( ndata > 0 )
297    {
298       xml_header(type);
299       while ( ndata > 0 )
300       {
301          //  Make a separate <gcmt:raw> element from each line
302  
303          char *line, *nextline;
304
305          line = buffer;
306          nextline = buffer;
307
308          while (nextline < buffer + ndata)
309          {
310             while (*nextline != '\n' && nextline < buffer + ndata)
311             {
312                nextline++;
313             }
314             if (*nextline == '\n')
315             {
316                // Another line found - make the split.
317                *nextline++ = '\0';
318
319                logline += line;
320
321                // See if we have to select the host and apply filters to this log entry
322
323                bool filtered_out = false;
324
325                if (fromhost && (fromhost.in(logline) == -1 || fromhost.in(logline) > 20))
326                {
327                   filtered_out = true;
328                }
329
330                std::list<regex>::iterator f = filter.begin();
331                while (f != filter.end())
332                {
333                   if (logline == *f)
334                   {
335                      filtered_out = true;
336                   }
337                   f++;
338                }
339
340                if (!filtered_out)
341                {
342                   logline = XML_Entities(logline);
343
344                   write(out_stream, "<gcmt:raw>", 10);
345                   write(out_stream, logline, ~logline);
346                   write(out_stream, "</gcmt:raw>\n", 12);
347                }
348                else if (verbose)
349                {
350                   std::cerr << logline << " is filtered out.\n";
351                }
352
353                line = nextline;
354                logline = "";
355             }
356             else
357             {
358                //  We have a buffer full of data but no newline.
359                //  Flush the buffer into logline and continue reading.
360
361                nextline[0] = '\0';
362                logline += line;
363                line = nextline;
364             }
365          }
366          if (line != nextline)
367          {
368             //  There is still an incomplete line in the buffer.
369             memmove(buffer, line, nextline - line);
370          }
371          position += ndata - (nextline - line);
372          ndata -= line - buffer;
373          ndata += read(fd, buffer + (nextline - line), sizeof(buffer) - 1 - (nextline - line));
374       }
375       xml_footer();
376       something_has_changed = 1;
377    }   
378    if ( ndata < 0 )
379    {
380       std::cerr << "!!! logfile: read failed: " << name << ", " << strerror(errno) << "\n";
381       std::cerr << "    file descriptor = " << fd << "\n";
382    }
383 }
384
385 void write_status_file()
386 {
387    FILE* dumpfile;
388    int localerror = 0;
389
390    std::ofstream   statusfile(newstatusname);
391    std::list<LogFile>::iterator lf = logs_to_run.begin();
392    while (lf != logs_to_run.end())
393    {
394       if (verbose)
395       {
396          std::cerr  << "Write status for " << lf->pathname() << "\n";
397       }
398       statusfile << lf->status() << "\n";
399       lf++;
400    }
401    
402    if ( localerror == 0 )
403    {
404       if ( rename(newstatusname, statusname) )
405       {
406          output_error("!!! dumpstatus: rename failed");
407       }
408    }
409    something_has_changed = 0;
410 }
411
412 void read_status()
413 {
414    FILE* statusfile;
415    char buffer[4096];
416    char* xp;
417    long ino;
418    unsigned long pos;
419    struct fileinfo* fp;
420
421    statusfile = fopen(statusname, "r");
422    if ( statusfile == NULL )
423    {
424       fprintf(stderr, "logrunner: can\'t open status file \'%s\': ",
425          statusname);
426       perror("");
427       return;
428    }
429    while ( fgets(buffer, sizeof(buffer), statusfile ) )
430    {
431       xp = strchr(buffer, ' ');
432       if ( xp )
433       {
434          *xp = 0;
435          sscanf(xp+1, "%ld %lu", &ino, &pos);
436
437          //  Search for the logfile in the list of logs to run
438
439          std::list<LogFile>::iterator lf = logs_to_run.begin();
440          while (lf != logs_to_run.end())
441          {
442             if (lf->pathname() == String(buffer))
443             {
444                if (verbose)
445                {
446                   std::cerr << "Read status for " << lf->pathname() << "\n";
447                }
448                lf->update_status(ino, pos);
449             }
450             lf++;
451          }
452
453       }
454    }
455    fclose(statusfile);
456 }
457
458 void read_config(gnucomo_config cfg)
459 {
460    /*
461     *   The configuration for logrunner is stored in the central Gnucomo
462     *   configuration file. Multiple 'logfile' elements can be put in this
463     *   XML file, one for each logfile to scan.
464     *   Each 'logfile' element has at least a 'name' and a 'type' element
465     *   that denote the pathname of the logfile and the type of its content.
466     */
467
468    String logfilename;
469    String logfiletype;
470    String fromhost;
471
472    int l = 0;
473
474    logfilename = cfg.find_parameter("logfile", "name", l);
475    while (logfilename != String(""))
476    {
477       logfiletype = cfg.find_parameter("logfile", "type", l);
478       fromhost = cfg.find_parameter("logfile", "fromhost", l);
479       if (verbose)
480       {
481          std::cerr << "LogFile " << logfilename << " of type "
482                    << logfiletype << " from host " << fromhost << "\n";
483       }
484
485       LogFile lf(logfilename, logfiletype, fromhost);
486
487       int f = 0;
488       String exp  = cfg.find_parameter("logfile", "filter", l, f);
489       while (exp != "")
490       {
491          lf.add_filter(exp);
492
493          f++;
494          exp = cfg.find_parameter("logfile", "filter", l, f);
495       }
496
497       logs_to_run.push_back(lf);
498
499       l++;
500       logfilename = cfg.find_parameter("logfile", "name", l);
501    }
502
503 }
504
505 void process_options(int argc, char* argv[])
506 {
507    const char* const options = "1c:l:p:s:v";
508    int opt;
509
510    statusname    = default_statusname;
511    newstatusname = default_newstatusname;
512    String logfilename;
513
514    opt = getopt(argc, argv, options);
515    while ( opt != -1 )
516    {
517       switch ( opt )
518       {
519       case '1':
520          oneshot = 1;
521          break;
522       case 'c':
523          confname = strdup(optarg);
524          break;
525       case 'l':
526          logfilename = optarg;
527          break;
528       case 'p':
529          port = atoi(optarg);
530          break;
531       case 's':
532          statusname = strdup(optarg);
533          newstatusname = (char *)malloc(strlen(statusname) + 6);
534          strcpy(newstatusname, statusname);
535          strcat(newstatusname, ".new");
536          break;
537       case 'v':
538          verbose = 1;
539          break;
540       default:
541          fputs(usage, stderr);
542          exit(2);
543       }
544       opt = getopt(argc, argv, options);
545    }
546    switch ( argc-optind )
547    {
548    case 0:
549       break;
550    case 1:
551       hostname = argv[optind];
552       break;
553    default:
554       fputs(usage, stderr);
555       exit(2);
556    }
557
558    if (logfilename)
559    {
560       LogFile lf(logfilename, String("system log"), String(""));
561       logs_to_run.push_back(lf);
562    }
563 }
564
565 int main(int argc, char* argv[])
566 {
567
568    gnucomo_config    cfg;
569
570    process_options(argc, argv);
571    open_output();
572    
573    if (!cfg.read(confname))
574    {
575       std::cerr << "Can not read Gnucomo configuration file for " << confname << ".\n";
576       exit(1);
577    }
578
579    if (logs_to_run.empty())
580    {
581       read_config(cfg);
582    }
583    read_status();
584
585    set_signal_handler();
586
587    while ( sig_seen == 0 )
588    {
589       std::list<LogFile>::iterator lf = logs_to_run.begin();
590       while (lf != logs_to_run.end() && !(something_has_changed && oneshot))
591       {
592          lf->do_file();
593          lf++;
594       }
595
596       if ( something_has_changed )
597       {
598          write_status_file();
599       }
600       if ( oneshot )
601       {
602          return 0;
603       }
604       sleep(1);
605    }
606
607    fprintf(stderr, "logrunner: stopped by signal %d\n", sig_seen);
608    /* shouldn't we close files and release memory here? */
609    return 0;
610 }