rename parsers, better journald integration
[logparse.git] / logparse / formatting.py
index 8528e1b262413bc00dd477b62b7a8298ff7da47b..5b5367166fe120046cde95cc9437be67170e4c01 100644 (file)
@@ -2,9 +2,10 @@
 
 """   
 This file contains global functions for formatting and printing data. This file
-should be imported into individual log-parsing scripts located in logs/*. Data
-is formatted in HTML or plaintext. Writing to disk and/or emailng data is left
-to interface.py.
+should be imported into individual log-parsing scripts located in the default
+logparse.parsers module or in the user-supplied parsers directory. Data is
+formatted in HTML or plaintext. Writing to disk and/or emailng data is left to
+interface.py.
 """
 
 import os
@@ -13,6 +14,7 @@ import locale
 from string import Template
 from math import floor, ceil
 from tabulate import tabulate
+import textwrap
 
 import logparse
 from logparse import interface, util, mail, config
@@ -37,6 +39,7 @@ JXNCHARS_DOUBLE = ['╠', '╣', '╦', '╩', '╬']
 JXNCHARS_SINGLE = ['├', '┤', '┬', '┴', '┼']
 BULLET = "• "
 INDENT = "  "
+SPLIT_CHARS = ['.', '(', ')', '[', ']', '&', r"/", "\\", ',', '-', '_']
 
 
 global VARSUBST
@@ -66,7 +69,9 @@ def init_var():
         "hostname": util.hostname(config.prefs.get(
             "logparse", "hostname-path")),
         "version": logparse.__version__,
-        "css": css_path
+        "css": css_path,
+        "period": util.LogPeriod("logparse").startdate.strftime(
+            TIMEFMT + " " + DATEFMT)
     }
 
 
@@ -132,17 +137,17 @@ class PlaintextOutput(Output):
         Print details with some primitive formatting
         """
         box = PlaintextBox(content=
-                Template("$title $version on $hostname\n\n$time $date")
+                Template("$title $version on $hostname\n\n$time $date"
+                    "\nParsing logs since $period")
                 .safe_substitute(VARSUBST),
                 vpadding=2, hpadding="\t\t", linewidth=self.linewidth)
-        line = PlaintextLine(self.linewidth)
-        self.append(box.draw() + line.draw())
+        self.append(box.draw() + "\n"*2)
 
     def append_footer(self):
         """
         Append a horizontal line and some details
         """
-        self.append(PlaintextLine(self.linewidth, vpadding=1).draw())
+        self.append(PlaintextLine(self.linewidth).draw())
         self.append(Template("$hostname $time $date").safe_substitute(VARSUBST))
 
     def append_section(self, section):
@@ -151,6 +156,9 @@ class PlaintextOutput(Output):
         This should be run by interface.py after every instance of parse_log().
         """
 
+        if section == None:
+            logger.warning("Received null section")
+            return
         self.append(PlaintextBox(
             content=section.title, double=False,
             fullwidth=False, vpadding=0, hpadding=" ").draw())
@@ -177,35 +185,40 @@ class PlaintextOutput(Output):
             logger.warning("No subtitle provided.. skipping section")
             return
 
+        logger.debug("Processing data {}".format(subtitle))
+
         if (data == None or len(data) == 0):
-            logger.debug("No data provided.. just printing subtitle")
-            return subtitle + '\n'
+            # If no list items are provided, just print the subtitle
+            return subtitle + "\n"
+        elif (len(data) == 1):
+            # If only one item is provided, print it inline with subtitle
+            return self._wrap_datum("{}: {}".format(subtitle, data[0]),
+                    bullet=False, indent=False) + "\n"
         else:
