New options added:
[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 <AXE/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 = "/var/lib/logrunner.status";
125 static char* newstatusname = "/var/lib/logrunner.status.new";
126
127 static const char *hostname = NULL;
128 static int port = 2996;         /* random magic number */
129 static int out_stream = 1;
130
131 static int oneshot = 0;
132 static int verbose = 0;
133
134 static int something_has_changed = 0;
135    
136 static volatile sig_atomic_t sig_seen = 0;
137
138 static void sighandler(int sig)
139 {
140    sig_seen = sig;
141 }
142
143 static void set_signal_handler(void)
144 {
145    /* These aren't all; but it's a nice set to start with */
146    signal(SIGHUP, sighandler);
147    signal(SIGINT, sighandler);
148    signal(SIGQUIT, sighandler);
149    signal(SIGABRT, sighandler);
150    signal(SIGPIPE, sighandler);
151    signal(SIGTERM, sighandler);
152 }
153
154
155 void open_output()
156 {
157    struct hostent* hostptr;
158    struct sockaddr_in addr;
159    unsigned int namelen = sizeof(addr);
160
161    if ( hostname )
162    {
163       hostptr = gethostbyname(hostname);
164       if ( !hostptr )
165       {
166          fprintf(stderr, "logrunner: FATAL: cannot resolve %s\n", hostname);
167          exit(2);
168       }
169       out_stream = socket(PF_INET, SOCK_STREAM, 0);
170       if ( out_stream < 0 )
171       {
172          fprintf(stderr, "logrunner: FATAL: Socket creation failed\n");
173          exit(2);
174       }
175       addr.sin_family = AF_INET;
176       addr.sin_addr.s_addr = *((long*)(hostptr->h_addr));
177       addr.sin_port = htons(port);
178       if ( connect(out_stream, (struct sockaddr*) &addr, namelen) < 0 )
179       {
180          fprintf(stderr, "logrunner: FATAL: connect to %s failed\n", hostname);
181          exit(2);
182       }
183    }
184    else
185    {
186       out_stream = 1;
187    }
188 }
189
190 void xsend(const char* str)
191 {
192    write(out_stream, str, strlen(str));
193 }
194
195 void xml_header(String type)
196 {
197    struct hostent *host;
198    char buffer[256];
199    *buffer = 0;
200    gethostname(buffer, sizeof(buffer));
201
202    //  Try to obtain the official name of the host (FQDN)
203  
204    host = gethostbyname(buffer);
205    if (host != NULL)
206    {
207       strcpy(buffer, host->h_name);
208    }
209
210    xsend("<?xml version='1.0'?>\n");
211    xsend("<gcmt:message xmlns:gcmt=\"http://gnucomo.org/transport/\">\n");
212    xsend("<gcmt:header>\n<gcmt:hostname>");
213    xsend(buffer);
214    xsend("</gcmt:hostname>\n<gcmt:messagetype>");
215    xsend(type);
216    xsend("</gcmt:messagetype>\n</gcmt:header>\n");
217    xsend("<gcmt:data><gcmt:log>");
218 }
219
220 void xml_footer(void)
221 {
222    xsend("</gcmt:log></gcmt:data>\n</gcmt:message>\n");
223 }
224
225 void output(const char* str)
226 {
227    std::cerr << str;
228 }
229
230 void output_error(const char* str)
231 {
232    std::cerr << str << " errno=" << errno << "\n";
233 }
234
235 void LogFile::do_file()
236 {
237    struct stat statinfo;
238    char buffer[80];
239
240    /* inode check */
241    if ( stat(name, &statinfo) )
242    {
243       std::cerr << "!!! logfile: stat failed: " << name << ", errno=" << errno << "\n";
244    }
245    else
246    {
247       if ( statinfo.st_ino != inode )
248       {
249          if ( fd >= 0 )
250          {
251             copy_data();
252             close(fd);
253             fd = -1;
254          }
255          if (verbose)
256          {
257             std::cerr << "@@@ logfile: logrotate detected: " << name << "\n";
258          }
259          inode = statinfo.st_ino;
260          position = 0;
261       }
262    }
263    if ( fd < 0 )
264    {
265       /* attempt to open the file */
266       fd = open(name, O_RDONLY);
267       if ( fd < 0 )
268       {
269          std::cerr << "!!! logfile: open failed: " << name << ", " << strerror(errno) << "\n";
270          return;
271       }
272       lseek(fd, position, SEEK_SET);
273
274       if (verbose)
275       {
276          std::cerr << "*** logfile: opened: " << name;
277          std::cerr << "\n*** logfile: resumed read from position ";
278          std::cerr << position << "\n";
279          std::cerr << "This logfile has " << filter.size() << " filters.\n";
280       }
281    }
282
283    copy_data();
284 }
285
286 void LogFile::copy_data()
287 {
288    char buffer[4096];
289    int ndata;
290    String logline("");
291
292    /* read data and dump to output */
293    ndata = read(fd, buffer, sizeof(buffer)-1);
294    if ( ndata > 0 )
295    {
296       xml_header(type);
297       while ( ndata > 0 )
298       {
299          //  Make a separate <gcmt:raw> element from each line
300  
301          char *line, *nextline;
302
303          line = buffer;
304          nextline = buffer;
305
306          while (nextline < buffer + ndata)
307          {
308             while (*nextline != '\n' && nextline < buffer + ndata)
309             {
310                nextline++;
311             }
312             if (*nextline == '\n')
313             {
314                // Another line found - make the split.
315                *nextline++ = '\0';
316
317                logline += line;
318
319                // See if we have to select the host and apply filters to this log entry
320
321                bool filtered_out = false;
322
323                if (fromhost && (fromhost.in(logline) == -1 || fromhost.in(logline) > 20))
324                {
325                   filtered_out = true;
326                }
327
328                std::list<regex>::iterator f = filter.begin();
329                while (f != filter.end())
330                {
331                   if (logline == *f)
332                   {
333                      filtered_out = true;
334                   }
335                   f++;
336                }
337
338                if (!filtered_out)
339                {
340                   logline = XML_Entities(logline);
341
342                   write(out_stream, "<gcmt:raw>", 10);
343                   write(out_stream, logline, ~logline);
344                   write(out_stream, "</gcmt:raw>\n", 12);
345                }
346                else if (verbose)
347                {
348                   std::cerr << logline << " is filtered out.\n";
349                }
350
351                line = nextline;
352                logline = "";
353             }
354             else
355             {
356                //  We have a buffer full of data but no newline.
357                //  Flush the buffer into logline and continue reading.
358
359                nextline[0] = '\0';
360                logline += line;
361                line = nextline;
362             }
363          }
364          if (line != nextline)
365          {
366             //  There is still an incomplete line in the buffer.
367             memmove(buffer, line, nextline - line);
368          }
369          position += ndata - (nextline - line);
370          ndata -= line - buffer;
371          ndata += read(fd, buffer + (nextline - line), sizeof(buffer) - 1 - (nextline - line));
372       }
373       xml_footer();
374       something_has_changed = 1;
375    }   
376    if ( ndata < 0 )
377    {
378       std::cerr << "!!! logfile: read failed: " << name << ", " << strerror(errno) << "\n";
379       std::cerr << "    file descriptor = " << fd << "\n";
380    }
381 }
382
383 void write_status_file()
384 {
385    FILE* dumpfile;
386    int localerror = 0;
387
388    std::ofstream   statusfile(newstatusname);
389    std::list<LogFile>::iterator lf = logs_to_run.begin();
390    while (lf != logs_to_run.end())
391    {
392       if (verbose)
393       {
394          std::cerr  << "Write status for " << lf->pathname() << "\n";
395       }
396       statusfile << lf->status() << "\n";
397       lf++;
398    }
399    
400    if ( localerror == 0 )
401    {
402       if ( rename(newstatusname, statusname) )
403       {
404          output_error("!!! dumpstatus: rename failed");
405       }
406    }
407    something_has_changed = 0;
408 }
409
410 void read_status()
411 {
412    FILE* statusfile;
413    char buffer[4096];
414    char* xp;
415    long ino;
416    unsigned long pos;
417    struct fileinfo* fp;
418
419    statusfile = fopen(statusname, "r");
420    if ( statusfile == NULL )
421    {
422       fprintf(stderr, "logrunner: can\'t open status file \'%s\': ",
423          statusname);
424       perror("");
425       return;
426    }
427    while ( fgets(buffer, sizeof(buffer), statusfile ) )
428    {
429       xp = strchr(buffer, ' ');
430       if ( xp )
431       {
432          *xp = 0;
433          sscanf(xp+1, "%ld %lu", &ino, &pos);
434
435          //  Search for the logfile in the list of logs to run
436
437          std::list<LogFile>::iterator lf = logs_to_run.begin();
438          while (lf != logs_to_run.end())
439          {
440             if (lf->pathname() == String(buffer))
441             {
442                if (verbose)
443                {
444                   std::cerr << "Read status for " << lf->pathname() << "\n";
445                }
446                lf->update_status(ino, pos);
447             }
448             lf++;
449          }
450
451       }
452    }
453    fclose(statusfile);
454 }
455
456 void read_config(gnucomo_config cfg)
457 {
458    /*
459     *   The configuration for logrunner is stored in the central Gnucomo
460     *   configuration file. Multiple 'logfile' elements can be put in this
461     *   XML file, one for each logfile to scan.
462     *   Each 'logfile' element has at least a 'name' and a 'type' element
463     *   that denote the pathname of the logfile and the type of its content.
464     */
465
466    String logfilename;
467    String logfiletype;
468    String fromhost;
469
470    int l = 0;
471
472    logfilename = cfg.find_parameter("logfile", "name", l);
473    while (logfilename != String(""))
474    {
475       logfiletype = cfg.find_parameter("logfile", "type", l);
476       fromhost = cfg.find_parameter("logfile", "fromhost", l);
477       if (verbose)
478       {
479          std::cerr << "LogFile " << logfilename << " of type "
480                    << logfiletype << " from host " << fromhost << "\n";
481       }
482
483       LogFile lf(logfilename, logfiletype, fromhost);
484
485       int f = 0;
486       String exp  = cfg.find_parameter("logfile", "filter", l, f);
487       while (exp != "")
488       {
489          lf.add_filter(exp);
490
491          f++;
492          exp = cfg.find_parameter("logfile", "filter", l, f);
493       }
494
495       logs_to_run.push_back(lf);
496
497       l++;
498       logfilename = cfg.find_parameter("logfile", "name", l);
499    }
500
501 }
502
503 void process_options(int argc, char* argv[])
504 {
505    const char* const options = "1c:l:p:s:v";
506    int opt;
507
508    String logfilename;
509
510    opt = getopt(argc, argv, options);
511    while ( opt != -1 )
512    {
513       switch ( opt )
514       {
515       case '1':
516          oneshot = 1;
517          break;
518       case 'c':
519          confname = strdup(optarg);
520          break;
521       case 'l':
522          logfilename = optarg;
523          break;
524       case 'p':
525          port = atoi(optarg);
526          break;
527       case 's':
528          statusname = strdup(optarg);
529          newstatusname = (char *)malloc(strlen(statusname) + 6);
530          strcpy(newstatusname, statusname);
531          strcat(newstatusname, ".new");
532          break;
533       case 'v':
534          verbose = 1;
535          break;
536       default:
537          fputs(usage, stderr);
538          exit(2);
539       }
540       opt = getopt(argc, argv, options);
541    }
542    switch ( argc-optind )
543    {
544    case 0:
545       break;
546    case 1:
547       hostname = argv[optind];
548       break;
549    default:
550       fputs(usage, stderr);
551       exit(2);
552    }
553
554    if (logfilename)
555    {
556       LogFile lf(logfilename, String("system log"), String(""));
557       logs_to_run.push_back(lf);
558    }
559 }
560
561 int main(int argc, char* argv[])
562 {
563
564    gnucomo_config    cfg;
565
566    process_options(argc, argv);
567    open_output();
568    
569    if (!cfg.read(confname))
570    {
571       std::cerr << "Can not read Gnucomo configuration file for " << confname << ".\n";
572       exit(1);
573    }
574
575    if (logs_to_run.empty())
576    {
577       read_config(cfg);
578    }
579    read_status();
580
581    set_signal_handler();
582
583    while ( sig_seen == 0 )
584    {
585       std::list<LogFile>::iterator lf = logs_to_run.begin();
586       while (lf != logs_to_run.end() && !(something_has_changed && oneshot))
587       {
588          lf->do_file();
589          lf++;
590       }
591
592       if ( something_has_changed )
593       {
594          write_status_file();
595       }
596       if ( oneshot )
597       {
598          return 0;
599       }
600       sleep(1);
601    }
602
603    fprintf(stderr, "logrunner: stopped by signal %d\n", sig_seen);
604    /* shouldn't we close files and release memory here? */
605    return 0;
606 }