/*****************************************************************************
*
* fetchlog.c - logfile fetcher: pick up last new messages of a logfile
*
* Copyright (c) 2002 .. 2004 Alexander Haderer (alexander.haderer@charite.de)
*
* Last Update: $Author: afrika $
* Update Date: $Date: 2004/03/26 19:46:03 $
* Source File: $Source: /home/cvsroot/tools/fetchlog/fetchlog.c,v $
* CVS/RCS Revision: $Revision: 1.6 $
* Status: $State: Exp $
*
* CVS/RCS Log at end of file
*
* License:
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*****************************************************************************/
#include <stdio.h> /* sprintf */
#include <stdlib.h> /* atoi */
#include <ctype.h> /* isalpha */
#include <string.h> /* strcat, strcpy */
#include <fcntl.h> /* open close */
#include <sys/types.h> /* stat */
#include <sys/stat.h> /* stat */
#include <sys/mman.h> /* mmap, madvise */
#include <unistd.h> /* access */
#include <errno.h> /* errno */
#ifdef HAS_REGEX
#include <regex.h> /* posix regex stuff */
#endif
/*************************************************
* constants
*************************************************/
#define MIN_FIRSTCOL 1 /* Min first col for fetching */
#define MAX_LASTCOL 300 /* Max last col for fetching */
#define MIN_COLS 20 /* Min no of cols to fetch */
#define MIN_FETCHLEN 50 /* Min length of fetched data */
#define MAX_FETCHLEN 10000 /* Max length of fetched data */
#define OK_MESSAGE "OK: no messages"
#define ERR_MESSAGE "ERROR: fetchlog: "
/* suffix for temp bookmarkfile: mkstemp() template */
#define FETCH_FILE_SUFFIX "XXXXXX"
/* conversion flags */
#define CONV_NONE 0
#define CONV_BRACKET 1
#define CONV_PERCENT 2
#define CONV_NEWLINE 4
#define CONV_OKMSG 8
#define CONV_SHELL 16
#define CONV_ALL (CONV_BRACKET|CONV_PERCENT|CONV_NEWLINE|CONV_OKMSG|CONV_SHELL )
/* return/exit codes */
#define RET_OK 0 /* ok, no messages ok */
#define RET_ERROR 1 /* internal error warn */
#define RET_NEWMSG 2 /* logfile has new messages critical */
#define RET_PARAM 3 /* wrong parameter, print help */
/* version info */
#define FL_VERSION FETCHLOG_VERSION_NO
/*************************************************
* typedefs
*************************************************/
typedef struct {
long last; /* pos lastchar fetched plus 1 (=first new char) */
time_t mtime; /* mtime of logfile when this bookmark was valid */
ino_t inode; /* inode number of logfile */
} bookmark;
/*************************************************
* prototypes
*************************************************/
void usage( void );
int fetch_logfile( char *logfilename, char *bookmarkfile, int updbm_flag );
int read_bookmark( char *bmfile, bookmark *bm );
int write_bookmark( char *bmfile, bookmark *bm );
int copyline( int opos, char *obuf, char *ipt, int illen );
int check_farg( char *farg, int *conv );
void perr( char *msg1, char *msg2, int err );
/*************************************************
* globals
*************************************************/
/* fetching */
int firstcol_G = MIN_FIRSTCOL;
int lastcol_G = MAX_LASTCOL;
int fetchlen_G = MAX_FETCHLEN;
int conv_G = CONV_NONE;
#ifdef HAS_REGEX
regex_t *rx_G = NULL;
int numrx_G = 0;
#define RXERRBUFLEN 1000
char rxerrbuf_G[RXERRBUFLEN];
#endif
/*************************************************
* code...
*************************************************/
/************************************************
* perr( .. )
* print error message to stdout with respect to fetchlen_G
************************************************
*/
void perr( char *msg1, char *msg2, int err ) {
char *msg = NULL;
int len = 0;
if( !msg1 ) return;
len = sizeof( ERR_MESSAGE ) + strlen( msg1 ) + 1; /* 1 == '\n' */
if( msg2 ) len += strlen( msg2 ) + 2; /* 2 == ': ' */
if( err ) len += strlen( strerror( err ) ) + 2; /* 2 == ': ' */
if( (msg=malloc( len ) ) == NULL ) { exit( RET_ERROR ); }
strcpy( msg, ERR_MESSAGE );
strcat( msg, msg1 );
if( msg2 ) {
strcat( msg, ": " );
strcat( msg, msg2 );
}
if( err ) {
strcat( msg, ": " );
strcat( msg, strerror( err ) );
}
if( len-1 <= fetchlen_G ) {
strcat( msg, "\n" );
}else{
msg[fetchlen_G-1] = '\n';
msg[fetchlen_G-2] = '~';
len = fetchlen_G + 1;
}
write( STDOUT_FILENO, msg, len-1 );
free( msg );
}
/************************************************
* read_bookmark( char *bmfile, bookmark *bm );
* return: 0: ok 1: error
************************************************
*/
int read_bookmark( char *bmfile, bookmark *bm ) {
int fd = -1;
struct stat sb;
fd = open( bmfile, O_RDONLY );
if( fd == -1 ) {
if( errno == ENOENT ) {
/* no bookmarkfile --> acts like infinite old bookmarkfile */
bm->last = -1;
bm->mtime = 0;
bm->inode = 0;
return 0;
}else{
perr( "open", bmfile, errno );
return 1;
}
}
if( fstat( fd, &sb ) == -1 ) {
perr( "stat", bmfile, errno );
close( fd );
return 1;
}
if( (int)sb.st_size != sizeof(bookmark) ||
((sb.st_mode & S_IFMT) != S_IFREG) )
{
perr( "no file/wrong size", bmfile, 0);
close( fd );
return 1;
}
if( read( fd, bm, sizeof(bookmark)) != sizeof( bookmark ) ) {
perr( "file to short", bmfile, 0 );
close( fd );
return 1;
}
close( fd );
return 0; /* ok */
} /* read_bookmark() */
/************************************************
* write_bookmark( char *bmfile, bookmark *bm );
* return: 0: ok 1: error
************************************************
*/
int write_bookmark( char *bmfile, bookmark *bm ) {
char *nbmfile = NULL; /* new bmfile (a tmp file to be renamed to bmfile) */
int nbmfd = -1;
nbmfile = (char*)malloc( strlen(bmfile) + sizeof(FETCH_FILE_SUFFIX) );
if( nbmfile == NULL ) {
perr("malloc", NULL, errno );
return 1;
}
strcpy( nbmfile, bmfile );
strcat( nbmfile, FETCH_FILE_SUFFIX );
nbmfd = mkstemp( nbmfile );
if( nbmfd == -1 ) {
perr( "mkstemp", nbmfile, errno );
free( nbmfile );
return 1;
}
if( write( nbmfd, bm, sizeof( bookmark ) ) != sizeof( bookmark ) ) {
perr( "write", nbmfile, errno );
close( nbmfd );
unlink( nbmfile );
free( nbmfile );
return 1;
}
close( nbmfd );
if( rename( nbmfile, bmfile ) == -1 ) {
perr( "rename tmpfile to", bmfile, errno );
unlink( nbmfile );
free( nbmfile );
return 1;
}
free( nbmfile );
return 0; /* ok */
} /* write_bookmark() */
/************************************************
* fetch_logfile( char *logfilename, char *bookmarkfile, int updbm_flag )
************************************************
*/
int fetch_logfile( char *logfile, char *bmfile, int updbm_flag ) {
bookmark obm, nbm; /* old, new bookmark */
char *ibuf = NULL; /* input buf (--> the logfile) */
size_t ilen = 0;
char *ipt = NULL;
char *bmpt = NULL; /* points to first new char if bm exists */
char *obuf = NULL; /* output buf (filled backwards) */
int opos; /* first used char pos in obuf */
int i;
int fd = -1;
struct stat sb;
char *lgfile = NULL;
int llen = 0;
int done = 0;
if( read_bookmark( bmfile, &obm ) ) return RET_ERROR;
if( (fd=open( logfile, O_RDONLY )) == -1 ) {
perr( "open", logfile, errno );
return RET_ERROR;
}
if( fstat( fd, &sb ) == -1 ) {
perr( "stat", logfile, errno );
close( fd );
return RET_ERROR;
}
nbm.last = (size_t) sb.st_size;
nbm.mtime = sb.st_mtime;
nbm.inode = sb.st_ino;
/* something changed meanwhile ? */
if( obm.mtime==nbm.mtime && obm.inode==nbm.inode && obm.last==nbm.last ) {
if( conv_G & CONV_OKMSG )
write( STDOUT_FILENO, OK_MESSAGE "\n", sizeof( OK_MESSAGE ) );
close( fd );
return RET_OK;
}
/*****************/
obuf = malloc( fetchlen_G+1 );
if( obuf == NULL ) {
perr( "malloc", NULL, errno );
close( fd );
return RET_ERROR;
}
opos = fetchlen_G;
*(obuf + opos) = '\0'; /* dummy: opos -> first used char in obuf */
if( conv_G & CONV_NEWLINE ) {
/* when using CONV_NEWLINE the obuf is filled up like this:
1. init: write '\\' 'n' '\n' to the end (3 chars)
2. fill (copyline() ) by first prepend line contents and
then prepend '\\' 'n'
result: An additional '\\' 'n' at the beginning
else
1. fill (copyline() ) by first prepend newline and then prepend
line contents
*/
*(obuf + --opos) = '\n';
*(obuf + --opos) = 'n';
*(obuf + --opos) = '\\';
}
lgfile = (char*)malloc( strlen(logfile) + sizeof(".X") );
if( lgfile == NULL ) {
free( obuf );
perr( "malloc", NULL, errno );
close( fd );
return RET_ERROR;
}
/* read in all logfiles and backward fill obuf upto fetchlen_G chars */
for( i=-1; i<10; i++ ) {
/* i==-1: logfile without suffix, else i==logfile suffix */
if( i==-1 ) {
/* lgfile is already open and sb contains the stat */
strcpy( lgfile, logfile );
}else{
sprintf( lgfile, "%s.%1d", logfile, i );
if( (fd=open( lgfile, O_RDONLY )) == -1 ) {
if( errno==ENOENT && i>-1 ) { break; }
else{
perr( "open", lgfile, errno );
free( obuf ); free( lgfile );
return RET_ERROR;
}
}
if( fstat( fd, &sb ) == -1 ) {
perr( "stat", lgfile, errno );
free( obuf ); free( lgfile ); close( fd );
return RET_ERROR;
}
}
ilen = (size_t) sb.st_size;
if( ilen == 0 ) {
close( fd );
if( obm.inode == sb.st_ino ) break;
continue;
}
ibuf = mmap( NULL, ilen, PROT_READ, MAP_SHARED, fd, (off_t)0 );
if( ibuf == MAP_FAILED ) {
perr( "mmap", lgfile, errno );
free( obuf ); free( lgfile ); close( fd );
return RET_ERROR;
}
#ifdef HAS_MADVISE
if( madvise( ibuf, ilen, MADV_RANDOM ) ) {
perr( "madvise", NULL, errno );
free( obuf ); free( lgfile ); close( fd );
munmap( ibuf, ilen );
return RET_ERROR;
}
#endif
/* check for old bookmark */
bmpt = NULL;
if( obm.inode == sb.st_ino ) {
bmpt = ibuf+obm.last;
}
/* scan backwards for lines but the first */
done = 0;
for( llen=1,ipt=ibuf+ilen-2; ipt>=ibuf; llen++,ipt-- ) {
if( *ipt=='\n' ) {
if( ipt+1<bmpt ) { done=1; break; }
opos = copyline( opos, obuf, ipt+1, llen );
if( opos==0 ) { done=1; break; }
llen = 0;
}
}
/* copy first line ? */
if( ipt+1==ibuf && done==0 ) {
if( ipt+1<bmpt ) { done=1; }
else{ opos = copyline( opos, obuf, ipt+1, llen ); }
if( opos==0 ) { done=1; }
}
munmap( ibuf, ilen );
close( fd );
if( done ) break;
if( bmpt ) break; /* processed a bookmarked file? --> finito */
}
if( updbm_flag ) {
if( write_bookmark( bmfile, &nbm ) ) return RET_ERROR;
}
/* we only return a message if there is at least on char to print */
if( ((conv_G & CONV_NEWLINE)==0 && fetchlen_G-opos==0 ) ||
((conv_G & CONV_NEWLINE)!=0 && fetchlen_G-opos==3 ) ) {
if( conv_G & CONV_OKMSG ) {
write( STDOUT_FILENO, OK_MESSAGE "\n", sizeof( OK_MESSAGE ) );
}
i = RET_OK;
}else{
write( STDOUT_FILENO, obuf+opos,fetchlen_G-opos);
i = RET_NEWMSG;
}
free( obuf ); free( lgfile );
return i;
} /* fetch_logfile() */
/************************************************
* copyline( int opos, char *obuf, char *ipt, int illen );
* trim new line (ipt, illen), copy it to obuf, convert it, return new opos
************************************************
*/
int copyline( int opos, char *obuf, char *ipt, int illen ) {
char *p = NULL;
int len = 0;
int i;
#ifdef HAS_REGEX
int rxerr = 0;
char rxbuf[MAX_LASTCOL+1]; /* +1 for extra '\0' */
if( numrx_G > 0 ) {
/* we have to copy the line to another buffer because regexec()
wants a c-string, and we only have a \n terminated line in ipt.
Some platforms (e.g. FreeBSD) have a non-portable extension in
regcomp() which allows to regex non-null terminated string slices,
but to be portable...
*/
if( illen <= firstcol_G ) {
rxbuf[0] = '\0';
}else{
if( illen > lastcol_G ) len = lastcol_G;
else len = illen - 1;
len -= firstcol_G - 1;
memcpy( rxbuf, ipt+firstcol_G-1, len );
if( illen-1 > lastcol_G ) rxbuf[len-1] = '~';
rxbuf[len] = '\0';
}
for( i=0; i<numrx_G; i++ ) {
rxerr = regexec( &(rx_G[i]), rxbuf, (size_t)0, NULL, 0);
if( rxerr == 0 ) break; /* match */
/* else: no match (rxerr==REG_NOMATCH) or an error (ignored) */
}
if( rxerr != 0 ) return opos;
}
#endif
if( conv_G & CONV_NEWLINE ) {
/* fill obuf:
prepend concat( '\\' + 'n' + iline ) (newline sequence first) */
if( opos > 2 ) {
if( illen <= firstcol_G ) {
*(obuf+opos-1) = 'n'; *(obuf+opos-2) = '\\';
opos -= 2;
}else{
if( illen > lastcol_G ) len = lastcol_G;
else len = illen - 1;
len -= firstcol_G - 1;
if( len+2 > opos ) {
memcpy( obuf+2, ipt+firstcol_G-1+len+2-opos, opos-2 );
len = opos - 2;
}else{
memcpy( obuf+opos-len, ipt+firstcol_G-1, len );
}
if( illen-1 > lastcol_G ) *(obuf+opos-1) = '~';
opos -= len+2;
*(obuf+opos+1) = 'n';
*(obuf+opos+0) = '\\';
}
}else{
opos = 0;
}
if( opos==0 ) {
p = obuf;
*p++='\\'; *p++='n'; *p++='.'; *p++='.'; *p++='.';
if( obuf[5]=='n' ) { *p++='.'; }
}
}else{
/* without newline conversion */
/* fill obuf:
prepend concat( iline + '\n' ) (newline char last) */
if( opos > 1 ) {
if( illen <= firstcol_G ) {
*(obuf+opos-1) = '\n';
opos -= 1;
}else{
if( illen > lastcol_G ) len = lastcol_G;
else len = illen - 1;
len -= firstcol_G - 1;
if( len+1 > opos ) {
memcpy( obuf, ipt+firstcol_G-1+len+1-opos, opos-1 );
len = opos - 1;
}else{
memcpy( obuf+opos-len-1, ipt+firstcol_G-1, len );
}
*(obuf+opos-1) = '\n';
if( illen-1 > lastcol_G ) *(obuf+opos-2) = '~';
opos -= len+1;
}
}else{
opos = 0;
}
if( opos==0 ) {
p = obuf;
*p++='.'; *p++='.'; *p++='.';
}
}
p = obuf+opos;
if( conv_G & CONV_NEWLINE ) p += 2; /* +2: skip '\\' 'n' */
if( conv_G & CONV_PERCENT ) {
for( i=0; i<len; i++ ) {
if( *(p+i) == '%' ) *(p+i) = 'p';
}
}
if( conv_G & CONV_BRACKET ) {
for( i=0; i<len; i++ ) {
if( *(p+i) == '<' ) *(p+i) = '(';
else if( *(p+i) == '>' ) *(p+i) = ')';
}
}
if( conv_G & CONV_SHELL ) {
for( i=0; i<len; i++ ) {
if( *(p+i) == '$' ) *(p+i) = '_';
else if( *(p+i) == '\'' ) *(p+i) = '_';
else if( *(p+i) == '\"' ) *(p+i) = '_';
else if( *(p+i) == '`' ) *(p+i) = '_';
else if( *(p+i) == '^' ) *(p+i) = '_';
else if( *(p+i) == '\\' ) *(p+i) = '/';
}
}
return opos;
} /* copyline() */
/************************************************
* check_farg( char *farg )
* check if -f arg is in proper format for sccanf(): nnn:nnn:nnnn:XXXXX
************************************************
*/
int check_farg( char *farg, int *conv ) {
char *pt = farg;
int numdig = 0;
int numc = 0;
*conv = 0;
for( numdig=0 ; *pt; pt++ ) {
if( isdigit( (int) *pt ) ) numdig++;
else if( *pt==':' ) break;
else return 1;
}
if( numdig <1 || numdig > 3 ) return 1;
for( pt++,numdig=0 ; *pt; pt++ ) {
if( isdigit( (int) *pt ) ) numdig++;
else if( *pt==':' ) break;
else return 1;
}
if( numdig <1 || numdig > 3 ) return 1;
for( pt++,numdig=0 ; *pt; pt++ ) {
if( isdigit( (int) *pt ) ) numdig++;
else if( *pt==':' ) break;
else return 1;
}
if( numdig <1 || numdig > 4 ) return 1;
for( pt++,numc=0 ; *pt; pt++ ) {
if ( *pt=='b' ) { *conv |= CONV_BRACKET; numc++; }
else if( *pt=='p' ) { *conv |= CONV_PERCENT; numc++; }
else if( *pt=='n' ) { *conv |= CONV_NEWLINE; numc++; }
else if( *pt=='o' ) { *conv |= CONV_OKMSG; numc++; }
else if( *pt=='s' ) { *conv |= CONV_SHELL; numc++; }
else return 1;
}
if( numc > 5 ) return 1;
return 0; /* ok */
} /* check_farg() */
/*************************************************/
void usage( void ) {
printf(
"fetchlog - fetch the last new log messages - version %s \n\n"
" usage 1: fetchlog -f firstcol:lastcol:len:conv logfile bmfile [pattern ...]\n"
" usage 2: fetchlog -F firstcol:lastcol:len:conv logfile bmfile [pattern ...]\n"
" usage 3: fetchlog [ -V | -h ] \n\n"
"1: Read all messages of <logfile> [matching any of regex <pattern>] which are\n"
"newer than the bookmark in <bmfile>. Print at most <len> bytes from column\n"
"<firstcol> upto column <lastcol> to stdout. Adds '...' characters when skip-\n"
"ping old lines or '~' when cutting long lines. <conv> sets output conversion:\n"
" 'b': convert < and > to ( and ) for safe HTML output\n"
" 'p': convert %% to p for safe printf output\n"
" 's': convert $'\"`^\\ to _____/ for safe shell parameter input\n"
" 'n': convert newline to \\n for single line output\n"
" 'o': show '%s' message if no new messages\n"
" 0 < <firstcol> < <lastcol> < %d\n"
" <lastcol> - <firstcol> > %d \n"
" <len> valid range %d..%d\n"
" <conv> is zero ore more of 'bpsno' \n"
" <logfile> absolute path to logfile\n"
" <bmfile> absolute path to bookmarkfile\n"
"2: like 1 and update <bmfile> to remember fetched messages as 'read'\n"
"3: print version (-V) or print this help message (-h) \n"
, FL_VERSION,
OK_MESSAGE,MAX_LASTCOL+1,MIN_COLS-1,MIN_FETCHLEN, MAX_FETCHLEN
);
} /* usage() */
/*************************************************/
int main(int argc, char **argv)
{
int ret=0;
int i;
int rxerr;
/* check args */
if (argc == 2) {
if( argv[1][0] == '-' && argv[1][1] == 'V' ) {
printf( "fetchlog version %s \n", FL_VERSION );
exit( RET_PARAM );
}
}else if (argc >= 5) {
if( argv[1][0] == '-' &&
argv[3][0] == '/' && isalpha((int)argv[3][1]) &&
argv[4][0] == '/' && isalpha((int)argv[4][1]) ) {
if( argv[1][1] == 'f' || argv[1][1] == 'F' ) {
if( check_farg( argv[2], &conv_G ) ) {
usage();
exit( RET_PARAM );
}
ret = sscanf( argv[2], "%3d:%3d:%4d",
&firstcol_G, &lastcol_G, &fetchlen_G );
if( ret != 3 ) {
usage();
exit( RET_PARAM );
}
if( firstcol_G >= MIN_FIRSTCOL &&
lastcol_G >= firstcol_G + MIN_COLS - 1 &&
lastcol_G <= MAX_LASTCOL &&
fetchlen_G >= MIN_FETCHLEN &&
fetchlen_G <= MAX_FETCHLEN ) {
/* prepare regex, if any */
if( argc > 5 ) {
#ifdef HAS_REGEX
numrx_G = argc - 5;
rx_G = (regex_t*)malloc(numrx_G*sizeof(regex_t));
if( rx_G == NULL ) {
perr("malloc", NULL, errno );
exit( RET_ERROR );
}
for( i=5; i<argc; i++ ) {
rxerr = regcomp( &(rx_G[i-5]), argv[i],
REG_EXTENDED | REG_NOSUB );
if( rxerr ) {
regerror( rxerr, &(rx_G[i-5]),
rxerrbuf_G, RXERRBUFLEN );
perr("regex", rxerrbuf_G, 0 );
exit( RET_ERROR );
}
}
#else
i = 0; rxerr = 0;
perr("regex", "not supported on this platform", 0 );
exit( RET_ERROR );
#endif
}
if( argv[1][1] == 'f' ) {
exit( fetch_logfile( argv[3], argv[4], 0 ) );
}else{
exit( fetch_logfile( argv[3], argv[4], 1 ) );
}
}
}
}
}
usage();
return RET_PARAM;
} /* main() */
/*
* CVS/RCS Log:
* $Log: fetchlog.c,v $
* Revision 1.6 2004/03/26 19:46:03 afrika
* added regex pattern matching
*
* Revision 1.5 2003/11/19 15:24:19 afrika
* only return "new message" if there is at least one char to put out
*
* Revision 1.4 2003/11/18 18:44:23 afrika
* - removed compile option pre 0.93 exit codes
* - no longer use copy of last line fetched to find bookmark location
* - use inode to identify a (rotated) logfile instead
* - handling of empty (and rotated) logfiles now correct
*
* Revision 1.3 2002/12/17 18:40:05 afrika
* exit code now nagios compatible
*
* Revision 1.2 2002/12/17 18:04:48 afrika
* - inserted CVS tags
* - change docs: Netsaint --> Nagios(TM)
*
*
*/
syntax highlighted by Code2HTML, v. 0.9.1