#!/usr/bin/env python
from __future__ import with_statement
"""
Parse log files and output Best-Practices format
"""

__rcsid__ = "$Id: nl_parser 980 2008-09-05 16:00:53Z dang $"
__author__ = "Dan Gunter (dkgunter (at) lbl.gov)"

# Imports
from copy import copy
import logging
from logging import INFO, DEBUG
from optparse import Option, OptionParser, make_option
import os
import re
import signal
import sys
import time
import uuid
import warnings
# Local imports
from netlogger import nlapi
from netlogger import nllog
from netlogger import util
from netlogger import pipeline
from netlogger.parsers import nlparser

# Symbolic name of program, for pipeline module
g_prog = pipeline.PROG_PARSER

# Always have a valid log object
log = nllog.NullLogger()

def activateLogging(name="netlogger.nl_parser", pipeline_started=False):
    """Activate the logging in the script.
    If the pipeline has started, use a different suffix so the same
    log destination (an error file in /tmp) is not re-used.
    """
    global log
    if pipeline_started:
        log = nllog.getLogger(name + ".pipeline")
    else:
        log = nllog.getScriptLogger(name + ".script")
        warnings.simplefilter("ignore", RuntimeWarning)

def activateAfterPipeline():
    activateLogging(pipeline_started=True)

# global application obj (for signal handling)
app = None

## Signal handlers ##

def killHandler(signo, frame):
    """Signal handler for a graceful exit."""
    app.close()
    with nllog.logged(log, "exit", level=logging.INFO):
        app.exit(0, g_prog, g_pidfile)

def rotateHandler(signo, frame):
    """Signal handler that rotates the output file."""
    with nllog.logged(log, "rotate", level=logging.INFO):
        app.rotate()

def reconfigHandler(signo, frame):
    """Signal handler that re-reads the configuration.
    """
    with nllog.logged(log, "reconfigure", level=logging.INFO):
        app.reconfigure()

def _exit(status):
    log.info("end", status=status)
    app.exit(status, g_prog, g_pidfile)

# Other functions

def parseArgs(shell_args):
    usage = "%prog [options] files..."
    parser = OptionParser(usage=usage, version="2.0")
    group = parser.add_option_group("General options")
    group.add_option('-c', '--config', 
                     action="store", dest="config", default=None,
                     metavar="FILE",
                     help="use configuration in FILE")
    group.add_option('-d', '--daemon', action='store', metavar='PIDFILE',
                     default=None, dest="daemon",
                     help="run in daemon mode, writing PID to PIDFILE. ")
    group.add_option('-D', '--desc', action='store', dest='help_name',
                     default=None, metavar="PARSER_MODULE",
                     help="describe parser PARSER_MODULE")
    group.add_option('-g', '--progress',
                     action="store", dest="progress", type="int",
                     default=0, metavar="NUM", 
                     help="report progress to standard error in increments "
                     "of NUM lines")
    group.add_option('--secret', action='store',
                     dest="secret_file", default=None, metavar='FILE',
                     help="For pipeline mode, FILE has authentication "
                     "secret to use with commands with nl_pipeline. Users "
                     "should not need to worry about this option.")
    group = parser.add_option_group("Command-line parser configuration")
    group.add_option('-e', '--header', action="store",  dest="mod_hdr",
                     metavar="EXPR",
                     help="extract header from each line using EXPR, "
                     "a regular expression")
    group.add_option('-m', '--module', action='store', dest='mod_name',
                     default=None, metavar="PARSER_MODULE",
                     help="parser module to use if not using -c")
    group.add_option('-n', '--no-action', action='store_true',
                     default=False, dest="noexec",
                     help="do not do anything, just show what would be done"
                     " (default=%default)")
    group.add_option('-p', '--param', action="append", dest="mod_param",
                     default=[],
                     help="parameter for module if not using -c, in the "
                     "format name=value. Repeatable. Any parameter not "
                     "used by the parsing module will end up as a "
                     "name=value appended to each line of output.")
    group.add_option('-t', '--throttle', action='store', type="float",
                     default=1.0,
                     help="maximum fraction of CPU to use (default=%default)")
    group.add_option('-u', '--unparsed-file', action='store', dest='ufile',
                     metavar="FILE", default=None,
                     help="file to store unparseable events in "
                     "(default=%default)")
    group.add_option('-v', '--verbose', action="count", default=0,
                     dest='verbosity', help="Verbose logging")
    group.add_option('-x', '--external', action='store_true', dest='ext',
                     help="Module is external, not in "
                     "netlogger.parsers.modules. Look first in '.'")
    options, args = parser.parse_args(shell_args)
    return parser, options, args