-            logger.debug("Received data " + str(data))
-            subtitle += ':'
-            if (len(data) == 1):
-                return subtitle + ' ' + data[0] + '\n'
-            else:
-                itemoutput = subtitle + '\n'
-                for datum in data:
-                    datum = BULLET + datum
-                    if len(datum) > self.linewidth - 3:
-                        words = datum.split()
-                        if max(map(len, words)) > self.linewidth - len(INDENT):
-                            continue
-                        res, part, others = [], words[0], words[1:]
-                        for word in others:
-                            if 1 + len(word) > self.linewidth - len(part):
-                                res.append(part)
-                                part = word
-                            else:
-                                part += ' ' + word
-                        if part:
-                            res.append(part)
-                        datum = ('\n    ').join(res)
-                    datum = INDENT + datum
-                    itemoutput += datum + '\n'
-                return itemoutput
+            # If many items are provided, print them all as a bulleted list
+            itemoutput = subtitle + ":\n"
+            for datum in data:
+                itemoutput += self._wrap_datum(datum) + "\n"
+            return itemoutput
+
+    def _wrap_datum(self, text, bullet=True, indent=True):
+        """
+        Use cpython's textwrap module to limit line width to the value 
+        specified in self.linewidth. This is much easier than doing it all
+        from scratch (which I tried to do originally). Note that line 
+        continuations are automatically indented even if they don't have a 
+        bullet. This is to make it clear which lines are continuations.
+        """
+
+        wrapper = textwrap.TextWrapper(
+                initial_indent=(INDENT if indent else "") \
+                        + (BULLET if bullet else ""),
+                subsequent_indent=INDENT + (' '*len(BULLET) if bullet else ""),
+                width=self.linewidth,
+                replace_whitespace=True)
+
+        return wrapper.fill(text)
+
 
 class HtmlOutput(Output):
     """
@@ -279,6 +292,9 @@ class HtmlOutput(Output):
         instance of parse_log().
         """
 
+        if section == None:
+            logger.warning("Received null section")
+            return
         self.append(opentag('div', 1, section.title, 'section'))
         self.append(self._gen_title(section.title))
         if section.period and section.period.unique:
@@ -315,7 +331,7 @@ class HtmlOutput(Output):
             logger.debug("No data provided.. just printing subtitle")
             return tag('p', False, subtitle, cl="severity-" + str(severity))
         else:
-            logger.debug("Received data " + str(data))
+            logger.debug("Received data {}: {}".format(subtitle, data))
             subtitle += ':'
             if (len(data) == 1):
                 return tag('p', False, subtitle + ' ' + data[0],
@@ -400,9 +416,11 @@ class Data:
     def truncl(self, limit):      # truncate list
         """
         Truncate self.items to a specified value and state how many items are
-        hidden.
+        hidden. Set limit to -1 to avoid truncating any items.
         """
 
+        if limit == -1:
+            return self
         if (len(self.items) > limit):
             more = len(self.items) - limit
             if more == 1:
@@ -536,8 +554,8 @@ class Row(object):
 
 class Column(object):
     """
-    Object representing a single table cell. This is somewhat of a misnomer - 
-    one column object exists for each cell in the table. Columns are children
+    Object representing a single table cell. "Column" is somewhat of a misnomer 
+    one column object exists for each cell in the table. Columns are children
     of rows.
     """
 
@@ -556,7 +574,7 @@ class PlaintextLine:
     Draw a horizontal line for plain text format, with optional padding/styling.
     """
 
-    def __init__(self, linewidth=80, double=True, vpadding=1, hpadding=""):
+    def __init__(self, linewidth=80, double=True, vpadding=0, hpadding=""):
         """
         Initialise variables
         """
@@ -574,7 +592,7 @@ class PlaintextLine:
         line = (LINECHARS_DOUBLE[1] if self.double else LINECHARS_SINGLE[1])
         return "\n" * self.vpadding + self.hpadding \
                 +  line * (self.linewidth - 2 * len(self.hpadding)) \
-                + self.hpadding + "\n" * self.vpadding
+                + self.hpadding + "\n" * (self.vpadding + 1)
 
 
 class PlaintextBox:
@@ -616,7 +634,7 @@ class PlaintextBox:
         contentwidth = int((self.linewidth if self.linewidth > 0 else 80)
                 if self.content.splitlines()
                 else len(max(contentlines, key=len)))
-        logger.debug("Contentwidth is {0}".format(str(contentwidth)))
+        logger.debug("Content width is {0}".format(str(contentwidth)))
         logger.debug("Longest line is {0}".format(
             len(max(contentlines, key=len))))
         contentwidth += -2*(len(self.hpadding)+1)