#!/usr/bin/env python
"""
Re-format NetLogger log lines.
"""
__author__ = "Dan Gunter <dkgunter@lbl.gov>"
__rcsid__ = "$Id: nl_view 23923 2009-09-18 22:42:26Z ksb $"

import random
import sys
from netlogger.nllog import OptionParser, get_logger
from netlogger.parsers.base import NLFastParser

## Classes

class TinyIDMapper:
    """Map incoming identifiers to much shorter ones.
    """
    N = 4
    BASE = 'a'
    def __init__(self):
        self.t = { }
        random.seed(self.N)

    def shrink(self, guid):
        try:
            tiny_guid = self.t[guid]
        except KeyError:
            self.t[guid] = tiny_guid = self._tiny()
        return tiny_guid

    def _tiny(self):
        n, a = range(self.N), ord(self.BASE)
        while 1:
            x = [chr(a + random.randint(0, 25)) for i in n]
            s = ''.join(x)
            if not self.t.has_key(s):
                break
        return s

## Functions

def run(inf, outf, format=None, options=None, header=None, ts_fmt=None):
    long_ = options.fmt_long # 'long' format
    deltas = options.deltas or options.cumdeltas
    cum_deltas = options.cumdeltas
    ns_len = len(options.ns)
    parser = NLFastParser(inf, parse_date=deltas, strip_quotes=False)
    if deltas:
        last_ts = -1
    if options.ids and not options.all_attrs:
        # don't duplicate attributes that will be displayed
        # anyways because they end in '.id'
        options.attrs = filter(lambda a: not a.endswith('.id'),
                               options.attrs)
    if options.tinyid:
        tinyids = TinyIDMapper()
    else:
        tinyids = None
    if not long_ and header:
        outf.write(header)
        outf.write("\n")
    for d in parser:
        if not d: continue
        try:
            ts = d['ts']
            e = d['event']
        except KeyError:
            continue
        if deltas:
            if last_ts == -1:
                cur_ts = 0
            else:
                cur_ts = ts - last_ts
            if last_ts == -1 or not cum_deltas:
                last_ts = ts
            d['ts'] = cur_ts
        else:
            d['ts'] = ts[:10] + " " + ts[11:]
        if tinyids:
            for key in d.keys():
                if key == 'guid' or key.endswith(".id"):
                    d[key] = tinyids.shrink(d[key])
        if not d.has_key('guid') and options.add_guid:
            d['guid'] = '?'
        if options.add_level and not d.has_key('level'):
            d['level'] = 'INFO'
        if ns_len:
            e = d['event']
            if e.startswith(options.ns):
                d['event'] = e[ns_len:]
        if long_:
            outf.write(ts_fmt % d['ts'] + " " + d['event'] + "\n")
            for key in sorted(d.keys()):
                if key in ('ts', 'event'): continue
                outf.write('  ' + key + ': ' + str(d[key]) + "\n")
        else:
            outf.write(format % d)
            if options.all_attrs:
                first = True
                for key in d.keys():
                    if key in ('ts', 'event'): continue
                    if key == 'guid' and options.add_guid: continue
                    if key == 'level' and options.add_level: continue
                    if not first:
                        outf.write(' ')
                    else:
                        first = False
                    outf.write("%s=%s" % (key, d[key]))
            else:
                first = True
                if options.ids:
                    for key in d.keys():
                        if not first:
                            outf.write(' ')
                        else:
                            first = False
                        if key.endswith(".id"):
                            if options.noname:
                                outf.write("%s" % d[key])
                            else:
                                outf.write("%s=%s" % (key, d[key]))
                for attr in options.attrs:
                    if not first:
                        outf.write(' ')
                    else:
                        first = False
                    if d.has_key(attr):
                        if options.noname:
                            outf.write("%s" % d[attr])
                        else:
                            outf.write("%s=%s" % (attr, d[attr]))
        outf.write("\n")

