1# flake8: noqa 2# This Source Code Form is subject to the terms of the Mozilla Public 3# License, v. 2.0. If a copy of the MPL was not distributed with this file, 4# You can obtain one at http://mozilla.org/MPL/2.0/. 5 6from __future__ import absolute_import 7 8from logging import getLogger as getSysLogger 9from logging import * 10 11# Some of the build slave environments don't see the following when doing 12# 'from logging import *' 13# see https://bugzilla.mozilla.org/show_bug.cgi?id=700415#c35 14from logging import ( 15 getLoggerClass, 16 addLevelName, 17 setLoggerClass, 18 shutdown, 19 debug, 20 info, 21 basicConfig, 22) 23import json 24 25_default_level = INFO 26_LoggerClass = getLoggerClass() 27 28# Define mozlog specific log levels 29START = _default_level + 1 30END = _default_level + 2 31PASS = _default_level + 3 32KNOWN_FAIL = _default_level + 4 33FAIL = _default_level + 5 34CRASH = _default_level + 6 35# Define associated text of log levels 36addLevelName(START, "TEST-START") 37addLevelName(END, "TEST-END") 38addLevelName(PASS, "TEST-PASS") 39addLevelName(KNOWN_FAIL, "TEST-KNOWN-FAIL") 40addLevelName(FAIL, "TEST-UNEXPECTED-FAIL") 41addLevelName(CRASH, "PROCESS-CRASH") 42 43 44class MozLogger(_LoggerClass): 45 """ 46 MozLogger class which adds some convenience log levels 47 related to automated testing in Mozilla and ability to 48 output structured log messages. 49 """ 50 51 def testStart(self, message, *args, **kwargs): 52 """Logs a test start message""" 53 self.log(START, message, *args, **kwargs) 54 55 def testEnd(self, message, *args, **kwargs): 56 """Logs a test end message""" 57 self.log(END, message, *args, **kwargs) 58 59 def testPass(self, message, *args, **kwargs): 60 """Logs a test pass message""" 61 self.log(PASS, message, *args, **kwargs) 62 63 def testFail(self, message, *args, **kwargs): 64 """Logs a test fail message""" 65 self.log(FAIL, message, *args, **kwargs) 66 67 def testKnownFail(self, message, *args, **kwargs): 68 """Logs a test known fail message""" 69 self.log(KNOWN_FAIL, message, *args, **kwargs) 70 71 def processCrash(self, message, *args, **kwargs): 72 """Logs a process crash message""" 73 self.log(CRASH, message, *args, **kwargs) 74 75 def log_structured(self, action, params=None): 76 """Logs a structured message object.""" 77 if params is None: 78 params = {} 79 80 level = params.get("_level", _default_level) 81 if isinstance(level, int): 82 params["_level"] = getLevelName(level) 83 else: 84 params["_level"] = level 85 level = getLevelName(level.upper()) 86 87 # If the logger is fed a level number unknown to the logging 88 # module, getLevelName will return a string. Unfortunately, 89 # the logging module will raise a type error elsewhere if 90 # the level is not an integer. 91 if not isinstance(level, int): 92 level = _default_level 93 94 params["action"] = action 95 96 # The can message be None. This is expected, and shouldn't cause 97 # unstructured formatters to fail. 98 message = params.get("_message") 99 100 self.log(level, message, extra={"params": params}) 101 102 103class JSONFormatter(Formatter): 104 """Log formatter for emitting structured JSON entries.""" 105 106 def format(self, record): 107 # Default values determined by logger metadata 108 # pylint: disable=W1633 109 output = { 110 "_time": int(round(record.created * 1000, 0)), 111 "_namespace": record.name, 112 "_level": getLevelName(record.levelno), 113 } 114 115 # If this message was created by a call to log_structured, 116 # anything specified by the caller's params should act as 117 # an override. 118 output.update(getattr(record, "params", {})) 119 120 if record.msg and output.get("_message") is None: 121 # For compatibility with callers using the printf like 122 # API exposed by python logging, call the default formatter. 123 output["_message"] = Formatter.format(self, record) 124 125 return json.dumps(output, indent=output.get("indent")) 126 127 128class MozFormatter(Formatter): 129 """ 130 MozFormatter class used to standardize formatting 131 If a different format is desired, this can be explicitly 132 overriden with the log handler's setFormatter() method 133 """ 134 135 level_length = 0 136 max_level_length = len("TEST-START") 137 138 def __init__(self, include_timestamp=False): 139 """ 140 Formatter.__init__ has fmt and datefmt parameters that won't have 141 any affect on a MozFormatter instance. 142 143 :param include_timestamp: if True, include formatted time at the 144 beginning of the message 145 """ 146 self.include_timestamp = include_timestamp 147 Formatter.__init__(self) 148 149 def format(self, record): 150 # Handles padding so record levels align nicely 151 if len(record.levelname) > self.level_length: 152 pad = 0 153 if len(record.levelname) <= self.max_level_length: 154 self.level_length = len(record.levelname) 155 else: 156 pad = self.level_length - len(record.levelname) + 1 157 sep = "|".rjust(pad) 158 fmt = "%(name)s %(levelname)s " + sep + " %(message)s" 159 if self.include_timestamp: 160 fmt = "%(asctime)s " + fmt 161 # this protected member is used to define the format 162 # used by the base Formatter's method 163 self._fmt = fmt 164 return Formatter(fmt=fmt).format(record) 165 166 167def getLogger(name, handler=None): 168 """ 169 Returns the logger with the specified name. 170 If the logger doesn't exist, it is created. 171 If handler is specified, adds it to the logger. Otherwise a default handler 172 that logs to standard output will be used. 173 174 :param name: The name of the logger to retrieve 175 :param handler: A handler to add to the logger. If the logger already exists, 176 and a handler is specified, an exception will be raised. To 177 add a handler to an existing logger, call that logger's 178 addHandler method. 179 """ 180 setLoggerClass(MozLogger) 181 182 if name in Logger.manager.loggerDict: 183 if handler: 184 raise ValueError( 185 "The handler parameter requires " 186 + "that a logger by this name does " 187 + "not already exist" 188 ) 189 return Logger.manager.loggerDict[name] 190 191 logger = getSysLogger(name) 192 logger.setLevel(_default_level) 193 194 if handler is None: 195 handler = StreamHandler() 196 handler.setFormatter(MozFormatter()) 197 198 logger.addHandler(handler) 199 logger.propagate = False 200 return logger 201