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