def main():
    """ Main entry point """
    usage = "%prog [options] [files..]"
    desc = ' '.join(__doc__.split())
    parser = OptionParser(usage=usage, description=desc)
    parser.add_option('-a', '--attr', action="append", dest="attrs",
                      metavar="ATTR", default=[],
                      help="add attribute ATTR to output line, repeatable")
    parser.add_option('-A', '--all', action="store_true", dest="all_attrs",
                      default=False,
                      help="add all attributes to output line")
    parser.add_option('-c', '--cum-delta', action="store_true",
                      dest="cumdeltas", default=False,
                      help="show times as deltas since first (defalt=%default)")
    parser.add_option('-d', '--delta', action="store_true", dest="deltas",
                      default=False, help="show times as deltas from "
                      "previous (defalt=%default)")
    parser.add_option('-D', '--delimiter', dest="delim", default=" ",
                      help="column delimiter (default='%default')")
    parser.add_option('-e', '--long', dest="fmt_long", action="store_true",
                      help="Break each attribute onto its own line. "
                      "Voids other formatting options and implies '-A'.")
    parser.add_option('-g', '--guid',
                      action="store_true", dest="add_guid",
                      help="add 'guid' attribute")
    parser.add_option('-H', '--header', action="store_true", dest="hdr",
                      help="add header row (default=%default)", default=False)
    parser.add_option('-i', '--host',
                      action="store_true", dest="add_host",
                      help="add 'host' attribute")
    parser.add_option('-I', '--identifiers', action="store_true", dest="ids",
                      help="add any attribute ending in '.id'")
    parser.add_option('-l', '--level', dest="add_level", action="store_true",
                      help="add 'level' attribute'")
    parser.add_option('-m', '',
                      action='store_true', dest="add_msg",
                      default=False,
                      help="add 'msg' attribute")
    parser.add_option('-n', '--namespace', default="",
                      metavar="PREFIX", dest="ns",
                      help="strip namespace PREFIX if found")
    parser.add_option('-N', '--no-names', action='store_true',
                      dest="noname", default=False,
                      help="Do not show attribute names")
    parser.add_option('-s', '--status',
                      action='store_true', dest="add_status",
                      default=False,
                      help="add 'status' attribute")
    parser.add_option('-t', '--tiny-id',
                      action='store_true', dest="tinyid",
                      default=False,
                      help="replace *.id and guid values with shorter id's, "
                      "like tinyurl")
    parser.add_option('-w', '--width', metavar="NUM",
                      action='store', type='int', dest='event_width',
                      default=40,
                      help="set event column width to NUM (default=%default)")
    parser.add_option('-x', '',
                      action="store_true", dest="noecho",
                      default=False,
                      help="ignore non-NetLogger lines")
    options, args = parser.parse_args()
    log = get_logger(__file__)  # Should be first done, just after parsing args
    if options.tinyid and not (options.add_guid or options.ids):
        parser.error("option -t/--tiny-id given with no id's to reduce, "
                     "add -g/--guid or -I/--identifiers")
    if options.deltas:
        ts_fmt = "%11.06lf"
        ts_hdr = "%-11s"
    elif options.cumdeltas:
        ts_fmt = "%9.06lf"
        ts_hdr = "%-9s"
    else:
        ts_fmt = "%s"
        ts_hdr = "%-27s"
    format_bits = ["%%(ts)%s" % ts_fmt[1:],
                   "%%(event)-%ds" % options.event_width]
    header_bits = [ts_hdr % 'time', ('%%-%ds' % options.event_width) % 'event']
    if options.add_guid:
        if options.tinyid:
            format_bits.insert(1, "%%(guid)-%ds" % TinyIDMapper.N)
            header_bits.insert(1, ("%%-%ds" % TinyIDMapper.N) % 'guid')
        else:
            format_bits.insert(1, "%(guid)-36s")
            header_bits.insert(1, "%-36s" % 'guid')
    if options.add_level:
        format_bits.insert(1, "%(level)-8s")
        header_bits.insert(1, "%-8s" % 'level')
    for a in ('host', 'status', 'msg'):
        if getattr(options, 'add_' + a):
            options.attrs.append(a)
    if len(args) == 0:
        args = [None]
    fmt = options.delim.join(format_bits)
    if options.hdr:
        hdr = options.delim.join(header_bits)
    else:
        hdr = None
    if options.attrs or options.all_attrs:
        fmt += options.delim
        if options.hdr:
            hdr += options.delim + "attrs"
    outf = sys.stdout
    # Run
    log.info("run.start", args=args)
    for a in args:
        if a is None:
            infile = sys.stdin
        else:
            infile = file(a)
        log.debug("run.file.start", file=infile.name, format=fmt, header=hdr)
        run(infile, outf, format=fmt, header=hdr, options=options,
            ts_fmt=ts_fmt)
        log.debug("run.file.end", file=infile.name, status=0)
    log.info("run.end", status=0)

if __name__ == "__main__":
    sys.exit(main())
