#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2010             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk 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 in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# Call with -d for debug mode: colored output, no saving of status

import sys,os,re

if '-d' in sys.argv[1:]:
    tty_red     = '\033[1;31m'
    tty_green   = '\033[1;32m'
    tty_yellow  = '\033[1;33m'
    tty_blue    = '\033[1;34m'
    tty_normal  = '\033[0m'
    debug = True
else:
    tty_red     = ''
    tty_green   = ''
    tty_yellow  = ''
    tty_blue    = ''
    tty_normal  = ''
    debug = False 

# The configuration file and status file are searched
# in the directory named by the environment variable
# LOGWATCH_DIR. If that is not set, MK_CONFDIR is used.
# If that is not set either, the current directory ist
# used.
logwatch_dir = os.getenv("LOGWATCH_DIR")
if not logwatch_dir:
    logwatch_dir = os.getenv("MK_CONFDIR")
    if not logwatch_dir:
        logwatch_dir = "."
    
print "<<<logwatch>>>"

config_filename = logwatch_dir + "/logwatch.cfg"
status_filename = logwatch_dir + "/logwatch.state"

def is_not_comment(line):
    if line.lstrip().startswith('#') or \
       line.strip() == '':
        return False
    return True

def parse_filenames(line):
    return line.split()

def parse_pattern(line):
    level, pattern = line.split(None, 1)
    try:
        compiled = re.compile(pattern)
    except:
        raise(Exception("Invalid regular expression in line '%s'" % line))
    if level not in [ 'C', 'W', 'I' ]:
        raise(Exception("Invalid pattern line '%s'" % line))
    return (level, compiled)

def read_config():
    config_lines = [ line.rstrip() for line in filter(is_not_comment, file(config_filename).readlines()) ]

    have_filenames = False
    config = []
    # Line starts with whitespace -> pattern line
    # otherwise -> file name line
    for line in config_lines:
        if line[0].isspace():
            if not have_filenames:
                raise Exception("Missing logfile names")
            patterns.append(parse_pattern(line))
        else:
            patterns = []
            config.append((parse_filenames(line), patterns))
            have_filenames = True
    return config

# structure of statusfile
# # LOGFILE         OFFSET    INODE
# /var/log/messages|7767698|32455445
# /var/test/x12134.log|12345|32444355
def read_status():
    status = {}
    for line in file(status_filename).readlines():
        # TODO: Remove variants with spaces. rsplit is
        # not portale. split fails if logfilename contains
        # spaces
        inode = -1
        try:
            parts = line.split('|')
            filename = parts[0]
            offset = parts[1]
            if len(parts) >= 3:
                inode = parts[2]

        except:
            try:
                filename, offset = line.rsplit(None, 1)
            except:
                filename, offset = line.split(None, 1)
        status[filename] = int(offset), int(inode)
    return status

def save_status(status):
    f = file(status_filename, "w")
    for filename, (offset, inode) in status.items():
        f.write("%s|%d|%d\n" % (filename, offset, inode))

def process_logfile(logfile, patterns):
    # Look at which file offset we have finished scanning
    # the logfile last time. If we have never seen this file
    # before, we set the offset to -1
    offset, prev_inode = status.get(logfile, (-1, -1))
    try:
        fl = os.open(logfile, os.O_RDONLY)
        inode = os.fstat(fl)[1] # 1 = st_ino
    except:
        print "[[[%s:cannotopen]]]" % logfile
        return

    print "[[[%s]]]" % logfile

    # Seek to the current end in order to determine file size
    current_end = os.lseek(fl, 0, 2) # os.SEEK_END not available in Python 2.4
    status[logfile] = current_end, inode
        
    # If we have never seen this file before, we just set the
    # current pointer to the file end. We do not want to make
    # a fuss about ancient log messages...
    if offset == -1:
	if not debug:
            return
    	else:
	    offset = 0
   

    # If the inode of the logfile has changed it has appearently
    # been started from new (logfile rotation). At least we must
    # assume that. In some rare cases (restore of a backup, etc)
    # we are wrong and resend old log messages
    if prev_inode >= 0 and inode != prev_inode:
        offset = 0

    # Our previously stored offset is the current end ->
    # no new lines in this file
    if offset == current_end:
        return # nothing new

    # If our offset is beyond the current end, the logfile has been
    # truncated or wrapped while keeping the same inode. We assume
    # that it contains all new data in that case and restart from
    # offset 0.
    if offset > current_end:
        offset = 0

    # now seek to offset where interesting data begins
    os.lseek(fl, offset, 0) # os.SEEK_SET not available in Python 2.4
    f = os.fdopen(fl)
    worst = 0
    outputtxt = ""
    for line in f.readlines():
        level = "."
        for lev, pattern in patterns:
            if pattern.search(line[:-1]):
                level = lev
                levelint = {'C': 2, 'W': 1, 'I': 0, '.': 0}[lev]
                worst = max(levelint, worst)
                break
        color = {'C': tty_red, 'W': tty_yellow, 'I': tty_blue, '.': ''}[level]
        outputtxt += "%s%s %s%s\n" % (color, level, line[:-1], tty_normal)
    new_offset = os.lseek(fl, 0, 1) # os.SEEK_CUR not available in Python 2.4
    status[logfile] = new_offset, inode

    # output all lines if at least one warning or error has been found
    if worst > 0:
        sys.stdout.write(outputtxt)
        sys.stdout.flush()

try:
    config = read_config()
except Exception, e:
    print "CANNOT READ CONFIG FILE: %s" % e
    sys.exit(1)

try:
    status = read_status()
except IOError:
    status = {}
except Exception, e:
    print "CANNOT PARSE STATUS FILE: %s" % e
    sys.exit(1)
    status = {}

for filenames, patterns in config:
    for glob in filenames:
        logfiles = [ l.strip() for l in os.popen("ls %s 2>/dev/null" % glob).readlines() ]
        if len(logfiles) == 0:
            print '[[[%s:missing]]]' % glob
        else:
            for logfile in logfiles:
                process_logfile(logfile, patterns)

if not debug:
    save_status(status)