def parseOptions(options, args, parser):
    """Check for config-file/command-line conflicts
    and parse command-line options.
    """
    if options.config:
        if options.mod_name or options.mod_hdr or options.mod_param:
            warnings.warn("ignoring command-line configuration; using "
                          "configuration file instead")
        return
    options._args, options._params = [ ], [ ]
    # Parse command-line configuration
    # ~ look for requested parser module help
    if options.help_name is not None:
        showHelp(options.help_name, options, parser)
        sys.exit(0)
    # ~ check that parser module was given
    if options.mod_name is None:
        parser.error("either a parser module or configuration file "
                     "is required")
    # ~ parse name=value parameters, if any
    options._args = args
    for nameval in options.mod_param:
        try:
            name, val = nameval.split('=')
            options._params.append((name, val))
        except ValueError:
            parser.error("'%s': bad parameter format, use name=value" % 
                         nameval)

def showHelp(parsemod, options, parser):
    """Show help for parsing module 'parsemod'
    """
    options.mod_name = parsemod
    app = initApp(options, parser)
    if options.ext:
        full_parsemod = parsemod
    else:
        full_parsemod = "netlogger.parsers.modules.%s" % parsemod
    clazz = app.cfg.getModuleClass(full_parsemod)
    if clazz == None:
        parser.error("failed to load module '%s'" % parsemod)
    doc = clazz.__doc__
    if not doc:
        s = "No documentation found (contact module author and demand some!)"
    else:
        lines = doc.split("\n")
        if len(lines) == 1:
            summ, details = doc, "None"
        else:
            summ, details = lines[0], '\n'.join(lines[1:])
        s = "\nParser module '%s' (%s)\nSummary: %s\nDetails:\n%s" % (
                parsemod, full_parsemod, summ, details)
    print >>sys.stderr, s

def initApp(options, parser, *args):
    """Initialize application instance.
    
    If this fails, will write an error and exit.
    """
    app = nlparser.Application(options, *args)
    try:
        app.configure()
    except util.ConfigError,E:
        parser.error("configuration error: %s" % E)
    except ValueError, E:
        parser.error("configuration value error: %s" % E)
    return app

def initSignals():
    util.handleSignals(
        (killHandler, ('SIGTERM', 'SIGINT', 'SIGUSR2')),
        (rotateHandler, ('SIGUSR1','SIGALRM')),
        (reconfigHandler, ('SIGHUP',)) )

def main(shell_args):    
    """Program entry point.
    """
    global app, g_pidfile, log
    parser, options, args = parseArgs(shell_args)
    activateLogging()
    # Argument handling
    g_pidfile, is_daemonic = None, False
    if options.daemon:
        g_pidfile = pipeline.getPidFile(options.daemon, parser)
        # check that -d also includes -c
        if options.config is None:
            parser.error("daemon mode requires a configuration file")
        pipeline.daemonize(log, g_prog, g_pidfile)
        is_daemonic = True
    else:
        # set default logging level from -v flag
        log.setDefaultLevel(nllog.verbosityToLevel(options.verbosity))
    # Do more argument parsing
    parseOptions(options, args, parser)
    # Init, part 1
    app = initApp(options, parser, activateAfterPipeline)
    # Stop if a dry run
    if options.noexec:
        pipeline.dumpConfigAndExit(app)
    # Init, part 2
    initSignals()
    if is_daemonic:
        app.addEofMarkers()
    # Run
    status = app.run()
    _exit(status) 

if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
