473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
1 |
# ubuntuone.syncdaemon.logger - logging utilities
|
2 |
#
|
|
3 |
# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
|
|
4 |
#
|
|
1222.1.1
by Rodney Dawes
Add required OpenSSL license exception text |
5 |
# Copyright 2010-2012 Canonical Ltd.
|
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
6 |
#
|
7 |
# This program is free software: you can redistribute it and/or modify it
|
|
8 |
# under the terms of the GNU General Public License version 3, as published
|
|
9 |
# by the Free Software Foundation.
|
|
10 |
#
|
|
11 |
# This program is distributed in the hope that it will be useful, but
|
|
12 |
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
|
13 |
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
|
14 |
# PURPOSE. See the GNU General Public License for more details.
|
|
15 |
#
|
|
16 |
# You should have received a copy of the GNU General Public License along
|
|
17 |
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
1222.1.1
by Rodney Dawes
Add required OpenSSL license exception text |
18 |
#
|
19 |
# In addition, as a special exception, the copyright holders give
|
|
20 |
# permission to link the code of portions of this program with the
|
|
21 |
# OpenSSL library under certain conditions as described in each
|
|
22 |
# individual source file, and distribute linked combinations
|
|
23 |
# including the two.
|
|
24 |
# You must obey the GNU General Public License in all respects
|
|
25 |
# for all of the code used other than OpenSSL. If you modify
|
|
26 |
# file(s) with this exception, you may extend this exception to your
|
|
27 |
# version of the file(s), but you are not obligated to do so. If you
|
|
28 |
# do not wish to do so, delete this exception statement from your
|
|
29 |
# version. If you delete this exception statement from all source
|
|
30 |
# files in the program, then also delete it here.
|
|
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
31 |
"""Ubuntuone client logging utilities and config. """
|
1222.1.1
by Rodney Dawes
Add required OpenSSL license exception text |
32 |
|
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
33 |
from __future__ import with_statement |
34 |
||
35 |
import contextlib |
|
1064.1.1
by Natalia B. Bidart
Moving xdg specifics to windows platform. |
36 |
import functools |
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
37 |
import logging |
1065.2.1
by Natalia B. Bidart
Avoiding circular imports. |
38 |
import re |
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
39 |
import sys |
40 |
import weakref |
|
41 |
||
42 |
from logging.handlers import TimedRotatingFileHandler |
|
43 |
||
1064.1.1
by Natalia B. Bidart
Moving xdg specifics to windows platform. |
44 |
|
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
45 |
# extra levels
|
46 |
# be more verbose than logging.DEBUG(10)
|
|
47 |
TRACE = 5 |
|
48 |
# info that we almost always want to log (logging.ERROR - 1)
|
|
49 |
NOTE = logging.ERROR - 1 |
|
50 |
||
51 |
# map names to the extra levels
|
|
1149.1.1
by Diego Sarmentero
Fixed PEP8 issues. |
52 |
levels = {'TRACE': TRACE, 'NOTE': NOTE} |
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
53 |
for k, v in levels.items(): |
54 |
logging.addLevelName(v, k) |
|
55 |
||
56 |
||
57 |
class Logger(logging.Logger): |
|
1041.2.2
by Eric Casteleijn
fixed logging for status |
58 |
"""Logger that support our custom levels."""
|
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
59 |
|
60 |
def note(self, msg, *args, **kwargs): |
|
61 |
"""log at NOTE level"""
|
|
62 |
if self.isEnabledFor(NOTE): |
|
63 |
self._log(NOTE, msg, args, **kwargs) |
|
64 |
||
65 |
def trace(self, msg, *args, **kwargs): |
|
66 |
"""log at TRACE level"""
|
|
67 |
if self.isEnabledFor(TRACE): |
|
68 |
self._log(TRACE, msg, args, **kwargs) |
|
69 |
||
70 |
||
71 |
class DayRotatingFileHandler(TimedRotatingFileHandler): |
|
72 |
"""A mix of TimedRotatingFileHandler and RotatingFileHandler configured for
|
|
73 |
daily rotation but that uses the suffix and extMatch of Hourly rotation, in
|
|
74 |
order to allow seconds based rotation on each startup.
|
|
75 |
The log file is also rotated when the specified size is reached.
|
|
76 |
"""
|
|
77 |
||
78 |
def __init__(self, *args, **kwargs): |
|
79 |
""" create the instance and override the suffix and extMatch.
|
|
80 |
Also accepts a maxBytes keyword arg to rotate the file when it reachs
|
|
81 |
maxBytes.
|
|
82 |
"""
|
|
83 |
kwargs['when'] = 'D' |
|
84 |
kwargs['backupCount'] = LOGBACKUP |
|
85 |
# check if we are in 2.5, only for PQM
|
|
86 |
if sys.version_info[:2] >= (2, 6): |
|
87 |
kwargs['delay'] = 1 |
|
88 |
if 'maxBytes' in kwargs: |
|
89 |
self.maxBytes = kwargs.pop('maxBytes') |
|
90 |
else: |
|
91 |
self.maxBytes = 0 |
|
92 |
TimedRotatingFileHandler.__init__(self, *args, **kwargs) |
|
93 |
# override suffix
|
|
94 |
self.suffix = "%Y-%m-%d_%H-%M-%S" |
|
95 |
self.extMatch = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$") |
|
96 |
||
97 |
def shouldRollover(self, record): |
|
98 |
"""
|
|
99 |
Determine if rollover should occur.
|
|
100 |
||
101 |
Basically, see if TimedRotatingFileHandler.shouldRollover and if it's
|
|
102 |
False see if the supplied record would cause the file to exceed
|
|
103 |
the size limit we have.
|
|
104 |
||
105 |
The size based rotation are from logging.handlers.RotatingFileHandler
|
|
106 |
"""
|
|
107 |
if TimedRotatingFileHandler.shouldRollover(self, record): |
|
108 |
return 1 |
|
109 |
else: |
|
110 |
# check the size
|
|
111 |
if self.stream is None: # delay was set... |
|
112 |
self.stream = self._open() |
|
113 |
if self.maxBytes > 0: # are we rolling over? |
|
114 |
msg = "%s\n" % self.format(record) |
|
1149.1.1
by Diego Sarmentero
Fixed PEP8 issues. |
115 |
# due to non-posix-compliant Windows feature
|
116 |
self.stream.seek(0, 2) |
|
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
117 |
if self.stream.tell() + len(msg) >= self.maxBytes: |
118 |
return 1 |
|
119 |
return 0 |
|
120 |
||
121 |
||
122 |
class MultiFilter(logging.Filter): |
|
123 |
"""Our own logging.Filter.
|
|
124 |
||
125 |
To allow filter by multiple names in a single handler or logger.
|
|
126 |
||
127 |
"""
|
|
128 |
||
129 |
def __init__(self, names=None): |
|
130 |
logging.Filter.__init__(self) |
|
131 |
self.names = names or [] |
|
132 |
self.filters = [] |
|
133 |
for name in self.names: |
|
134 |
self.filters.append(logging.Filter(name)) |
|
135 |
||
136 |
def filter(self, record): |
|
137 |
"""Determine if the specified record is to be logged.
|
|
138 |
||
139 |
This work a bit different from the standard logging.Filter, the
|
|
140 |
record is logged if at least one filter allows it.
|
|
141 |
If there are no filters, the record is allowed.
|
|
142 |
||
143 |
"""
|
|
144 |
if not self.filters: |
|
145 |
# no filters, allow the record
|
|
146 |
return True |
|
147 |
for f in self.filters: |
|
148 |
if f.filter(record): |
|
149 |
return True |
|
150 |
return False |
|
151 |
||
152 |
||
153 |
class DebugCapture(logging.Handler): |
|
154 |
"""
|
|
155 |
A context manager to capture debug logs.
|
|
156 |
"""
|
|
157 |
||
158 |
def __init__(self, logger, raise_unhandled=False, on_error=True): |
|
159 |
"""Creates the instance.
|
|
160 |
||
161 |
@param logger: the logger to wrap
|
|
162 |
@param raise_unhandled: raise unhandled errors (which are alse logged)
|
|
163 |
@param on_error: if it's True (default) the captured debug info is
|
|
164 |
dumped if a record with log level >= ERROR is logged.
|
|
165 |
"""
|
|
166 |
logging.Handler.__init__(self, logging.DEBUG) |
|
167 |
self.on_error = on_error |
|
168 |
self.dirty = False |
|
169 |
self.raise_unhandled = raise_unhandled |
|
170 |
self.records = [] |
|
171 |
# insert myself as the handler for the logger
|
|
172 |
self.logger = weakref.proxy(logger) |
|
173 |
# store the logger log level
|
|
174 |
self.old_level = logger.level |
|
175 |
# remove us from the Handler list and dict
|
|
176 |
self.close() |
|
177 |
||
178 |
def emit_debug(self): |
|
179 |
"""emit stored records to the original logger handler(s)"""
|
|
180 |
enable_debug = self.enable_debug |
|
181 |
for record in self.records: |
|
182 |
for slave in self.slaves: |
|
183 |
with enable_debug(slave): |
|
184 |
slave.handle(record) |
|
185 |
||
186 |
@contextlib.contextmanager |
|
187 |
def enable_debug(self, obj): |
|
188 |
"""context manager that temporarily changes the level attribute of obj
|
|
189 |
to logging.DEBUG.
|
|
190 |
"""
|
|
191 |
old_level = obj.level |
|
192 |
obj.level = logging.DEBUG |
|
193 |
yield obj |
|
194 |
obj.level = old_level |
|
195 |
||
196 |
def clear(self): |
|
197 |
"""cleanup the captured records"""
|
|
198 |
self.records = [] |
|
199 |
||
200 |
def install(self): |
|
201 |
"""Install the debug capture in the logger"""
|
|
202 |
self.slaves = self.logger.handlers |
|
203 |
self.logger.handlers = [self] |
|
204 |
# set the logger level in DEBUG
|
|
205 |
self.logger.setLevel(logging.DEBUG) |
|
206 |
||
207 |
def uninstall(self): |
|
208 |
"""restore the logger original handlers"""
|
|
209 |
# restore the logger
|
|
210 |
self.logger.handlers = self.slaves |
|
211 |
self.logger.setLevel(self.old_level) |
|
212 |
self.clear() |
|
213 |
self.dirty = False |
|
214 |
self.slaves = [] |
|
215 |
||
216 |
def emit(self, record): |
|
217 |
"""A emit() that append the record to the record list"""
|
|
218 |
self.records.append(record) |
|
219 |
||
220 |
def handle(self, record): |
|
221 |
""" handle a record """
|
|
222 |
# if its a DEBUG level record then intercept otherwise
|
|
223 |
# pass through to the original logger handler(s)
|
|
224 |
if self.old_level <= logging.DEBUG: |
|
225 |
return sum(slave.handle(record) for slave in self.slaves) |
|
226 |
if record.levelno == logging.DEBUG: |
|
227 |
return logging.Handler.handle(self, record) |
|
228 |
elif self.on_error and record.levelno >= logging.ERROR and \ |
|
229 |
record.levelno != NOTE: |
|
230 |
# if it's >= ERROR keep it, but mark the dirty falg
|
|
231 |
self.dirty = True |
|
232 |
return logging.Handler.handle(self, record) |
|
233 |
else: |
|
234 |
return sum(slave.handle(record) for slave in self.slaves) |
|
235 |
||
236 |
def __enter__(self): |
|
237 |
"""ContextManager API"""
|
|
238 |
self.install() |
|
239 |
return self |
|
240 |
||
241 |
def __exit__(self, exc_type, exc_value, traceback): |
|
242 |
"""ContextManager API"""
|
|
243 |
if exc_type is not None: |
|
244 |
self.emit_debug() |
|
245 |
self.on_error = False |
|
246 |
self.logger.error('unhandled exception', exc_info=(exc_type, |
|
247 |
exc_value, traceback)) |
|
248 |
elif self.dirty: |
|
249 |
# emit all debug messages collected after the error
|
|
250 |
self.emit_debug() |
|
251 |
self.uninstall() |
|
252 |
if self.raise_unhandled and exc_type is not None: |
|
253 |
raise exc_type, exc_value, traceback |
|
254 |
else: |
|
255 |
return True |
|
256 |
||
257 |
||
1117.2.1
by Natalia B. Bidart
Do not log sensible data in CredentialsMangementTool (LP: #837488). |
258 |
def log_call(log_func, with_args=True, with_result=True): |
1106.1.2
by Natalia B. Bidart
Improvements, cleanup. |
259 |
"""Decorator to add a log entry using 'log_func'.
|
260 |
||
1117.2.1
by Natalia B. Bidart
Do not log sensible data in CredentialsMangementTool (LP: #837488). |
261 |
If not 'with_args', do not log arguments. Same apply to 'with_result'.
|
262 |
||
1106.1.2
by Natalia B. Bidart
Improvements, cleanup. |
263 |
An example of use would be:
|
264 |
||
265 |
@log_call(logger.debug)
|
|
266 |
def f(a, b, c):
|
|
267 |
....
|
|
268 |
||
269 |
"""
|
|
270 |
||
271 |
def middle(f): |
|
272 |
"""Add logging when calling 'f'."""
|
|
273 |
||
274 |
@functools.wraps(f) |
|
275 |
def inner(*args, **kwargs): |
|
276 |
"""Call f(*args, **kwargs)."""
|
|
1117.2.1
by Natalia B. Bidart
Do not log sensible data in CredentialsMangementTool (LP: #837488). |
277 |
if with_args: |
278 |
a, kw = args, kwargs |
|
279 |
else: |
|
280 |
a, kw = '<hidden args>', '<hidden kwargs>' |
|
281 |
log_func('%s: args %r, kwargs %r.', f.__name__, a, kw) |
|
282 |
||
1106.1.2
by Natalia B. Bidart
Improvements, cleanup. |
283 |
res = f(*args, **kwargs) |
1117.2.1
by Natalia B. Bidart
Do not log sensible data in CredentialsMangementTool (LP: #837488). |
284 |
|
285 |
if with_result: |
|
286 |
log_func('%s: result %r.', f.__name__, res) |
|
287 |
||
1106.1.2
by Natalia B. Bidart
Improvements, cleanup. |
288 |
return res |
289 |
||
290 |
return inner |
|
291 |
||
292 |
return middle |
|
293 |
||
294 |
||
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
295 |
### configure the thing ###
|
1149.1.1
by Diego Sarmentero
Fixed PEP8 issues. |
296 |
LOGBACKUP = 5 # the number of log files to keep around |
473.3.1
by guillermo.gonzalez at canonical
move ubuntuone.syncdaemon.logger generic stuff to ubuntuone.logger to avoid creating the syncdaemon handlers and loggers in non-syncdaemon code. |
297 |
|
298 |
basic_formatter = logging.Formatter(fmt="%(asctime)s - %(name)s - " \ |
|
299 |
"%(levelname)s - %(message)s") |
|
300 |
debug_formatter = logging.Formatter(fmt="%(asctime)s %(name)s %(module)s " \ |
|
301 |
"%(lineno)s %(funcName)s %(message)s") |
|
302 |
||
303 |
# a constant to change the default DEBUG level value
|
|
304 |
_DEBUG_LOG_LEVEL = logging.DEBUG |
|
305 |
||
306 |
||
307 |
# partial config of the handler to rotate when the file size is 1MB
|
|
308 |
CustomRotatingFileHandler = functools.partial(DayRotatingFileHandler, |
|
309 |
maxBytes=1048576) |
|
310 |
||
311 |
# use our logger as the default Logger class
|
|
312 |
logging.setLoggerClass(Logger) |