~ubuntuone-control-tower/ubuntuone-client/trunk

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)