From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <gentoo-catalyst+bounces-3328-garchives=archives.gentoo.org@lists.gentoo.org>
Received: from lists.gentoo.org (pigeon.gentoo.org [208.92.234.80])
	by finch.gentoo.org (Postfix) with ESMTP id 1978313888F
	for <garchives@archives.gentoo.org>; Fri,  9 Oct 2015 05:57:44 +0000 (UTC)
Received: from pigeon.gentoo.org (localhost [127.0.0.1])
	by pigeon.gentoo.org (Postfix) with SMTP id 47FF7E083B;
	Fri,  9 Oct 2015 05:57:36 +0000 (UTC)
Received: from smtp.gentoo.org (smtp.gentoo.org [140.211.166.183])
	(using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits))
	(No client certificate requested)
	by pigeon.gentoo.org (Postfix) with ESMTPS id 83EF6E080D
	for <gentoo-catalyst@lists.gentoo.org>; Fri,  9 Oct 2015 05:57:35 +0000 (UTC)
Received: from localhost.localdomain (localhost [127.0.0.1])
	by smtp.gentoo.org (Postfix) with ESMTP id 7E78C3406CE
	for <gentoo-catalyst@lists.gentoo.org>; Fri,  9 Oct 2015 05:57:32 +0000 (UTC)
From: Mike Frysinger <vapier@gentoo.org>
To: gentoo-catalyst@lists.gentoo.org
Subject: [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output
Date: Fri,  9 Oct 2015 01:57:27 -0400
Message-Id: <1444370248-13159-2-git-send-email-vapier@gentoo.org>
X-Mailer: git-send-email 2.5.2
In-Reply-To: <1444370248-13159-1-git-send-email-vapier@gentoo.org>
References: <1444370248-13159-1-git-send-email-vapier@gentoo.org>
Precedence: bulk
List-Post: <mailto:gentoo-catalyst@lists.gentoo.org>
List-Help: <mailto:gentoo-catalyst+help@lists.gentoo.org>
List-Unsubscribe: <mailto:gentoo-catalyst+unsubscribe@lists.gentoo.org>
List-Subscribe: <mailto:gentoo-catalyst+subscribe@lists.gentoo.org>
List-Id: Gentoo Linux mail <gentoo-catalyst.gentoo.org>
X-BeenThere: gentoo-catalyst@lists.gentoo.org
Reply-to: gentoo-catalyst@lists.gentoo.org
X-Archives-Salt: 754dd71f-e42b-4bf1-9430-13f6084222d7
X-Archives-Hash: 918dbe4353f9be00300ea52f306221bf

This has everything you could ever want:
- control where output is sent (stdout or a file)
- control over log level
- automatic exit when CRITICAL is used
- automatic colorization of critical/error/warning messages
- explicit control over use of colors
- automatic handling of embedded newlines
- standardized output format
- all logging routed through a single logger (not the root)
- additional log level between warning & info: notice

This just lays the groundwork -- no code is actually converted over
to using this.  That will be done in follow up commit(s).

Note: The default output level has changed from "info" to "notice".
That means the default display won't spam w/irrelevant details.

Example output (only the main.py module is converted):
$ ./bin/wrapper.py -s test
09 Oct 2015 01:34:56 EDT: NOTICE  : Using default Catalyst configuration file: /etc/catalyst/catalyst.conf
ContentsMap: __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l', 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f']
Creating Portage tree snapshot test from /usr/portage...
^C

$ ./bin/wrapper.py -s test --debug
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Catalyst git
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: vcs version 4440e8908c677d8764e29b0f127e2dd6c02b7621, date Fri Oct 9 01:28:19 2015 -0400
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Copyright 2003-2015 Gentoo Foundation
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Copyright 2008-2012 various authors
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Distributed under the GNU General Public License version 2.1
09 Oct 2015 01:35:43 EDT: NOTICE  : log.py:notice: Using default Catalyst configuration file: /etc/catalyst/catalyst.conf
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Snapshot cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Kernel cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Autoresuming support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Package cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Seed cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Envscript support enabled.
09 Oct 2015 01:35:43 EDT: DEBUG   : main.py:main: conf_values[options] = set(['snapcache', 'kerncache', 'autoresume', 'pkgcache', 'bindist', 'seedcache'])
^C

$ ./bin/wrapper.py -s test -C /asdf
09 Oct 2015 01:36:59 EDT: NOTICE  : Using default Catalyst configuration file: /etc/catalyst/catalyst.conf
ContentsMap: __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l', 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f']

!!! catalyst: Syntax error: 0

09 Oct 2015 01:36:59 EDT: CRITICAL: Could not parse commandline
---
 catalyst/log.py  | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 catalyst/main.py |  30 ++++++++++++-
 2 files changed, 158 insertions(+), 2 deletions(-)
 create mode 100644 catalyst/log.py

diff --git a/catalyst/log.py b/catalyst/log.py
new file mode 100644
index 0000000..bf39116
--- /dev/null
+++ b/catalyst/log.py
@@ -0,0 +1,130 @@
+# 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"""
+		#print(dir(record))
+		#print(record.getMessage())
+		msg = super(CatalystFormatter, self).format(record, **kwargs)
+		#print('{', msg, '}')
+		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)
diff --git a/catalyst/main.py b/catalyst/main.py
index e6b6447..c9a2219 100644
--- a/catalyst/main.py
+++ b/catalyst/main.py
@@ -18,6 +18,7 @@ from DeComp.definitions import (COMPRESS_DEFINITIONS, DECOMPRESS_DEFINITIONS,
 	CONTENTS_DEFINITIONS)
 from DeComp.contents import ContentsMap
 
+from catalyst import log
 import catalyst.config
 import catalyst.util
 from catalyst.defaults import confdefaults, option_messages
@@ -176,10 +177,23 @@ $ catalyst -f stage1-specfile.spec"""
 	group = parser.add_argument_group('Program output options')
 	group.add_argument('-d', '--debug',
 		default=False, action='store_true',
-		help='enable debugging')
+		help='enable debugging (and default --log-level debug)')
 	group.add_argument('-v', '--verbose',
 		default=False, action='store_true',
-		help='verbose output')
+		help='verbose output (and default --log-level info)')
+	group.add_argument('--log-level',
+		default=None,
+		choices=('critical', 'error', 'warning', 'notice', 'info', 'debug'),
+		help='set verbosity of output')
+	group.add_argument('--log-file',
+		type=FilePath(exists=False),
+		help='write all output to this file (instead of stdout)')
+	group.add_argument('--color',
+		default=None, action='store_true',
+		help='colorize output all the time (default: detect)')
+	group.add_argument('--nocolor',
+		dest='color', action='store_false',
+		help='never colorize output all the time (default: detect)')
 
 	group = parser.add_argument_group('Temporary file management')
 	group.add_argument('-a', '--clear-autoresume',
@@ -218,6 +232,18 @@ def main():
 	parser = get_parser()
 	opts = parser.parse_args(sys.argv[1:])
 
+	# Initialize the logger before anything else.
+	log_level = opts.log_level
+	if log_level is None:
+		if opts.debug:
+			log_level = 'debug'
+		elif opts.verbose:
+			log_level = 'info'
+		else:
+			log_level = 'notice'
+	log.setup_logging(log_level, output=opts.log_file, debug=opts.debug,
+		color=opts.color)
+
 	# Parse the command line options.
 	myconfig = opts.config
 	myspecfile = opts.file
-- 
2.5.2