/* count_log_lines.c Count log lines added to a file over the last 5 minutes. by Doke Scott, doke at udel.edu, 2008.10.2 $Id: count_log_lines.c,v 1.5 2014/03/27 17:59:03 doke Exp $ */ #ifdef __sun // has to come before includes for strptime to work right #define _STRPTIME_DONTZERO 1 #endif #include #include #include #include #include #ifdef __sun #include #endif #include #include #include #include #define __USE_XOPEN #ifdef __sun #define _STRPTIME_DONTZERO 1 #endif #include #define version "1.6" #define bufsize 262144 #define null NULL #define debug 1 #define STATE_OK 0 #define STATE_WARNING 1 #define STATE_CRITICAL 2 #define STATE_UNKNOWN -1 // defaults char *crit_range = "1000"; // critical if line count >= this char *warn_range = "500"; // warning if line count >= this int bytes_back = 100000; // bytes to seek back char *pattern = null; // user provided strptime date format string char *offset = "0.0"; // default offset in line, char *format_option = null; // user provided strptime date format string int period = 300; // seconds int emit = 0; // emit lines instead of counting them. // List of strptime() date formats to try, in reverse order // They'll be tried sequentially from the end to the beginning of the array. char *formats[] = { "%c", // locale default "%b %d %T", // Sep 30 10:27:07 "%Y-%m-%d %H:%M:%S", // 2009-02-06 16:28:35 "%d/%b/%Y:%T", // 29/May/2008:13:04:44 "%d/%b/%Y:%T %z", // 29/May/2008:13:04:44 -0400 doesn't work right "%a %b %d %T %Y", // Mon Nov 19 16:09:23 2007 "%a %b %d %T %Y %z", // Mon Nov 19 16:09:23 2007 EST null // will be replaced with command line format }; int verbose = 0; int quiet = 0; char *program_name; void usage( int ); int in_range( int count, char *range ) { int l = 0, h = 0; if ( strchr( range, ':' ) ) { if ( sscanf( range, "%u:%u", &l, &h ) == 2 ) { //printf( "count %d, l %d, h %d\n", count, l, h ); if ( count < l || h < count ) return 0; return 1; } else if ( sscanf( range, ":%u", &h ) == 1 ) { //printf( "count %d, l %d, h %d\n", count, l, h ); if ( h < count ) return 0; return 1; } else if ( sscanf( range, "%u:", &l ) == 1 ) { //printf( "count %d, l %d, h %d\n", count, l, h ); if ( count < l ) return 0; return 1; } } else if ( sscanf( range, "%u", &h ) == 1 ) { //printf( "count %d, l %d, h %d\n", count, l, h ); if ( h < count ) return 0; return 1; } fprintf( stderr, "cannot parse range '%s'\n", range ); usage( -1 ); } void parse_offset( char *offset, int *offset_field, int *offset_byte, int *offset_length ) { if ( sscanf( offset, "%u.%u.%u", offset_field, offset_byte, offset_length ) == 3 ) { // good } if ( sscanf( offset, "%u.%u", offset_field, offset_byte ) == 2 ) { *offset_length = 0; // good } else if ( sscanf( offset, "%u", offset_field ) == 1 ) { *offset_byte = 0; *offset_length = 0; } else { fprintf( stderr, "cannot parse offset '%s'\n", offset ); usage( -1 ); } } int count_log_lines( char *pathname, int bytes_back, int period, char *format_option, int offset_field, int offset_byte, int offset_length, int *found_at ) { register char *s; register int i; FILE *fp; char buf[ bufsize ], *buf2, *format; struct tm now_tm, then_tm; time_t now, start, then; int nformats, position, prev_position, file_size, j, k, l, lines = 0, total_lines = 0, stalled = 0; regex_t reg; int found = 0; int r; if ( pattern ) { r = regcomp( ®, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB ); if ( r ) { regerror( r, ®, buf, bufsize ); fprintf( stderr, "can't compile pattern '%s': %s\n", pattern, buf ); return -1; } } // open the file fp = fopen( pathname, "r" ); if ( ! fp ) { printf( "can't open pathname %s: %s\n", pathname, strerror( errno ) ); return -1; } nformats = sizeof( formats ) / sizeof( formats[0] ); if ( format_option ) { formats[ nformats - 1 ] = format_option; } else { nformats--; } if ( offset_length > 0 ) { buf2 = (char *) malloc( offset_length + 2 ); // +2 for \n and \0 } time( &now ); start = now - period; //TODO: option for UTC logs? localtime_r( &now, &now_tm ); if ( fseek( fp, 0, SEEK_END ) != 0 ) { // can't seek fprintf( stderr, "can't seek in pathname %s: %s\n", pathname, strerror( errno ) ); return -1; } file_size = ftell( fp ); if ( verbose ) fprintf( stderr, "file size %u bytes\n", file_size ); if ( file_size == 0 ) { return 0; // empty file, zero lines } position = file_size; prev_position = file_size; // try to find the starting point and date format for ( k = 10; k > 0; k-- ) { // seek back if ( verbose ) fprintf( stderr, "seeking to end - %u\n", bytes_back ); if ( fseek( fp, - bytes_back, SEEK_END ) != 0 ) { // can't seek, file not big enough? if ( fseek( fp, 0, 0 ) != 0 ) { fprintf( stderr, "can't seek in pathname %s: %s\n", pathname, strerror( errno ) ); return -1; } } prev_position = position; position = ftell( fp ); if ( verbose ) fprintf( stderr, "position %u, prev_position %u\n", position, prev_position ); // skip over the incomplete line we seeked into s = fgets( buf, bufsize, fp ); if ( ! s ) { if ( ferror( fp ) ) { fprintf( stderr, "can't read from pathname %s: %s\n", pathname, strerror( errno ) ); fclose( fp ); return -1; } else { // eof, need to move further back bytes_back *= 2; continue; } } if ( verbose ) fprintf( stderr, "skipping partial line, %d bytes\n", strlen( s ) ); //prev_position = position; position = ftell( fp ); if ( position >= prev_position ) { // Skipping the partial line brought us back to the initial seek point. bytes_back *= 2; continue; } // try 10 times to find a dated line for ( j = 10; j > 0; j-- ) { // get a complete line // FIXME: deal with all zeros from a large gap // Currently it will say it read something, but be unable to // parse it, try 10 times then fail. That will work for a tiny // gap, but not a big one produced by truncating a file while // something's writing to it in non-append mode. s = fgets( buf, bufsize, fp ); if ( ! s ) { if ( ferror( fp ) ) { fprintf( stderr, "can't read from pathname %s: %s\n", pathname, strerror( errno ) ); fclose( fp ); return -1; } else { // too close to eof to read a complete line, seek back break; } } if ( verbose ) fprintf( stderr, "> %s", s ); total_lines++; // find the offset in the line // todo: make this automatic? for ( i = 0; i < offset_field; i++ ) { s += strspn( s, " \t" ); s += strcspn( s, " \t" ); s += strspn( s, " \t" ); } if ( offset_byte < strlen( s ) ) s += offset_byte; if ( offset_length > 0 ) { strncpy( buf2, s, offset_length ); buf2[ offset_length ] = '\n'; // so we can print it in verbose buf2[ offset_length + 1 ] = 0; s = buf2; } if ( verbose && ( offset_field > 0 || offset_byte > 0 || offset_length > 0 ) ) fprintf( stderr, "o %s", s ); // try the various formats // this assumes all lines have the same date format for ( i = nformats - 1 ; i >= 0; i-- ) { format = formats[ i ]; bcopy( (void *) &now_tm, (void *) &then_tm, sizeof( struct tm ) ); if ( strptime( s, format, &then_tm ) ) // strptime worked, use this format string break; } if ( i >= 0 ) { if ( verbose ) fprintf( stderr, "using format '%s'\n", format ); break; } // couldn't parse date out of this line, loop around to try next line } if ( ! s ) { // too close to eof to read a complete line, seek back bytes_back *= 2; continue; } if ( j <= 0 ) { if ( ! quiet ) fprintf( stderr, "can't parse date string\n" ); fclose( fp ); return -1; } if ( verbose > 1 ) { fprintf( stderr, "then_tm: year %d, mon %d, mday %d, hour %d, min %d, sec %d, isdst %d\n", then_tm.tm_year, then_tm.tm_mon, then_tm.tm_mday, then_tm.tm_hour, then_tm.tm_min, then_tm.tm_sec, then_tm.tm_isdst ); } // see if we're back far enough in the file #ifdef __sun // This probably won't work right if the period // stradles the dst change. I HATE DST!!! // Ought to call mktime then if it set dst, subtract an hour and call // it again. But that's slower. //then_tm.tm_isdst = now_tm.tm_isdst; #endif then = mktime( &then_tm ); if ( then == -1 ) { fprintf( stderr, "unable to mktime: %s\n", strerror( errno ) ); continue; } if ( verbose ) fprintf( stderr, "then %d, %ds ago, %s", then, now - then, ctime( &then ) ); if ( then < start ) { // ok, we're back far enough if ( verbose ) fprintf( stderr, "\nok, back far enough, position = %d\n\n", position ); break; } position = ftell( fp ); if ( position == 0 ) { // can't seek back further, go with it if ( verbose ) fprintf( stderr, "not back far enough, but at start of file, continuing anyway\n" ); break; } else if ( position >= prev_position ) { if ( stalled ) { // can't seek back further, after second attempt, go with it. // // This probably happened because the log file got rotated. // We're not at position 0 because of the read that discards // the first (probably partial) line. So don't call this an // error. // // It might also have been truncated while something had it // open for normal write (not append). So the begining of the // file is a big gap, which reads as zeros. The read to // discard the incomplete line if ( verbose ) fprintf( stderr, "not back far enough, not making headway seeking " " further back, continuing anyway, position = %d, prev_position = %d\n", position, prev_position ); break; } else { if ( verbose ) fprintf( stderr, "stalled\n" ); stalled = 1; } } //prev_position = position; // Increase back bytes by the ratio of the time period we want over the time period we got, // assuming the log file ends at now and the logging rate is roughly uniform. // Plus arbitrary 10% because those assumptions are poor. if ( now > then ) { bytes_back *= 1.10 * period / ( now - then ); } else { // avoid divide by zero, and wierd forward dated logs bytes_back *= 2; } // now loop around to try seeking again. } if ( k <= 0 && verbose ) { fprintf( stderr, "exceeded maximum attempts to seek back far enough," " position = %d, continuing anyway\n", position ); } for (;;) { // get the next line position = ftell( fp ); s = fgets( buf, bufsize, fp ); if ( ! s ) { if ( ferror( fp ) ) { fprintf( stderr, "can't read from pathname %s: %s\n", pathname, strerror( errno ) ); fclose( fp ); return -1; } else { // eof fclose( fp ); if ( verbose ) fprintf( stderr, "total lines scanned %d\n", total_lines ); return lines; } } if ( verbose ) fprintf( stderr, "> %s", s ); total_lines++; if ( found == 0 ) { // havn't found a recent enough line yet, so parse the date // find the offset in the line for ( i = 0; i < offset_field; i++ ) { s += strspn( s, " \t" ); s += strcspn( s, " \t" ); s += strspn( s, " \t" ); } if ( offset_byte < strlen( s ) ) s += offset_byte; if ( offset_length > 0 ) { strncpy( buf2, s, offset_length ); buf2[ offset_length ] = '\n'; // so we can print it in verbose buf2[ offset_length + 1 ] = 0; s = buf2; } if ( verbose && ( offset_field > 0 || offset_byte > 0 || offset_length > 0 ) ) fprintf( stderr, "o %s", s ); bcopy( (void *) &now_tm, (void *) &then_tm, sizeof( struct tm ) ); if ( strptime( s, format, &then_tm ) ) { if ( verbose > 1 ) { fprintf( stderr, "then_tm: year %d, mon %d, mday %d, hour %d, min %d, sec %d, dst %d\n", then_tm.tm_year, then_tm.tm_mon, then_tm.tm_mday, then_tm.tm_hour, then_tm.tm_min, then_tm.tm_sec, then_tm.tm_isdst ); } #ifdef __sun // This probably won't work right if the period stradles // the DST change. I HATE DST!!! //then_tm.tm_isdst = now_tm.tm_isdst; #endif then = mktime( &then_tm ); if ( then == -1 ) { fprintf( stderr, "unable to mktime: %s\n", strerror( errno ) ); continue; } if ( verbose ) fprintf( stderr, "then %d, %ds ago, %s", then, now - then, ctime( &then ) ); if ( then > start ) { found = 1; *found_at = file_size - position ; if ( verbose ) fprintf( stderr, "\nfound recent line at byte %u of %u, %d\n\n", position, file_size, - *found_at ); if ( pattern && regexec( ®, buf, 0, null, 0 ) != 0 ) continue; lines++; if ( emit ) fputs( buf, stdout ); } } else { if ( ! quiet ) fprintf( stderr, "can't parse date string\n" ); } } else { // just accept any line after the first // one that parses and is new enough // This assumes the log file lines dates are non-decreaseing. if ( pattern && regexec( ®, buf, 0, null, 0 ) != 0 ) continue; lines++; if ( emit ) fputs( buf, stdout ); } } } void usage( int rc ) { fprintf( stderr, "Usage: %s [options] \n" " Nagios plugin that counts recent log lines, by seeking back from end,\n" " reading lines, and parsing the dates.\n" " Assumes most lines have dates, in the same format, in ascending order.\n" " Returns standard nagios plugin codes and output format.\n" " -P pat extended regular expression pattern that lines must match\n" " see regex(5) for extended regular expression syntax\n" " -c range critical if outside range [%s]\n" " -w range warning if outside range [%s]\n" " -p secs period, only count lines dated in the last seconds [%d]\n" " -b bytes how many bytes to seek back from end of file [%d]\n" " This is just an initial estimate. The program will\n" " make up to 10 attempts to seek further back to find\n" " an old enough starting line.\n" " -o offset offset of date string from start of each line [%s]\n" " as F[.B[.L]] where F is field number (whitespace seperated),\n" " B is byte in fieldfield, and L is length.\n" " Counts are zero referenced.\n" " example: -o 3.1 for Apache access_log\n" " -f format strptime(3) format for parsing dates\n" " Trys this first, then several common formats.\n" " -e emit the lines instead of counting them\n" " -q quiet, suppress warnings\n" " -v increase verbosity\n" " -h this help\n" " range is 1000, 200:1000, 200:, :1000, inclusive\n" " version %s, by Doke Scott, doke at udel dot edu\n", program_name, crit_range, warn_range, period, bytes_back, offset, version ); exit( rc ); } int main( int argc, char **argv ) { char *pathname, *msg; int c, rc, offset_field, offset_byte, offset_length, lines, found_at; program_name = argv[0]; while ( ( c = getopt( argc, argv, "P:c:w:p:b:o:f:eqvh" ) ) >= 0 ) { switch ( c ) { case 'P': pattern = optarg; break; case 'c': crit_range = optarg; break; case 'w': warn_range = optarg; break; case 'p': period = atoi( optarg ); break; case 'b': bytes_back = atoi( optarg ); break; case 'o': offset = optarg; break; case 'f': format_option = optarg; break; case 'e': emit = 1; break; case 'q': quiet++; break; case 'v': verbose++; break; case 'h': usage( 0 ); break; default: usage( -1 ); break; } } if ( optind >= argc ) usage( -1 ); pathname = argv[ optind ]; if ( verbose && quiet ) quiet = 0; // make sure they parse in_range( 0, crit_range ); in_range( 0, warn_range ); parse_offset( offset, &offset_field, &offset_byte, &offset_length ); found_at = bytes_back; lines = count_log_lines( pathname, bytes_back, period, format_option, offset_field, offset_byte, offset_length, &found_at ); if ( emit ) { if ( lines >= 0 ) return 0; return -1; } if ( lines < 0 ) { rc = STATE_UNKNOWN; printf( "UNKNOWN - can't get line count of %s\n", pathname ); return rc; } else if ( ! in_range( lines, crit_range ) ) { rc = STATE_CRITICAL; msg = "CRITICAL"; } else if ( ! in_range( lines, warn_range ) ) { rc = STATE_WARNING; msg = "Warning"; } else { rc = STATE_OK; msg = "Ok"; } if ( pattern ) // can't print pattern, because metacharacters might screw up nagios plugin output printf( "%s - %s %d lines matching pattern in last %d seconds and %d bytes | lines=%d\n", msg, pathname, lines, period, found_at, lines ); else printf( "%s - %s %d lines in last %d seconds and %d bytes | lines=%d\n", msg, pathname, lines, period, found_at, lines ); return rc; }