diff options
Diffstat (limited to 'testing/mozbase/mozlog/mozlog/unstructured/logger.py')
-rw-r--r-- | testing/mozbase/mozlog/mozlog/unstructured/logger.py | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/testing/mozbase/mozlog/mozlog/unstructured/logger.py b/testing/mozbase/mozlog/mozlog/unstructured/logger.py new file mode 100644 index 0000000000..db436c9d11 --- /dev/null +++ b/testing/mozbase/mozlog/mozlog/unstructured/logger.py @@ -0,0 +1,191 @@ +# flake8: noqa +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import json + +# Some of the build slave environments don't see the following when doing +# 'from logging import *' +# see https://bugzilla.mozilla.org/show_bug.cgi?id=700415#c35 +from logging import * +from logging import addLevelName, basicConfig, debug +from logging import getLogger as getSysLogger +from logging import getLoggerClass, info, setLoggerClass, shutdown + +_default_level = INFO +_LoggerClass = getLoggerClass() + +# Define mozlog specific log levels +START = _default_level + 1 +END = _default_level + 2 +PASS = _default_level + 3 +KNOWN_FAIL = _default_level + 4 +FAIL = _default_level + 5 +CRASH = _default_level + 6 +# Define associated text of log levels +addLevelName(START, "TEST-START") +addLevelName(END, "TEST-END") +addLevelName(PASS, "TEST-PASS") +addLevelName(KNOWN_FAIL, "TEST-KNOWN-FAIL") +addLevelName(FAIL, "TEST-UNEXPECTED-FAIL") +addLevelName(CRASH, "PROCESS-CRASH") + + +class MozLogger(_LoggerClass): + """ + MozLogger class which adds some convenience log levels + related to automated testing in Mozilla and ability to + output structured log messages. + """ + + def testStart(self, message, *args, **kwargs): + """Logs a test start message""" + self.log(START, message, *args, **kwargs) + + def testEnd(self, message, *args, **kwargs): + """Logs a test end message""" + self.log(END, message, *args, **kwargs) + + def testPass(self, message, *args, **kwargs): + """Logs a test pass message""" + self.log(PASS, message, *args, **kwargs) + + def testFail(self, message, *args, **kwargs): + """Logs a test fail message""" + self.log(FAIL, message, *args, **kwargs) + + def testKnownFail(self, message, *args, **kwargs): + """Logs a test known fail message""" + self.log(KNOWN_FAIL, message, *args, **kwargs) + + def processCrash(self, message, *args, **kwargs): + """Logs a process crash message""" + self.log(CRASH, message, *args, **kwargs) + + def log_structured(self, action, params=None): + """Logs a structured message object.""" + if params is None: + params = {} + + level = params.get("_level", _default_level) + if isinstance(level, int): + params["_level"] = getLevelName(level) + else: + params["_level"] = level + level = getLevelName(level.upper()) + + # If the logger is fed a level number unknown to the logging + # module, getLevelName will return a string. Unfortunately, + # the logging module will raise a type error elsewhere if + # the level is not an integer. + if not isinstance(level, int): + level = _default_level + + params["action"] = action + + # The can message be None. This is expected, and shouldn't cause + # unstructured formatters to fail. + message = params.get("_message") + + self.log(level, message, extra={"params": params}) + + +class JSONFormatter(Formatter): + """Log formatter for emitting structured JSON entries.""" + + def format(self, record): + # Default values determined by logger metadata + # pylint: disable=W1633 + output = { + "_time": int(round(record.created * 1000, 0)), + "_namespace": record.name, + "_level": getLevelName(record.levelno), + } + + # If this message was created by a call to log_structured, + # anything specified by the caller's params should act as + # an override. + output.update(getattr(record, "params", {})) + + if record.msg and output.get("_message") is None: + # For compatibility with callers using the printf like + # API exposed by python logging, call the default formatter. + output["_message"] = Formatter.format(self, record) + + return json.dumps(output, indent=output.get("indent")) + + +class MozFormatter(Formatter): + """ + MozFormatter class used to standardize formatting + If a different format is desired, this can be explicitly + overriden with the log handler's setFormatter() method + """ + + level_length = 0 + max_level_length = len("TEST-START") + + def __init__(self, include_timestamp=False): + """ + Formatter.__init__ has fmt and datefmt parameters that won't have + any affect on a MozFormatter instance. + + :param include_timestamp: if True, include formatted time at the + beginning of the message + """ + self.include_timestamp = include_timestamp + Formatter.__init__(self) + + def format(self, record): + # Handles padding so record levels align nicely + if len(record.levelname) > self.level_length: + pad = 0 + if len(record.levelname) <= self.max_level_length: + self.level_length = len(record.levelname) + else: + pad = self.level_length - len(record.levelname) + 1 + sep = "|".rjust(pad) + fmt = "%(name)s %(levelname)s " + sep + " %(message)s" + if self.include_timestamp: + fmt = "%(asctime)s " + fmt + # this protected member is used to define the format + # used by the base Formatter's method + self._fmt = fmt + return Formatter(fmt=fmt).format(record) + + +def getLogger(name, handler=None): + """ + Returns the logger with the specified name. + If the logger doesn't exist, it is created. + If handler is specified, adds it to the logger. Otherwise a default handler + that logs to standard output will be used. + + :param name: The name of the logger to retrieve + :param handler: A handler to add to the logger. If the logger already exists, + and a handler is specified, an exception will be raised. To + add a handler to an existing logger, call that logger's + addHandler method. + """ + setLoggerClass(MozLogger) + + if name in Logger.manager.loggerDict: + if handler: + raise ValueError( + "The handler parameter requires " + + "that a logger by this name does " + + "not already exist" + ) + return Logger.manager.loggerDict[name] + + logger = getSysLogger(name) + logger.setLevel(_default_level) + + if handler is None: + handler = StreamHandler() + handler.setFormatter(MozFormatter()) + + logger.addHandler(handler) + logger.propagate = False + return logger |