logparse / formatting.pyon commit update parsers to new output model (94973e5)
   1#
   2#   format.py
   3#   
   4#   This file contains global functions for formatting and printing data. This
   5#   file should be imported into individual log-parsing scripts located in
   6#   logs/*. Data is formatted in HTML or plaintext. Writing to disk and/or
   7#   emailng data is left to __main__.py.
   8#
   9
  10import os
  11import re
  12import locale
  13from string import Template
  14
  15import logparse
  16from . import interface, util, config
  17
  18import logging
  19logger = logging.getLogger(__name__)
  20
  21locale.setlocale(locale.LC_ALL, '') # inherit system locale
  22#DEG = "°".encode('unicode_escape')
  23DEG = u'\N{DEGREE SIGN}'
  24CEL = "C"
  25TIMEFMT = "%X"
  26DATEFMT = "%x"
  27CORNERCHARS_DOUBLE = ['╚', '╝', '╗', '╔']
  28CORNERCHARS_SINGLE = ['└', '┘', '┐', '┌']
  29LINECHARS_DOUBLE = ['║', '═']
  30LINECHARS_SINGLE = ['│', '─']
  31
  32class Output:
  33    """
  34    Base class for a data processor. 
  35    """
  36    
  37    def __init__(self):
  38        self.content = ""
  39        self.destination = ""
  40
  41    def append(self, content):
  42        self.content += content
  43
  44    def write(self, destination=""):
  45        if destination == "":
  46            destination = self.destination
  47        if destination == "":
  48            logger.warning("No destination path provided")
  49            return 1
  50        with open(destination, 'w') as f:
  51            f.write(self.content)
  52            logger.info("Written output to {}".format(destination))
  53
  54
  55class PlaintextOutput(Output):
  56    """
  57    Processes & outputs data in a plaintext form which can be read with cat or plaintext email.
  58    """
  59
  60    def __init__(self, linewidth=80):
  61        self.content = ""
  62        self.destination = ""
  63        self.linewidth = linewidth;
  64
  65    def append_header(self, template=''):
  66        init_varfilter()
  67        box = PlaintextBox(content=Template("$title $version on $hostname\n\n$time $date").safe_substitute(varsubst), vpadding=2, hpadding="\t\t", linewidth=config.prefs['linewidth'])
  68        line = PlaintextLine(self.linewidth)
  69        self.append(box.draw() + line.draw())
  70
  71    def append_footer(self):
  72        init_varfilter()
  73        self.append(PlaintextLine(self.linewidth, vpadding=1).draw())
  74        self.append(Template("$hostname $time $date").safe_substitute(varsubst))
  75
  76    def append_section(self, section):
  77        self.append(PlaintextBox(content=section.title, double=False, fullwidth=False, vpadding=0, hpadding=" ").draw())
  78        self.append('\n'*2)
  79        for data in section.data:
  80            self.append(self._fmt_data(data.subtitle, data.items))
  81        self.append('\n')
  82
  83    def _fmt_data(self, subtitle, data = None):   # write title and data
  84        if (subtitle == ""):
  85            logger.warning("No subtitle provided.. skipping section")
  86            return
  87
  88        if (data == None or len(data) == 0):
  89            logger.debug("No data provided.. just printing subtitle")
  90            return subtitle + '\n'
  91        else:
  92            logger.debug("Received data " + str(data))
  93            subtitle += ':'
  94            if (len(data) == 1):
  95                return subtitle + ' ' + data[0] + '\n'
  96            else:
  97                itemoutput = subtitle + '\n'
  98                for datum in data:
  99                    datum = '• ' + datum
 100                    if len(datum) > config.prefs['linewidth']:
 101                        words = datum.split()
 102                        if max(map(len, words)) > config.prefs['linewidth']:
 103                            raise ValueError("Content width is too small")
 104                        res, part, others = [], words[0], words[1:]
 105                        for word in others:
 106                            if len(' ') + len(word) > config.prefs['linewidth'] - len(part):
 107                                res.append(part)
 108                                part = word
 109                            else:
 110                                part += ' ' + word
 111                        if part:
 112                            res.append(part)
 113                        datum = '\n'.join(res)
 114                    itemoutput += datum + '\n'
 115                return itemoutput
 116
 117
 118class HtmlOutput(Output):
 119    """
 120    Process and output data in HTML format.
 121    All HTML formatting functions now reside in this class to differentiate them from plaintext.
 122    """
 123
 124    def __init__(self):
 125        self.content = ""
 126        self.destination = ""
 127        self.css = ""
 128
 129    def embed_css(self, css):
 130        self.content = mail.mailprep(self.content, css)
 131
 132    def append_header(self, template):   # insert variables into header template file
 133        init_varfilter()
 134        headercontent = Template(open(template, 'r').read())
 135        self.append(headercontent.safe_substitute(varsubst))
 136        self.append(self.opentag('div', id='main'))
 137
 138    def append_footer(self):
 139        self.append(closetag('div') + closetag('body') + closetag('html'))
 140
 141    def append_section(self, section):
 142        self.append(self.opentag('div', 1, section.title, 'section'))
 143        self.append(self._gen_title(section.title))
 144        for data in section.data:
 145            self.append(self._fmt_data(data.subtitle, data.items))
 146        self.append(closetag('div', 1))
 147
 148    def _gen_title(self, title):  # write title for a section
 149        if (title == '' or '\n' in title):
 150            logger.error("Invalid title")
 151            raise ValueError 
 152        logger.debug("Writing title for " + title)
 153        return tag('h2', 0, title)
 154
 155    def _fmt_data(self, subtitle, data = None):   # write title and data
 156        if (subtitle == ""):
 157            logger.warning("No subtitle provided.. skipping section")
 158            return
 159
 160        if (data == None or len(data) == 0):
 161            logger.debug("No data provided.. just printing subtitle")
 162            return tag('p', 0, subtitle)
 163        else:
 164            logger.debug("Received data " + str(data))
 165            subtitle += ':'
 166            if (len(data) == 1):
 167                return tag('p', 0, subtitle + ' ' + data[0])
 168            else:
 169                output = ""
 170                output += tag('p', 0, subtitle)
 171                output += self.opentag('ul', 1)
 172                coderegex = re.compile('`(.*)`')
 173                for datum in data:
 174                    if datum == "" or datum == None:
 175                        continue
 176                    datum = coderegex.sub(r'<code>{\1}</code>', str(datum))
 177                    output += tag('li', 0, datum)
 178                output += closetag('ul', 1)
 179                return output
 180
 181    def opentag(self, tag, block = 0, id = None, cl = None):   # write html opening tag
 182        output = ""
 183        if (block):
 184            output += '\n'
 185        output += '<' + tag
 186        if (id != None):
 187            output += " id='" + id + "'"
 188        if (cl != None):
 189            output += " class='" + cl + "'"
 190        output += '>'
 191        if (block):
 192            output += '\n'
 193        return output
 194
 195    def closetag(self, tag, block = 0):  # write html closing tag
 196        if (block == 0):
 197            return "</" + tag + ">"
 198        else:
 199            return "\n</" + tag + ">\n"
 200
 201    def tag(self, tag, block = 0, content = ""):  # write html opening tag, content, and html closing tag
 202        o = self.opentag(tag, block)
 203        c = self.closetag(tag, block)
 204        return o + content + c
 205
 206
 207
 208class Section:
 209    """
 210    Each parser should output a Section() which contains the title and returned data.
 211    """
 212
 213    def __init__(self, title):
 214        self.title = title
 215        self.data = []
 216
 217    def add_data(self, data):
 218        self.data.append(data)
 219
 220class Data:
 221    """
 222    Each section (parser) can have one or more Data() objects which are essentially glorified lists.
 223    """
 224    
 225    def __init__(self, subtitle=None, items=[]):
 226        self.subtitle = subtitle
 227        self.items = items 
 228
 229    def truncl(self, limit):      # truncate list
 230        if (len(self.items) > limit):
 231            more = str(len(self.items) - limit)
 232            self.items = self.items[:limit]
 233            self.items..append("+ " + more + " more")
 234
 235    def orderbyfreq(self, l):     # order a list by the frequency of its elements and remove duplicates
 236        temp_l = l[:]
 237        l = list(set(l))
 238        l = [[i, temp_l.count(i)] for i in l]   # add count of each element
 239        l.sort(key=lambda x:temp_l.count(x[0])) # sort by count
 240        l = [i[0] + ' (' + str(i[1]) + ')' for i in l]  # put element and count into string
 241        l = l[::-1]     # reverse
 242        self.items = l 
 243
 244
 245class PlaintextLine:
 246    """
 247    Draw a horizontal line for plain text format, with optional padding/styling
 248    """
 249
 250    def __init__(self, linewidth=80, double=True, vpadding=1, hpadding=""):
 251        self.linewidth = linewidth
 252        self.double = False
 253        self.vpadding = vpadding
 254        self.hpadding = hpadding
 255
 256    def draw(self):
 257        line = (LINECHARS_DOUBLE[1] if self.double else LINECHARS_SINGLE[1])
 258        return "\n" * self.vpadding + self.hpadding +  line * (self.linewidth - 2 * len(self.hpadding)) + self.hpadding + "\n" * self.vpadding
 259
 260class PlaintextBox:
 261    """
 262    Draw a rectangular box around text, with customisable padding/size/style
 263    """
 264
 265    def __init__(self, content="", double=True, fullwidth=True, linewidth=80, hpadding="\t", vpadding=1):
 266        self.content = content
 267        self.fullwidth = fullwidth
 268        self.linewidth = linewidth
 269        self.hpadding = hpadding 
 270        self.vpadding = vpadding
 271        self.double = double
 272
 273    def draw(self):
 274
 275        if self.double == True:
 276            cornerchars = CORNERCHARS_DOUBLE
 277            linechars = LINECHARS_DOUBLE
 278        else:
 279            cornerchars = CORNERCHARS_SINGLE
 280            linechars = LINECHARS_SINGLE
 281
 282        # Check hpadding has a definite width
 283        self.hpadding = self.hpadding.replace("\t", " "*4)
 284
 285        # Calculate number of characters per line
 286        contentlines = self.content.splitlines()
 287        contentwidth = int((self.linewidth if self.linewidth > 0 else 80) if self.content.splitlines() else len(max(contentlines, key=len)))
 288        logger.debug("Contentwidth is {0}".format(str(contentwidth)))
 289        logger.debug("Longest line is {0}".format(len(max(contentlines, key=len))))
 290        contentwidth += -2*(len(self.hpadding)+1)
 291        if not self.fullwidth:
 292            longestline = len(max(contentlines, key=len))
 293            if longestline <= self.linewidth - 2*(len(self.hpadding)+1):
 294                contentwidth = longestline
 295
 296        # Split lines that are too long
 297        for i, line in enumerate(contentlines):
 298            if len(line) > contentwidth:
 299                words = line.split()
 300                if max(map(len, words)) > contentwidth:
 301                    raise ValueError("Content width is too small")
 302                res, part, others = [], words[0], words[1:]
 303                for word in others:
 304                    if len(' ') + len(word) > contentwidth - len(part):
 305                        res.append(part)
 306                        part = word
 307                    else:
 308                        part += ' ' + word
 309                if part:
 310                    res.append(part)
 311                contentlines[i] = res
 312
 313        # Flatten list
 314        #   Note list comprehension doesn't work here, so we must iterate through each item
 315        newlines = []
 316        for line in contentlines:
 317            if isinstance(line, list):
 318                for subline in line:
 319                    newlines.append(subline)
 320            else:
 321                newlines.append(line)
 322        contentlines = newlines
 323               
 324        # Add vertical padding
 325        for _ in range(self.vpadding):
 326            contentlines.insert(0, ' '*contentwidth)
 327            contentlines.append(' '*contentwidth)
 328
 329        # Insert horizontal padding on lines that are too short
 330        contentlines = [linechars[0] + self.hpadding + x + ' '*(self.linewidth-(len(x)+2*len(self.hpadding)+2) if len(x) < contentwidth else 0) + self.hpadding + linechars[0] for x in contentlines]
 331        contentlines.insert(0, cornerchars[3] + linechars[1] * (contentwidth + len(self.hpadding)*2) + cornerchars[2])
 332        contentlines.append(cornerchars[0] + linechars[1] * (contentwidth + len(self.hpadding)*2) + cornerchars[1])
 333        return ('\n').join(contentlines)
 334
 335def init_varfilter():
 336    global varfilter
 337    global varpattern
 338    global varsubst
 339    varfilter = {"$title$": config.prefs['title'], "$date$": interface.start.strftime(DATEFMT),"$time$": interface.start.strftime(TIMEFMT), "$hostname$": util.hostname(config.prefs['hostname-path']), "$version$": logparse.__version__, "$css$": os.path.relpath(config.prefs['css'], os.path.dirname(config.prefs['output']))}
 340    varfilter = dict((re.escape(k), v) for k, v in varfilter.items())
 341    varpattern = re.compile("|".join(varfilter.keys()))
 342    varsubst = dict(title=config.prefs['title'], date=interface.start.strftime(DATEFMT), time=interface.start.strftime(TIMEFMT), hostname=util.hostname(config.prefs['hostname-path']), version=logparse.__version__, css=os.path.relpath(config.prefs['css'], os.path.dirname(config.prefs['output'])))
 343
 344def writetitle(title):  # write title for a section
 345    if (title == '' or '\n' in title):
 346        logger.error("Invalid title")
 347        raise ValueError 
 348    logger.debug("Writing title for " + title)
 349    return tag('h2', 0, title)
 350
 351def addtag(l, tag):  # add prefix and suffix tags to each item in a list
 352    l2 = ['<' + tag + '>' + i + '</' + tag + '>' for i in l]
 353    return l2
 354
 355def backticks(l):
 356    return ["`" + x + "`" for x in l]
 357
 358def plural(noun, quantity): # return "1 noun" or "n nouns"
 359    if (quantity == 1):
 360        return(str(quantity) + " " + noun)
 361    else:
 362        return(str(quantity) + " " + noun + "s")
 363
 364def parsesize(num, suffix='B'):     # return human-readable size from number of bytes
 365    for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
 366        if abs(num) < 1024.0:
 367            return "%3.1f %s%s" % (num, unit, suffix)
 368        num /= 1024.0
 369    return "%.1f%s%s" % (num, 'Yi', suffix)
 370
 371def fsubject(template): # Replace variables in the title template provided in config
 372    r = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], template)
 373    logger.debug("Returning subject line " + r)
 374    return r