Gentoo Archives: gentoo-catalyst

From: Brian Dolbec <dolsen@g.o>
To: gentoo-catalyst@l.g.o
Subject: Re: [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output
Date: Fri, 09 Oct 2015 16:31:30
Message-Id: 20151009093036.75026bc4.dolsen@gentoo.org
In Reply to: [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output by Mike Frysinger
1 On Fri, 9 Oct 2015 01:57:27 -0400
2 Mike Frysinger <vapier@g.o> wrote:
3
4 > This has everything you could ever want:
5 > - control where output is sent (stdout or a file)
6 > - control over log level
7 > - automatic exit when CRITICAL is used
8 > - automatic colorization of critical/error/warning messages
9 > - explicit control over use of colors
10 > - automatic handling of embedded newlines
11 > - standardized output format
12 > - all logging routed through a single logger (not the root)
13 > - additional log level between warning & info: notice
14 >
15 > This just lays the groundwork -- no code is actually converted over
16 > to using this. That will be done in follow up commit(s).
17 >
18 > Note: The default output level has changed from "info" to "notice".
19 > That means the default display won't spam w/irrelevant details.
20 >
21 > Example output (only the main.py module is converted):
22 > $ ./bin/wrapper.py -s test
23 > 09 Oct 2015 01:34:56 EDT: NOTICE : Using default Catalyst
24 > configuration file: /etc/catalyst/catalyst.conf ContentsMap:
25 > __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l',
26 > 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f'] Creating
27 > Portage tree snapshot test from /usr/portage... ^C
28 >
29 > $ ./bin/wrapper.py -s test --debug
30 > 09 Oct 2015 01:35:43 EDT: INFO : main.py:version: Catalyst git
31 > 09 Oct 2015 01:35:43 EDT: INFO : main.py:version: vcs version
32 > 4440e8908c677d8764e29b0f127e2dd6c02b7621, date Fri Oct 9 01:28:19
33 > 2015 -0400 09 Oct 2015 01:35:43 EDT: INFO : main.py:version:
34 > Copyright 2003-2015 Gentoo Foundation 09 Oct 2015 01:35:43 EDT:
35 > INFO : main.py:version: Copyright 2008-2012 various authors 09 Oct
36 > 2015 01:35:43 EDT: INFO : main.py:version: Distributed under the
37 > GNU General Public License version 2.1 09 Oct 2015 01:35:43 EDT:
38 > NOTICE : log.py:notice: Using default Catalyst configuration
39 > file: /etc/catalyst/catalyst.conf 09 Oct 2015 01:35:43 EDT: INFO :
40 > main.py:parse_config: Snapshot cache support enabled. 09 Oct 2015
41 > 01:35:43 EDT: INFO : main.py:parse_config: Kernel cache support
42 > enabled. 09 Oct 2015 01:35:43 EDT: INFO : main.py:parse_config:
43 > Autoresuming support enabled. 09 Oct 2015 01:35:43 EDT: INFO :
44 > main.py:parse_config: Package cache support enabled. 09 Oct 2015
45 > 01:35:43 EDT: INFO : main.py:parse_config: Seed cache support
46 > enabled. 09 Oct 2015 01:35:43 EDT: INFO : main.py:parse_config:
47 > Envscript support enabled. 09 Oct 2015 01:35:43 EDT: DEBUG :
48 > main.py:main: conf_values[options] = set(['snapcache', 'kerncache',
49 > 'autoresume', 'pkgcache', 'bindist', 'seedcache']) ^C
50 >
51 > $ ./bin/wrapper.py -s test -C /asdf
52 > 09 Oct 2015 01:36:59 EDT: NOTICE : Using default Catalyst
53 > configuration file: /etc/catalyst/catalyst.conf ContentsMap:
54 > __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l',
55 > 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f']
56 >
57 > !!! catalyst: Syntax error: 0
58 >
59 > 09 Oct 2015 01:36:59 EDT: CRITICAL: Could not parse commandline
60 > ---
61 > catalyst/log.py | 130
62 > +++++++++++++++++++++++++++++++++++++++++++++++++++++++
63 > catalyst/main.py | 30 ++++++++++++- 2 files changed, 158
64 > insertions(+), 2 deletions(-) create mode 100644 catalyst/log.py
65 >
66 > diff --git a/catalyst/log.py b/catalyst/log.py
67 > new file mode 100644
68 > index 0000000..bf39116
69 > --- /dev/null
70 > +++ b/catalyst/log.py
71 > @@ -0,0 +1,130 @@
72 > +# Copyright 2003-2015 Gentoo Foundation
73 > +# Distributed under the terms of the GNU General Public License v2
74 > +
75 > +"""Logging related code
76 > +
77 > +This largely exposes the same interface as the logging module except
78 > we add +another level "notice" between warning & info, and all output
79 > goes through +the "catalyst" logger.
80 > +"""
81 > +
82 > +from __future__ import print_function
83 > +
84 > +import logging
85 > +import logging.handlers
86 > +import os
87 > +import sys
88 > +import time
89 > +
90 > +
91 > +class CatalystLogger(logging.Logger):
92 > + """Override the _log member to autosplit on new lines"""
93 > +
94 > + def _log(self, level, msg, args, **kwargs):
95 > + """If given a multiline message, split it"""
96 > + # We have to interpolate it first in case they
97 > spread things out
98 > + # over multiple lines like: Bad Thing:\n%s\nGoodbye!
99 > + msg %= args
100 > + for line in msg.splitlines():
101 > + super(CatalystLogger, self)._log(level,
102 > line, (), **kwargs) +
103 > +
104 > +# The logger that all output should go through.
105 > +# This is ugly because we want to not perturb the logging module
106 > state. +_klass = logging.getLoggerClass()
107 > +logging.setLoggerClass(CatalystLogger)
108 > +logger = logging.getLogger('catalyst')
109 > +logging.setLoggerClass(_klass)
110 > +del _klass
111 > +
112 > +
113 > +# Set the notice level between warning and info.
114 > +NOTICE = (logging.WARNING + logging.INFO) / 2
115 > +logging.addLevelName(NOTICE, 'NOTICE')
116 > +
117 > +
118 > +# The API we expose to consumers.
119 > +def notice(msg, *args, **kwargs):
120 > + """Log a notice message"""
121 > + logger.log(NOTICE, msg, *args, **kwargs)
122 > +
123 > +def critical(msg, *args, **kwargs):
124 > + """Log a critical message and then exit"""
125 > + status = kwargs.pop('status', 1)
126 > + logger.critical(msg, *args, **kwargs)
127 > + sys.exit(status)
128 > +
129 > +error = logger.error
130 > +warning = logger.warning
131 > +info = logger.info
132 > +debug = logger.debug
133 > +
134 > +
135 > +class CatalystFormatter(logging.Formatter):
136 > + """Mark bad messages with colors automatically"""
137 > +
138 > + _COLORS = {
139 > + 'CRITICAL': '\033[1;35m',
140 > + 'ERROR': '\033[1;31m',
141 > + 'WARNING': '\033[1;33m',
142 > + 'DEBUG': '\033[1;34m',
143 > + }
144 > + _NORMAL = '\033[0m'
145 > +
146 > + @staticmethod
147 > + def detect_color():
148 > + """Figure out whether the runtime env wants color"""
149 > + if 'NOCOLOR' is os.environ:
150 > + return False
151 > + return os.isatty(sys.stdout.fileno())
152 > +
153 > + def __init__(self, *args, **kwargs):
154 > + """Initialize"""
155 > + color = kwargs.pop('color', None)
156 > + if color is None:
157 > + color = self.detect_color()
158 > + if not color:
159 > + self._COLORS = {}
160 > +
161 > + super(CatalystFormatter, self).__init__(*args,
162 > **kwargs) +
163 > + def format(self, record, **kwargs):
164 > + """Format the |record| with our color settings"""
165 > + #print(dir(record))
166 > + #print(record.getMessage())
167 > + msg = super(CatalystFormatter, self).format(record,
168 > **kwargs)
169 > + #print('{', msg, '}')
170 > + color = self._COLORS.get(record.levelname)
171 > + if color:
172 > + return color + msg + self._NORMAL
173 > + else:
174 > + return msg
175 > +
176 > +
177 > +def setup_logging(level, output=None, debug=False, color=None):
178 > + """Initialize the logging module using the |level| level"""
179 > + # The incoming level will be things like "info", but
180 > setLevel wants
181 > + # the numeric constant. Convert it here.
182 > + level = logging.getLevelName(level.upper())
183 > +
184 > + # The good stuff.
185 > + fmt = '%(asctime)s: %(levelname)-8s: '
186 > + if debug:
187 > + fmt += '%(filename)s:%(funcName)s: '
188 > + fmt += '%(message)s'
189 > +
190 > + # Figure out where to send the log output.
191 > + if output is None:
192 > + handler = logging.StreamHandler(stream=sys.stdout)
193 > + else:
194 > + handler = logging.FileHandler(output)
195 > +
196 > + # Use a date format that is readable by humans & machines.
197 > + # Think e-mail/RFC 2822: 05 Oct 2013 18:58:50 EST
198 > + tzname = time.strftime('%Z', time.localtime())
199 > + datefmt = '%d %b %Y %H:%M:%S ' + tzname
200 > + formatter = CatalystFormatter(fmt, datefmt, color=color)
201 > + handler.setFormatter(formatter)
202 > +
203 > + logger.addHandler(handler)
204 > + logger.setLevel(level)
205 > diff --git a/catalyst/main.py b/catalyst/main.py
206 > index e6b6447..c9a2219 100644
207 > --- a/catalyst/main.py
208 > +++ b/catalyst/main.py
209 > @@ -18,6 +18,7 @@ from DeComp.definitions import
210 > (COMPRESS_DEFINITIONS, DECOMPRESS_DEFINITIONS, CONTENTS_DEFINITIONS)
211 > from DeComp.contents import ContentsMap
212 >
213 > +from catalyst import log
214 > import catalyst.config
215 > import catalyst.util
216 > from catalyst.defaults import confdefaults, option_messages
217 > @@ -176,10 +177,23 @@ $ catalyst -f stage1-specfile.spec"""
218 > group = parser.add_argument_group('Program output options')
219 > group.add_argument('-d', '--debug',
220 > default=False, action='store_true',
221 > - help='enable debugging')
222 > + help='enable debugging (and default --log-level
223 > debug)') group.add_argument('-v', '--verbose',
224 > default=False, action='store_true',
225 > - help='verbose output')
226 > + help='verbose output (and default --log-level info)')
227 > + group.add_argument('--log-level',
228 > + default=None,
229 > + choices=('critical', 'error', 'warning', 'notice',
230 > 'info', 'debug'),
231 > + help='set verbosity of output')
232 > + group.add_argument('--log-file',
233 > + type=FilePath(exists=False),
234 > + help='write all output to this file (instead of
235 > stdout)')
236 > + group.add_argument('--color',
237 > + default=None, action='store_true',
238 > + help='colorize output all the time (default:
239 > detect)')
240 > + group.add_argument('--nocolor',
241 > + dest='color', action='store_false',
242 > + help='never colorize output all the time (default:
243 > detect)')
244 > group = parser.add_argument_group('Temporary file
245 > management') group.add_argument('-a', '--clear-autoresume',
246 > @@ -218,6 +232,18 @@ def main():
247 > parser = get_parser()
248 > opts = parser.parse_args(sys.argv[1:])
249 >
250 > + # Initialize the logger before anything else.
251 > + log_level = opts.log_level
252 > + if log_level is None:
253 > + if opts.debug:
254 > + log_level = 'debug'
255 > + elif opts.verbose:
256 > + log_level = 'info'
257 > + else:
258 > + log_level = 'notice'
259 > + log.setup_logging(log_level, output=opts.log_file,
260 > debug=opts.debug,
261 > + color=opts.color)
262 > +
263 > # Parse the command line options.
264 > myconfig = opts.config
265 > myspecfile = opts.file
266
267 Very nice. :) I learned a bit in here ;)
268
269 In gkeys I had it log to a file always, but didn't add color to the
270 console output.
271
272 --
273 Brian Dolbec <dolsen>