aboutsummaryrefslogtreecommitdiff
blob: 871c53cf79a61f12389fd720d23530326a041caf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# Copyright 2003-2015 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

"""Logging related code

This largely exposes the same interface as the logging module except we add
another level "notice" between warning & info, and all output goes through
the "catalyst" logger.
"""

from __future__ import print_function

import logging
import logging.handlers
import os
import sys
import time


class CatalystLogger(logging.Logger):
	"""Override the _log member to autosplit on new lines"""

	def _log(self, level, msg, args, **kwargs):
		"""If given a multiline message, split it"""
		# We have to interpolate it first in case they spread things out
		# over multiple lines like: Bad Thing:\n%s\nGoodbye!
		msg %= args
		for line in msg.splitlines():
			super(CatalystLogger, self)._log(level, line, (), **kwargs)


# The logger that all output should go through.
# This is ugly because we want to not perturb the logging module state.
_klass = logging.getLoggerClass()
logging.setLoggerClass(CatalystLogger)
logger = logging.getLogger('catalyst')
logging.setLoggerClass(_klass)
del _klass


# Set the notice level between warning and info.
NOTICE = (logging.WARNING + logging.INFO) / 2
logging.addLevelName(NOTICE, 'NOTICE')


# The API we expose to consumers.
def notice(msg, *args, **kwargs):
	"""Log a notice message"""
	logger.log(NOTICE, msg, *args, **kwargs)

def critical(msg, *args, **kwargs):
	"""Log a critical message and then exit"""
	status = kwargs.pop('status', 1)
	logger.critical(msg, *args, **kwargs)
	sys.exit(status)

error = logger.error
warning = logger.warning
info = logger.info
debug = logger.debug


class CatalystFormatter(logging.Formatter):
	"""Mark bad messages with colors automatically"""

	_COLORS = {
		'CRITICAL':	'\033[1;35m',
		'ERROR':	'\033[1;31m',
		'WARNING':	'\033[1;33m',
		'DEBUG':	'\033[1;34m',
	}
	_NORMAL = '\033[0m'

	@staticmethod
	def detect_color():
		"""Figure out whether the runtime env wants color"""
		if 'NOCOLOR' is os.environ:
			return False
		return os.isatty(sys.stdout.fileno())

	def __init__(self, *args, **kwargs):
		"""Initialize"""
		color = kwargs.pop('color', None)
		if color is None:
			color = self.detect_color()
		if not color:
			self._COLORS = {}

		super(CatalystFormatter, self).__init__(*args, **kwargs)

	def format(self, record, **kwargs):
		"""Format the |record| with our color settings"""
		msg = super(CatalystFormatter, self).format(record, **kwargs)
		color = self._COLORS.get(record.levelname)
		if color:
			return color + msg + self._NORMAL
		else:
			return msg


def setup_logging(level, output=None, debug=False, color=None):
	"""Initialize the logging module using the |level| level"""
	# The incoming level will be things like "info", but setLevel wants
	# the numeric constant.  Convert it here.
	level = logging.getLevelName(level.upper())

	# The good stuff.
	fmt = '%(asctime)s: %(levelname)-8s: '
	if debug:
		fmt += '%(filename)s:%(funcName)s: '
	fmt += '%(message)s'

	# Figure out where to send the log output.
	if output is None:
		handler = logging.StreamHandler(stream=sys.stdout)
	else:
		handler = logging.FileHandler(output)

	# Use a date format that is readable by humans & machines.
	# Think e-mail/RFC 2822: 05 Oct 2013 18:58:50 EST
	tzname = time.strftime('%Z', time.localtime())
	datefmt = '%d %b %Y %H:%M:%S ' + tzname
	formatter = CatalystFormatter(fmt, datefmt, color=color)
	handler.setFormatter(formatter)

	logger.addHandler(handler)
	logger.setLevel(level)