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, mail 17 18import logging 19logger = logging.getLogger(__name__) 20 21 22locale.setlocale(locale.LC_ALL,'')# inherit system locale 23#DEG = "°".encode('unicode_escape') 24DEG = u'\N{DEGREE SIGN}' 25CEL ="C" 26TIMEFMT ="%X" 27DATEFMT ="%x" 28CORNERCHARS_DOUBLE = ['╚','╝','╗','╔'] 29CORNERCHARS_SINGLE = ['└','┘','┐','┌'] 30LINECHARS_DOUBLE = ['║','═'] 31LINECHARS_SINGLE = ['│','─'] 32BULLET ="• " 33INDENT =" " 34 35 36class Output: 37""" 38 Base class for a data processor. 39 """ 40 41def__init__(self): 42 self.content ="" 43 self.destination ="" 44 45defappend(self, content): 46""" 47 Add a string 48 """ 49 self.content += content 50 51defwrite(self, destination=""): 52""" 53 Output contents into a file 54 """ 55if destination =="": 56 destination = self.destination 57if destination =="": 58 logger.warning("No destination path provided") 59return1 60withopen(destination,'w')as f: 61 f.write(self.content) 62 logger.info("Written output to {}".format(destination)) 63 64 65classPlaintextOutput(Output): 66""" 67 Processes & outputs data in a plaintext form which can be read with cat or plaintext email. 68 """ 69 70def__init__(self, linewidth=80): 71 self.content ="" 72 self.destination ="" 73 self.linewidth = linewidth; 74 75defappend_header(self, template=''): 76""" 77 Print details with some primitive formatting 78 """ 79init_varfilter() 80 box =PlaintextBox(content=Template("$title $version on $hostname\n\n$time $date").safe_substitute(varsubst), vpadding=2, hpadding="\t\t", linewidth=config.prefs['linewidth']) 81 line =PlaintextLine(self.linewidth) 82 self.append(box.draw() + line.draw()) 83 84defappend_footer(self): 85""" 86 Append a horizontal line and some details 87 """ 88init_varfilter() 89 self.append(PlaintextLine(self.linewidth, vpadding=1).draw()) 90 self.append(Template("$hostname $time $date").safe_substitute(varsubst)) 91 92defappend_section(self, section): 93""" 94 Call the appropriate methods to format a section (provided by a parser). 95 This should be run by interface.py after every instance of parse_log(). 96 """ 97 self.append(PlaintextBox(content=section.title, double=False, fullwidth=False, vpadding=0, hpadding=" ").draw()) 98 self.append('\n'*2) 99for data in section.data: 100 self.append(self._fmt_data(data.subtitle, data.items)) 101 self.append('\n') 102 103def_fmt_data(self, subtitle, data =None):# write title and data 104""" 105 Format the properties of a data object into usable plaintext form with a few fancy symbols/formatting tricks. 106 Subtitle is required, data is not. If only subtitle is supplied or subtitle + one data item, a single line will be printed. 107 """ 108if(subtitle ==""): 109 logger.warning("No subtitle provided.. skipping section") 110return 111 112if(data ==None orlen(data) ==0): 113 logger.debug("No data provided.. just printing subtitle") 114return subtitle +'\n' 115else: 116 logger.debug("Received data "+str(data)) 117 subtitle +=':' 118if(len(data) ==1): 119return subtitle +' '+ data[0] +'\n' 120else: 121 itemoutput = subtitle +'\n' 122for datum in data: 123 datum = BULLET + datum 124iflen(datum) > config.prefs['linewidth'] -3: 125 words = datum.split() 126ifmax(map(len, words)) > config.prefs['linewidth'] -len(INDENT): 127continue 128 res, part, others = [], words[0], words[1:] 129for word in others: 130if1+len(word) > config.prefs['linewidth'] -len(part): 131 res.append(part) 132 part = word 133else: 134 part +=' '+ word 135if part: 136 res.append(part) 137 datum = ('\n').join(res) 138 datum = INDENT + datum 139 itemoutput += datum +'\n' 140return itemoutput 141 142 143classHtmlOutput(Output): 144""" 145 Process and output data in HTML format. 146 All HTML formatting functions now reside in this class to differentiate them from plaintext. 147 """ 148 149def__init__(self): 150 self.content ="" 151 self.destination ="" 152 self.css ="" 153 154defembed_css(self, css): 155""" 156 Convert stylesheet to inline tags 157 """ 158 self.content = mail.mailprep(self.content, css) 159return self.content 160 161defappend_header(self, template): 162""" 163 Insert variables into header template file and append HTML tags 164 """ 165init_varfilter() 166 headercontent =Template(open(template,'r').read()) 167 self.append(headercontent.safe_substitute(varsubst)) 168 self.append(self.opentag('div',id='main')) 169 170defappend_footer(self): 171""" 172 Close HTML tags that were opened in the template. 173 TODO: add footer template similar to header template. 174 """ 175 self.append(self.closetag('div') + self.closetag('body') + self.closetag('html')) 176 177defappend_section(self, section): 178""" 179 Call the appropriate methods to generate HTML tags for a section (provided by a parser). 180 This should be run by interface.py after every instance of parse_log(). 181 """ 182 self.append(self.opentag('div',1, section.title,'section')) 183 self.append(self._gen_title(section.title)) 184for data in section.data: 185 self.append(self._fmt_data(data.subtitle, data.items)) 186 self.append(self.closetag('div',1)) 187 188def_gen_title(self, title): 189""" 190 Format the title for a section 191 """ 192if(title ==''or'\n'in title): 193 logger.error("Invalid title") 194raiseValueError 195 logger.debug("Writing title for "+ title) 196return self.tag('h2',False, title) 197 198def_fmt_data(self, subtitle, data =None): 199""" 200 Format the properties of a data object into usable HTML tags. 201 Subtitle is required, data is not. If only subtitle is supplied or subtitle + one data item, a single line will be printed. 202 """ 203if(subtitle ==""): 204 logger.warning("No subtitle provided.. skipping section") 205return 206 207if(data ==None orlen(data) ==0): 208 logger.debug("No data provided.. just printing subtitle") 209return self.tag('p',False, subtitle) 210else: 211 logger.debug("Received data "+str(data)) 212 subtitle +=':' 213if(len(data) ==1): 214return self.tag('p',False, subtitle +' '+ data[0]) 215else: 216 output ="" 217 output += self.tag('p',False, subtitle) 218 output += self.opentag('ul',1) 219 coderegex = re.compile('`(.*)`') 220for datum in data: 221if datum ==""or datum ==None: 222continue 223 datum = coderegex.sub(r"<code>\1</code>",str(datum)) 224 output += self.tag('li',False, datum) 225 output += self.closetag('ul',True) 226return output 227 228defopentag(self, tag, block=False,id=None, cl=None): 229""" 230 Write HTML opening tag 231 """ 232 output ="" 233if(block): 234 output +='\n' 235 output +='<'+ tag 236if(id!=None): 237 output +=" id='"+id+"'" 238if(cl !=None): 239 output +=" class='"+ cl +"'" 240 output +='>' 241if(block): 242 output +='\n' 243return output 244 245defclosetag(self, tag, block=False): 246""" 247 Write HTML closing tag 248 """ 249if block: 250return"\n</"+ tag +">\n" 251else: 252return"</"+ tag +">" 253 254deftag(self, tag, block=False, content=""): 255""" 256 Write HTML opening tag, content, and closing tag 257 """ 258 o = self.opentag(tag, block) 259 c = self.closetag(tag, block) 260return o + content + c 261 262 263class Section: 264""" 265 Each parser should output a Section() which contains the title and returned data. 266 """ 267 268def__init__(self, title): 269 self.title = title 270 self.data = [] 271 272defappend_data(self, data): 273 self.data.append(data) 274 275 276class Data: 277""" 278 Each section (parser) can have one or more Data() objects which are essentially glorified lists. 279 """ 280 281def__init__(self, subtitle="", items=[]): 282 self.subtitle = subtitle 283 self.items = items 284 285deftruncl(self, limit):# truncate list 286""" 287 Truncate self.items to a specified value and state how many items are hidden. 288 """ 289if(len(self.items) > limit): 290 more =str(len(self.items) - limit) 291 self.items = self.items[:limit] 292 self.items.append("+{0}more".format(more)) 293 294deforderbyfreq(self): 295""" 296 Order a list by frequency of each item, then remove duplicates and append frequency in parentheses. 297 """ 298 unsorted =list(self.items) 299 self.items = ["{0}({1})".format(y, unsorted.count(y))for y insorted(set(unsorted), key =lambda x: -unsorted.count(x))] 300 301 302class PlaintextLine: 303""" 304 Draw a horizontal line for plain text format, with optional padding/styling. 305 """ 306 307def__init__(self, linewidth=80, double=True, vpadding=1, hpadding=""): 308 self.linewidth = linewidth 309 self.double =False 310 self.vpadding = vpadding 311 self.hpadding = hpadding 312 313defdraw(self): 314 line = (LINECHARS_DOUBLE[1]if self.double else LINECHARS_SINGLE[1]) 315return"\n"* self.vpadding + self.hpadding + line * (self.linewidth -2*len(self.hpadding)) + self.hpadding +"\n"* self.vpadding 316 317 318class PlaintextBox: 319""" 320 Draw a rectangular box around text, with customisable padding/size/style 321 """ 322 323def__init__(self, content="", double=True, fullwidth=True, linewidth=80, hpadding="\t", vpadding=1): 324 self.content = content 325 self.fullwidth = fullwidth 326 self.linewidth = linewidth 327 self.hpadding = hpadding 328 self.vpadding = vpadding 329 self.double = double 330 331defdraw(self): 332 333if self.double ==True: 334 cornerchars = CORNERCHARS_DOUBLE 335 linechars = LINECHARS_DOUBLE 336else: 337 cornerchars = CORNERCHARS_SINGLE 338 linechars = LINECHARS_SINGLE 339 340# Check hpadding has a definite width 341 self.hpadding = self.hpadding.replace("\t"," "*4) 342 343# Calculate number of characters per line 344 contentlines = self.content.splitlines() 345 contentwidth =int((self.linewidth if self.linewidth >0else80)if self.content.splitlines()elselen(max(contentlines, key=len))) 346 logger.debug("Contentwidth is{0}".format(str(contentwidth))) 347 logger.debug("Longest line is{0}".format(len(max(contentlines, key=len)))) 348 contentwidth += -2*(len(self.hpadding)+1) 349if not self.fullwidth: 350 longestline =len(max(contentlines, key=len)) 351if longestline <= self.linewidth -2*(len(self.hpadding)+1): 352 contentwidth = longestline 353 354# Split lines that are too long 355for i, line inenumerate(contentlines): 356iflen(line) > contentwidth: 357 words = line.split() 358ifmax(map(len, words)) > contentwidth: 359continue 360 res, part, others = [], words[0], words[1:] 361for word in others: 362iflen(' ') +len(word) > contentwidth -len(part): 363 res.append(part) 364 part = word 365else: 366 part +=' '+ word 367if part: 368 res.append(part) 369 contentlines[i] = res 370 371# Flatten list 372# Note list comprehension doesn't work here, so we must iterate through each item 373 newlines = [] 374for line in contentlines: 375ifisinstance(line,list): 376for subline in line: 377 newlines.append(subline) 378else: 379 newlines.append(line) 380 contentlines = newlines 381 382# Add vertical padding 383for _ inrange(self.vpadding): 384 contentlines.insert(0,' '*contentwidth) 385 contentlines.append(' '*contentwidth) 386 387# Insert horizontal padding on lines that are too short 388 contentlines = [linechars[0] + self.hpadding + x +' '*(self.linewidth-(len(x)+2*len(self.hpadding)+2)iflen(x) < contentwidth else0) + self.hpadding + linechars[0]for x in contentlines] 389 contentlines.insert(0, cornerchars[3] + linechars[1] * (contentwidth +len(self.hpadding)*2) + cornerchars[2]) 390 contentlines.append(cornerchars[0] + linechars[1] * (contentwidth +len(self.hpadding)*2) + cornerchars[1]) 391return('\n').join(contentlines) 392 393 394definit_varfilter(): 395global varfilter 396global varpattern 397global varsubst 398 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']))} 399 varfilter =dict((re.escape(k), v)for k, v in varfilter.items()) 400 varpattern = re.compile("|".join(varfilter.keys())) 401 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']))) 402 403defbackticks(l): 404return["`"+ x +"`"for x in l] 405 406 407defplural(noun, quantity): 408""" 409 Return "1 noun" or "n nouns" 410 """ 411if(quantity ==1): 412return(str(quantity) +" "+ noun) 413else: 414return(str(quantity) +" "+ noun +"s") 415 416 417defparsesize(num, suffix='B'): 418""" 419 Return human-readable size from number of bytes 420 """ 421for unit in['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: 422ifabs(num) <1024.0: 423return"%3.1f%s%s"% (num, unit, suffix) 424 num /=1024.0 425return"%.1f%s%s"% (num,'Yi', suffix) 426 427 428deffsubject(template): 429""" 430 Replace variables in the title template provided in config 431 """ 432 r = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], template) 433 logger.debug("Returning subject line "+ r) 434return r