diff --git a/src/opengnsys/linux/log.py b/src/opengnsys/linux/log.py index 393f5e8..934bfa9 100644 --- a/src/opengnsys/linux/log.py +++ b/src/opengnsys/linux/log.py @@ -34,6 +34,8 @@ import logging import os import tempfile +from ..log_format import JsonFormatter + # Logging levels OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) @@ -48,15 +50,25 @@ class LocalLogger(object): for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()): try: - fname = os.path.join(logDir, 'opengnsys.log') - logging.basicConfig( - filename=fname, - filemode='a', - format='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s', - level=logging.DEBUG - ) - self.logger = logging.getLogger('opengnsys') - os.chmod(fname, 0o0600) + fname1 = os.path.join (logDir, 'opengnsys.log') + fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s') + fh1 = logging.FileHandler (filename=fname1, mode='a') + fh1.setFormatter (fmt1) + fh1.setLevel (logging.DEBUG) + + fname2 = os.path.join (logDir, 'opengnsys.json.log') + fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='') + fh2 = logging.FileHandler (filename=fname2, mode='a') + fh2.setFormatter (fmt2) + fh2.setLevel (logging.DEBUG) + + self.logger = logging.getLogger ('opengnsys') + self.logger.setLevel (logging.DEBUG) + self.logger.addHandler (fh1) + self.logger.addHandler (fh2) + + os.chmod (fname1, 0o0600) + os.chmod (fname2, 0o0600) return except Exception: pass diff --git a/src/opengnsys/log_format.py b/src/opengnsys/log_format.py new file mode 100644 index 0000000..41e68de --- /dev/null +++ b/src/opengnsys/log_format.py @@ -0,0 +1,55 @@ +import json +import logging + +class JsonFormatter(logging.Formatter): + """ + Formatter that outputs JSON strings after parsing the LogRecord. + + @param dict fmt_dict: Key: logging format attribute pairs. Defaults to {"message": "message"}. + @param str time_format: time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S" + @param str msec_format: Microsecond formatting. Appended at the end. Default: "%s.%03dZ" + """ + def __init__(self, fmt_dict: dict = None, time_format: str = "%Y-%m-%dT%H:%M:%S", msec_format: str = "%s.%03dZ"): + self.fmt_dict = fmt_dict if fmt_dict is not None else {"message": "message"} + self.default_time_format = time_format + self.default_msec_format = msec_format + self.datefmt = None + + def usesTime(self) -> bool: + """ + Overwritten to look for the attribute in the format dict values instead of the fmt string. + """ + return "asctime" in self.fmt_dict.values() + + def formatMessage(self, record) -> dict: + """ + Overwritten to return a dictionary of the relevant LogRecord attributes instead of a string. + KeyError is raised if an unknown attribute is provided in the fmt_dict. + """ + return {fmt_key: record.__dict__[fmt_val] for fmt_key, fmt_val in self.fmt_dict.items()} + + def format(self, record) -> str: + """ + Mostly the same as the parent's class method, the difference being that a dict is manipulated and dumped as JSON + instead of a string. + """ + record.message = record.getMessage() + + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + + message_dict = self.formatMessage(record) + + if record.exc_info: + # Cache the traceback text to avoid converting it multiple times + # (it's constant anyway) + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + + if record.exc_text: + message_dict["exc_info"] = record.exc_text + + if record.stack_info: + message_dict["stack_info"] = self.formatStack(record.stack_info) + + return json.dumps(message_dict, default=str) diff --git a/src/opengnsys/windows/log.py b/src/opengnsys/windows/log.py index ea875fd..05f81f3 100644 --- a/src/opengnsys/windows/log.py +++ b/src/opengnsys/windows/log.py @@ -36,6 +36,8 @@ import logging import os import tempfile +from ..log_format import JsonFormatter + # Valid logging levels, from UDS Broker (uds.core.utils.log) OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) @@ -44,13 +46,24 @@ class LocalLogger(object): def __init__(self): # tempdir is different for "user application" and "service" # service wil get c:\windows\temp, while user will get c:\users\XXX\appdata\local\temp - logging.basicConfig( - filename=os.path.join(tempfile.gettempdir(), 'opengnsys.log'), - filemode='a', - format='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s', - level=logging.DEBUG - ) + + fname1 = os.path.join (tempfile.gettempdir(), 'opengnsys.log') + fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s') + fh1 = logging.FileHandler (filename=fname1, mode='a') + fh1.setFormatter (fmt1) + fh1.setLevel (logging.DEBUG) + + fname2 = os.path.join (tempfile.gettempdir(), 'opengnsys.json.log') + fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='') + fh2 = logging.FileHandler (filename=fname2, mode='a') + fh2.setFormatter (fmt2) + fh2.setLevel (logging.DEBUG) + self.logger = logging.getLogger('opengnsys') + self.logger.setLevel (logging.DEBUG) + self.logger.addHandler (fh1) + self.logger.addHandler (fh2) + self.serviceLogger = False def log(self, level, message):