at 23.11-beta 3.5 kB view raw
1# mypy: disable-error-code="no-untyped-call" 2# drop the above line when mypy is upgraded to include 3# https://github.com/python/typeshed/commit/49b717ca52bf0781a538b04c0d76a5513f7119b8 4import codecs 5import os 6import sys 7import time 8import unicodedata 9from contextlib import contextmanager 10from queue import Empty, Queue 11from typing import Any, Dict, Iterator 12from xml.sax.saxutils import XMLGenerator 13 14from colorama import Fore, Style 15 16 17class Logger: 18 def __init__(self) -> None: 19 self.logfile = os.environ.get("LOGFILE", "/dev/null") 20 self.logfile_handle = codecs.open(self.logfile, "wb") 21 self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8") 22 self.queue: "Queue[Dict[str, str]]" = Queue() 23 24 self.xml.startDocument() 25 self.xml.startElement("logfile", attrs={}) 26 27 self._print_serial_logs = True 28 29 @staticmethod 30 def _eprint(*args: object, **kwargs: Any) -> None: 31 print(*args, file=sys.stderr, **kwargs) 32 33 def close(self) -> None: 34 self.xml.endElement("logfile") 35 self.xml.endDocument() 36 self.logfile_handle.close() 37 38 def sanitise(self, message: str) -> str: 39 return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C") 40 41 def maybe_prefix(self, message: str, attributes: Dict[str, str]) -> str: 42 if "machine" in attributes: 43 return f"{attributes['machine']}: {message}" 44 return message 45 46 def log_line(self, message: str, attributes: Dict[str, str]) -> None: 47 self.xml.startElement("line", attributes) 48 self.xml.characters(message) 49 self.xml.endElement("line") 50 51 def info(self, *args, **kwargs) -> None: # type: ignore 52 self.log(*args, **kwargs) 53 54 def warning(self, *args, **kwargs) -> None: # type: ignore 55 self.log(*args, **kwargs) 56 57 def error(self, *args, **kwargs) -> None: # type: ignore 58 self.log(*args, **kwargs) 59 sys.exit(1) 60 61 def log(self, message: str, attributes: Dict[str, str] = {}) -> None: 62 self._eprint(self.maybe_prefix(message, attributes)) 63 self.drain_log_queue() 64 self.log_line(message, attributes) 65 66 def log_serial(self, message: str, machine: str) -> None: 67 self.enqueue({"msg": message, "machine": machine, "type": "serial"}) 68 if self._print_serial_logs: 69 self._eprint(Style.DIM + f"{machine} # {message}" + Style.RESET_ALL) 70 71 def enqueue(self, item: Dict[str, str]) -> None: 72 self.queue.put(item) 73 74 def drain_log_queue(self) -> None: 75 try: 76 while True: 77 item = self.queue.get_nowait() 78 msg = self.sanitise(item["msg"]) 79 del item["msg"] 80 self.log_line(msg, item) 81 except Empty: 82 pass 83 84 @contextmanager 85 def nested(self, message: str, attributes: Dict[str, str] = {}) -> Iterator[None]: 86 self._eprint( 87 self.maybe_prefix( 88 Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL, attributes 89 ) 90 ) 91 92 self.xml.startElement("nest", attrs={}) 93 self.xml.startElement("head", attributes) 94 self.xml.characters(message) 95 self.xml.endElement("head") 96 97 tic = time.time() 98 self.drain_log_queue() 99 yield 100 self.drain_log_queue() 101 toc = time.time() 102 self.log(f"(finished: {message}, in {toc - tic:.2f} seconds)") 103 104 self.xml.endElement("nest") 105 106 107rootlog = Logger()