1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3 4""" 5This file contains global functions for formatting and printing data. This file 6should be imported into individual log-parsing scripts located in logs/*. Data 7is formatted in HTML or plaintext. Writing to disk and/or emailng data is left 8to interface.py. 9""" 10 11import os 12import re 13import locale 14from string import Template 15from math import floor, ceil 16from tabulate import tabulate 17 18import logparse 19from logparse import interface, util, mail, config 20 21import logging 22logger = logging.getLogger(__name__) 23 24 25locale.setlocale(locale.LC_ALL,'')# inherit system locale 26 27 28DEG = u'\N{DEGREE SIGN}' 29CEL ="C" 30TIMEFMT ="%X" 31DATEFMT ="%x" 32CORNERCHARS_DOUBLE = ['╚','╝','╗','╔'] 33CORNERCHARS_SINGLE = ['└','┘','┐','┌'] 34LINECHARS_DOUBLE = ['║','═'] 35LINECHARS_SINGLE = ['│','─'] 36JXNCHARS_DOUBLE = ['╠','╣','╦','╩','╬'] 37JXNCHARS_SINGLE = ['├','┤','┬','┴','┼'] 38BULLET ="• " 39INDENT =" " 40 41 42global VARSUBST 43 44 45definit_var(): 46""" 47 Initialise variable substitution templates (should be called before doing 48 any substitutions) 49 """ 50 51global VARSUBST 52 css_path = config.prefs.get("html","css") 53if config.prefs.getboolean("html","css-relpath"): 54if interface.argparser.parse_args().no_write: 55 css_path = os.path.relpath(css_path,".") 56elif interface.argparser.parse_args().destination: 57 css_path = os.path.relpath(css_path, interface.argparser.parse_args().destination()) 58elif config.prefs.get("logparse","output"): 59 css_path = os.path.relpath(css_path, config.prefs.get("logparse","output")) 60 VARSUBST = { 61"title": config.prefs.get("logparse","title"), 62"date": interface.start.strftime(DATEFMT), 63"time": interface.start.strftime(TIMEFMT), 64"hostname": util.hostname(config.prefs.get("logparse","hostname-path")), 65"version": logparse.__version__, 66"css": css_path 67} 68 69 70class Output: 71""" 72 Base class for a data processor. 73 """ 74 75def__init__(self): 76 self.content ="" 77 self.destination ="" 78 79defappend(self, content): 80""" 81 Add a string 82 """ 83 84 self.content += content 85 86defwrite(self, destination=""): 87""" 88 Output contents into a file 89 """ 90 91if destination =="": 92 destination = self.destination 93if destination =="": 94 logger.warning("No destination path provided") 95return1 96withopen(destination,'w')as f: 97 f.write(self.content) 98 logger.info("Written output to {}".format(destination)) 99 100defprint_stdout(self, lines=False): 101""" 102 Echo the contents to the console 103 """ 104 105print() 106if lines: 107 line =PlaintextLine(linewidth=config.prefs.getint("plain","linewidth"), double=True) 108print(line.draw()) 109print(self.content) 110if lines: 111print(line.draw()) 112print() 113 114 115classPlaintextOutput(Output): 116""" 117 Processes & outputs data in a plaintext form which can be read with cat or plaintext email. 118 """ 119 120def__init__(self, linewidth=80): 121 self.content ="" 122 self.destination ="" 123 self.linewidth = linewidth; 124 125defappend_header(self, template=''): 126""" 127 Print details with some primitive formatting 128 """ 129 box =PlaintextBox(content=Template("$title $version on $hostname\n\n$time $date").safe_substitute(VARSUBST), vpadding=2, hpadding="\t\t", linewidth=self.linewidth) 130 line =PlaintextLine(self.linewidth) 131 self.append(box.draw() + line.draw()) 132 133defappend_footer(self): 134""" 135 Append a horizontal line and some details 136 """ 137 self.append(PlaintextLine(self.linewidth, vpadding=1).draw()) 138 self.append(Template("$hostname $time $date").safe_substitute(VARSUBST)) 139 140defappend_section(self, section): 141""" 142 Call the appropriate methods to format a section (provided by a parser). 143 This should be run by interface.py after every instance of parse_log(). 144 """ 145 146 self.append(PlaintextBox(content=section.title, double=False, fullwidth=False, vpadding=0, hpadding=" ").draw()) 147if section.period and section.period.unique: 148 self.append("\n(since{0})".format(section.period.startdate.strftime(DATEFMT +" "+ TIMEFMT))) 149 self.append('\n'*2) 150for data in section.data: 151 self.append(self._fmt_data(data.subtitle, data.items)) 152 self.append('\n') 153for table in section.tables: 154 self.append(table.draw_plain()) 155 self.append("\n") 156 157def_fmt_data(self, subtitle, data =None):# write title and data 158""" 159 Format the properties of a data object into usable plaintext form with 160 a few fancy symbols/formatting tricks. Subtitle is required, data is 161 not. If only subtitle is supplied or subtitle + one data item, a single 162 line will be printed. 163 """ 164 165if(subtitle ==""): 166 logger.warning("No subtitle provided.. skipping section") 167return 168 169if(data ==None orlen(data) ==0): 170 logger.debug("No data provided.. just printing subtitle") 171return subtitle +'\n' 172else: 173 logger.debug("Received data "+str(data)) 174 subtitle +=':' 175if(len(data) ==1): 176return subtitle +' '+ data[0] +'\n' 177else: 178 itemoutput = subtitle +'\n' 179for datum in data: 180 datum = BULLET + datum 181iflen(datum) > self.linewidth -3: 182 words = datum.split() 183ifmax(map(len, words)) > self.linewidth -len(INDENT): 184continue 185 res, part, others = [], words[0], words[1:] 186for word in others: 187if1+len(word) > self.linewidth -len(part): 188 res.append(part) 189 part = word 190else: 191 part +=' '+ word 192if part: 193 res.append(part) 194 datum = ('\n').join(res) 195 datum = INDENT + datum 196 itemoutput += datum +'\n' 197return itemoutput 198 199classHtmlOutput(Output): 200""" 201 Process and output data in HTML format. All HTML formatting functions now 202 reside in this class to differentiate them from plain text. 203 """ 204 205def__init__(self): 206""" 207 Initialise variables (no parameters required for initialisation) 208 """ 209 210 self.content ="" 211 self.destination ="" 212 self.css ="" 213 self._embedded ="" 214 215defembed_css(self, css): 216""" 217 Convert stylesheet to inline tags 218 """ 219 220if not self._embedded: 221 self._embedded = mail.mailprep(re.sub(".*"+ re.escape(VARSUBST['css']) +".*\n","", self.content), css) 222return self._embedded 223 224defwrite_embedded(self, destination =""): 225""" 226 Write contents to file with inline CSS tags 227 """ 228 229 logger.debug("Writing HTML with embedded styles to "+ destination) 230if not self._embedded: 231 logger.warning("Call to write_embedded before embed_css - \ 232 embedding stylesheets immediately") 233 self.embed_css(config.prefs.get("html","css")) 234if destination =="": 235 destination = self.destination 236if destination =="": 237 logger.warning("No destination path provided") 238return1 239withopen(destination,'w')as f: 240 f.write(self._embedded) 241 logger.info("Written output to {}".format(destination)) 242 243 244defappend_header(self, template): 245""" 246 Insert variables into header template file and append HTML tags 247 """ 248 249 self.headertemplate = template 250 headercontent =Template(open(template,'r').read()) 251 self.append(headercontent.safe_substitute(VARSUBST)) 252 self.append(opentag('div',id='main')) 253 254defappend_footer(self): 255""" 256 Close HTML tags that were opened in the template. 257 TODO: add footer template similar to header template. 258 """ 259 260 self.append(closetag('div') +closetag('body') +closetag('html')) 261 262defappend_section(self, section): 263""" 264 Call the appropriate methods to generate HTML tags for a section (provided by a parser). 265 This should be run by interface.py after every instance of parse_log(). 266 """ 267 268 self.append(opentag('div',1, section.title,'section')) 269 self.append(self._gen_title(section.title)) 270if section.period and section.period.unique: 271 self.append(self._fmt_period(section.period)) 272for data in section.data: 273 self.append(self._fmt_data(data.subtitle, data.items, data.severity)) 274for table in section.tables: 275 self.append(table.draw_html()) 276 self.append(closetag('div',1)) 277 278def_gen_title(self, title): 279""" 280 Format the title for a section 281 """ 282 283if(title ==''or'\n'in title): 284 logger.error("Invalid title") 285raiseValueError 286 logger.debug("Writing title for "+ title) 287returntag('h2',False, title) 288 289def_fmt_data(self, subtitle, data=None, severity=0): 290""" 291 Format the properties of a data object into usable HTML tags. 292 Subtitle is required, data is not. If only subtitle is supplied or 293 subtitle + one data item, a single line will be printed. 294 """ 295 296if(subtitle ==""): 297 logger.warning("No subtitle provided.. skipping section") 298return 299 300if(data ==None orlen(data) ==0): 301 logger.debug("No data provided.. just printing subtitle") 302returntag('p',False, subtitle, cl="severity-"+str(severity)) 303else: 304 logger.debug("Received data "+str(data)) 305 subtitle +=':' 306if(len(data) ==1): 307returntag('p',False, subtitle +' '+ data[0], cl="severity-"+str(severity)) 308else: 309 output ="" 310 output +=tag('p',False, subtitle, cl="severity-"+str(severity)) 311 output +=opentag('ul',1) 312 coderegex = re.compile('`(.*)`') 313for datum in data: 314if datum ==""or datum ==None: 315continue 316 datum = coderegex.sub(r"<code>\1</code>",str(datum)) 317 output +=tag('li',False, datum) 318 output +=closetag('ul',True) 319return output 320 321def_fmt_period(self, period): 322 output ='' 323 output +=opentag('span', cl='period') 324 output +="since "+ period.startdate.strftime(DATEFMT +" "+ TIMEFMT) 325 output +=closetag('span') 326return output 327 328defprint_stdout_embedded(self, lines=False): 329""" 330 Echo the version with embedded style tags to the console 331 """ 332 333if self._embedded =="": 334 self.embed_css(config.prefs.get("html","css")) 335print() 336if lines: 337 line =PlaintextLine(linewidth=config.prefs.getint("plain","linewidth"), double=True) 338print(line.draw()) 339print(self._embedded) 340if lines: 341print(line.draw()) 342print() 343 344 345class Section: 346""" 347 Each parser should output a Section() which contains the title, returned 348 data, and applicable time period. 349 """ 350 351def__init__(self, title, period=None): 352 self.title = title 353 self.data = [] 354 self.tables = [] 355 self.period = util.LogPeriod(self.title) 356 357defappend_data(self, data): 358 self.data.append(data) 359 360defappend_table(self, table): 361 self.tables.append(table) 362 363 364class Data: 365""" 366 Each section (parser) can have one or more Data() objects which are 367 essentially glorified lists with titles (`self.subtitle`). 368 """ 369 370def__init__(self, subtitle="", items=[], severity=0): 371""" 372 Initialise variables. No parameters are enforced upon initialisation, 373 but at least the subtitle is required for valid output. Severity refers 374 to the importance of the data (integer from 0 to 5). e.g. a failed 375 system should have severity 5 and will be formatted appropriately by 376 the Output object. 377 """ 378 379 self.subtitle = subtitle 380 self.items = items 381 self.severity = severity 382 383deftruncl(self, limit):# truncate list 384""" 385 Truncate self.items to a specified value and state how many items are hidden. 386 """ 387 388if(len(self.items) > limit): 389 more =len(self.items) - limit 390if more ==1: 391return0 392 self.items = self.items[:limit] 393 self.items.append("+{0}more".format(str(more))) 394return self 395 396deforderbyfreq(self): 397""" 398 Order a list by frequency of each item, then remove duplicates and 399 append frequency in parentheses. 400 """ 401 402 unsorted =list(self.items) 403 self.items = ["{0}({1})".format(y, unsorted.count(y))for y insorted(set(unsorted), key =lambda x: -unsorted.count(x))] 404return self 405 406 407classTable(object): 408""" 409 A wrapper for python-tabulate's Tabulate type. 410 """ 411 412def__init__(self, double=False, borders=False, hpadding=" ", maxwidth=80, headers=[]): 413""" 414 Initialise variables. Note the keymap is used for a faster index map, 415 but is not currently used anywhere (may be removed in future). 416 """ 417 418 self.rows = []# List of row objects 419 self.keymap = {}# For fast lookup of row by value of first column 420 self.double = double 421 self.borders = borders 422 self.align_cols = [] 423 self.hpadding = hpadding 424 self.maxwidth = maxwidth 425 self.headers = headers 426 self._align_cols = [] 427 428defadd_row(self, row): 429""" 430 Append a row to the list and amend index mapping 431 """ 432 433 self.rows.append(row) 434iflen(row.columns) >0: 435 self.keymap[row.columns[0]] = row 436 437 logger.debug("Added row with{0}columns".format(str(len(row.columns)))) 438 439defalign_column(self, i, align): 440""" 441 Set alignment for the 'i'th column (`align` should be 'l', 'c' or 'r') 442 """ 443 444whilelen(self._align_cols) -1< i: 445 self._align_cols.append("") 446 self._align_cols[i] = align 447for row in self.rows: 448 row.columns[i].align = align 449 logger.debug("Column alignment is now{0}".format(str(self._align_cols))) 450 451def_gen_list(self): 452""" 453 Used locally for organising rows and columns into a 2D list structure 454 """ 455 456 hierarchy = [] 457for row in self.rows: 458 row_data = [] 459for column in row.columns: 460 row_data.append(column.content) 461 hierarchy.append(row_data) 462return hierarchy 463 464defdraw_html(self): 465""" 466 Output HTML string (wrapper for tabulate) 467 """ 468 469 output =tabulate(self._gen_list(), self.headers, tablefmt="html", colalign=tuple(self._align_cols)) 470return output 471 472defdraw_plain(self): 473""" 474 Output plain text string (wrapper for tabulate) 475 """ 476 477 output =tabulate(self._gen_list(), self.headers, tablefmt="fancy_grid"if self.borders else"plain", colalign=tuple(self._align_cols)) 478return output +"\n"*2 479 480 481classRow(object): 482""" 483 Object representing a literal row in a 2D table with the individual cells 484 in the row represented by columns[]. 485 """ 486 487def__init__(self, columns=[], header=False): 488""" 489 Initialise variables. The variable n is used locally to keep track of 490 the row width. 491 """ 492 493 self.columns = columns 494 self.header = header 495 self.n =len(self.columns) 496 497defadd_column(self, column): 498""" 499 Append a single cell horizontally and increment the cell count 500 """ 501 502 self.columns.append(column) 503 self.n +=1 504 505defrm_column(self, column): 506""" 507 Remove the specified column object and decrement the cell count 508 """ 509 510 self.remove(column) 511 self.n -=1 512 513 514classColumn(object): 515""" 516 Object representing a single table cell. This is somewhat of a misnomer - 517 one column object exists for each cell in the table. Columns are children 518 of rows. 519 """ 520 521def__init__(self, content="", align="right"): 522""" 523 Initialise variables. The align property sets the alignment of a single 524 cell ('l', 'c', or 'r'). 525 """ 526 527 self.content = content 528 self.align = align 529 530 531class PlaintextLine: 532""" 533 Draw a horizontal line for plain text format, with optional padding/styling. 534 """ 535 536def__init__(self, linewidth=80, double=True, vpadding=1, hpadding=""): 537""" 538 Initialise variables 539 """ 540 541 self.linewidth = linewidth 542 self.double = double 543 self.vpadding = vpadding 544 self.hpadding = hpadding 545 546defdraw(self): 547""" 548 Output a plain text string based on the current object parameters 549 """ 550 551 line = (LINECHARS_DOUBLE[1]if self.double else LINECHARS_SINGLE[1]) 552return"\n"* self.vpadding + self.hpadding + line * (self.linewidth -2*len(self.hpadding)) + self.hpadding +"\n"* self.vpadding 553 554 555class PlaintextBox: 556""" 557 Draw a rectangular box around text, with customisable padding/size/style 558 """ 559 560def__init__(self, content="", double=True, fullwidth=True, linewidth=80, hpadding="\t", vpadding=1): 561""" 562 Initialise variables 563 """ 564 self.content = content 565 self.fullwidth = fullwidth 566 self.linewidth = linewidth 567 self.hpadding = hpadding 568 self.vpadding = vpadding 569 self.double = double 570 571defdraw(self): 572""" 573 Output a plain text string based on the current object parameters. This 574 involves calculating the text width, breaking text at the maximum line 575 length, and then drawing a box around it all. 576 """ 577 578if self.double ==True: 579 cornerchars = CORNERCHARS_DOUBLE 580 linechars = LINECHARS_DOUBLE 581else: 582 cornerchars = CORNERCHARS_SINGLE 583 linechars = LINECHARS_SINGLE 584 585# Check hpadding has a definite width 586 self.hpadding = self.hpadding.replace("\t"," "*4) 587 588# Calculate number of characters per line 589 contentlines = self.content.splitlines() 590 contentwidth =int((self.linewidth if self.linewidth >0else80)if self.content.splitlines()elselen(max(contentlines, key=len))) 591 logger.debug("Contentwidth is{0}".format(str(contentwidth))) 592 logger.debug("Longest line is{0}".format(len(max(contentlines, key=len)))) 593 contentwidth += -2*(len(self.hpadding)+1) 594if not self.fullwidth: 595 longestline =len(max(contentlines, key=len)) 596if longestline <= self.linewidth -2*(len(self.hpadding)+1): 597 contentwidth = longestline 598 599# Split lines that are too long 600for i, line inenumerate(contentlines): 601iflen(line) > contentwidth: 602 words = line.split() 603ifmax(map(len, words)) > contentwidth: 604continue 605 res, part, others = [], words[0], words[1:] 606for word in others: 607iflen(' ') +len(word) > contentwidth -len(part): 608 res.append(part) 609 part = word 610else: 611 part +=' '+ word 612if part: 613 res.append(part) 614 contentlines[i] = res 615 616# Flatten list 617# Note list comprehension doesn't work here, so we must iterate through each item 618 newlines = [] 619for line in contentlines: 620ifisinstance(line,list): 621for subline in line: 622 newlines.append(subline) 623else: 624 newlines.append(line) 625 contentlines = newlines 626 627# Add vertical padding 628for _ inrange(self.vpadding): 629 contentlines.insert(0,' '*contentwidth) 630 contentlines.append(' '*contentwidth) 631 632# Insert horizontal padding on lines that are too short 633 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] 634 contentlines.insert(0, cornerchars[3] + linechars[1] * (contentwidth +len(self.hpadding)*2) + cornerchars[2]) 635 contentlines.append(cornerchars[0] + linechars[1] * (contentwidth +len(self.hpadding)*2) + cornerchars[1]) 636return('\n').join(contentlines) 637 638 639defbackticks(l): 640""" 641 Surround every item in a list by backticks. Used for showing code in both 642 HTML and plain text formats (converted to <code> tags for HTML) 643 """ 644 645return["`"+ x +"`"for x in l] 646 647 648defplural(noun, quantity, print_quantity=True): 649""" 650 Return "1 noun" or "n nouns" 651 """ 652 653if(quantity ==1): 654if print_quantity: 655return(str(quantity) +" "+ noun) 656else: 657return noun 658else: 659if noun.endswith("s"): 660 noun +="e" 661if print_quantity: 662return(str(quantity) +" "+ noun +"s") 663else: 664return noun +"s" 665 666 667defparsesize(num, suffix='B'): 668""" 669 Return human-readable size from number of bytes 670 """ 671 672for unit in['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: 673ifabs(num) <1024.0: 674return"%3.1f%s%s"% (num, unit, suffix) 675 num /=1024.0 676return"%.1f%s%s"% (num,'Yi', suffix) 677 678 679deffsubject(subject): 680""" 681 Replace variables in the title template provided in config 682 """ 683 684 r =Template(subject).safe_substitute(VARSUBST) 685 logger.debug("Returning subject line "+ r) 686return r 687 688 689defopentag(tag, block=False,id=None, cl=None, style=None): 690""" 691 Write HTML opening tag 692 """ 693 694 output ="" 695if block: 696 output +='\n' 697 output +='<'+ tag 698ifid: 699 output +=" id='"+id+"'" 700if cl: 701 output +=" class='"+ cl +"'" 702if style: 703 output +=" style='" 704 output +=" ".join("{0}:{1};".format(attr, value)for attr, value in style.items()) 705 output +="'" 706 output +='>' 707if block: 708 output +='\n' 709return output 710 711 712defclosetag(tag, block=False): 713""" 714 Write HTML closing tag 715 """ 716 717if block: 718return"\n</"+ tag +">\n" 719else: 720return"</"+ tag +">" 721 722 723deftag(tag, block=False, content="",id=None, cl=None, style=None): 724""" 725 Write HTML opening tag, content, and closing tag 726 """ 727 728 o =opentag(tag, block,id, cl, style) 729 c =closetag(tag, block) 730return o + content + c