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