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