~launchpad-pqm/launchpad/devel

13515.2.1 by Jeroen Vermeulen
Allow scripts' logger initialization to be skipped.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.18 by Karl Fogel
Add the copyright header block to files under lib/canonical/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4983.1.3 by Curtis Hovey
Added pylint exceptions to script classes.
4
# pylint: disable-msg=W0702
5
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
6
"""Logging setup for scripts.
7
14565.2.15 by Curtis Hovey
Moved canonical.launchpad.scripts __init__ to lp.services.scripts.
8
Don't import from this module. Import it from lp.services.scripts.
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
9
"""
10
11
__metaclass__ = type
12
13
# Don't import stuff from this module. Import it from canonical.scripts
7483.2.55 by Jonathan Lange
Add backport of WatchedFileHandler, exporting it from scripts and
14
__all__ = [
12336.2.2 by Gavin Panella
New function traceback_info().
15
    'DEBUG2',
16
    'DEBUG3',
17
    'DEBUG4',
18
    'DEBUG5',
19
    'DEBUG6',
20
    'DEBUG7',
21
    'DEBUG8',
22
    'DEBUG9',
13515.2.1 by Jeroen Vermeulen
Allow scripts' logger initialization to be skipped.
23
    'dummy_logger_options',
12336.2.2 by Gavin Panella
New function traceback_info().
24
    'LaunchpadFormatter',
7483.2.55 by Jonathan Lange
Add backport of WatchedFileHandler, exporting it from scripts and
25
    'log',
26
    'logger',
27
    'logger_options',
11300.2.26 by Stuart Bishop
Test unhandled exception handling by LaunchpadScript
28
    'OopsHandler',
7483.2.55 by Jonathan Lange
Add backport of WatchedFileHandler, exporting it from scripts and
29
    ]
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
30
10293.1.1 by Barry Warsaw, Max Bowsher
Switch from using sha and md5 to hashlib. Also use hashlib.sha256 instead of
31
12394.1.3 by Gavin Panella
Context manager to temporarily override the script logger.
32
from contextlib import contextmanager
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
33
from cStringIO import StringIO
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
34
from datetime import timedelta
10293.1.1 by Barry Warsaw, Max Bowsher
Switch from using sha and md5 to hashlib. Also use hashlib.sha256 instead of
35
import hashlib
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
36
import logging
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
37
from logging.handlers import WatchedFileHandler
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
from optparse import OptionParser
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
39
import os.path
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
40
import re
41
import sys
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
42
import time
12336.2.1 by Gavin Panella
Change LaunchpadFormatter to inherit from zope.exceptions.log.Formatter, so that __traceback_info__ is included in tracebacks.
43
from traceback import format_exception_only
10293.1.1 by Barry Warsaw, Max Bowsher
Switch from using sha and md5 to hashlib. Also use hashlib.sha256 instead of
44
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
45
from zope.component import getUtility
12336.2.1 by Gavin Panella
Change LaunchpadFormatter to inherit from zope.exceptions.log.Formatter, so that __traceback_info__ is included in tracebacks.
46
from zope.exceptions.log import Formatter
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
47
14605.1.1 by Curtis Hovey
Moved canonical.config to lp.services.
48
from lp.services.config import config
14606.2.3 by William Grant
canonical.librarian.interfaces -> lp.services.librarian.interfaces.client
49
from lp.services.librarian.interfaces.client import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
50
    ILibrarianClient,
51
    UploadFailed,
52
    )
11300.2.49 by Stuart Bishop
Tidy and improve logging system infrastructure for great justice.
53
from lp.services.log import loglevels
13333.5.3 by Jonathan Lange
Use the new utc_now()
54
from lp.services.utils import (
55
    compress_hash,
56
    utc_now,
57
    )
14606.2.8 by William Grant
format-imports
58
from lp.services.webapp.errorlog import (
59
    globalErrorUtility,
60
    ScriptRequest,
61
    )
11300.2.49 by Stuart Bishop
Tidy and improve logging system infrastructure for great justice.
62
63
# Reexport our custom loglevels for old callsites. These callsites
64
# should be importing the symbols from lp.services.log.loglevels
65
DEBUG2 = loglevels.DEBUG2
66
DEBUG3 = loglevels.DEBUG3
67
DEBUG4 = loglevels.DEBUG4
68
DEBUG5 = loglevels.DEBUG5
69
DEBUG6 = loglevels.DEBUG6
70
DEBUG7 = loglevels.DEBUG7
71
DEBUG8 = loglevels.DEBUG8
72
DEBUG9 = loglevels.DEBUG9
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
73
4476.6.17 by Danilo Šegan
Add corner-case test, factor out FakeLogger into scripts.logger.
74
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
75
class OopsHandler(logging.Handler):
76
    """Handler to log to the OOPS system."""
77
11300.2.35 by Stuart Bishop
Better parameter name
78
    def __init__(self, script_name, level=logging.WARN):
11300.2.25 by Stuart Bishop
Context to cronscript OOPS reports
79
        logging.Handler.__init__(self, level)
80
        # Context for OOPS reports.
81
        self.request = ScriptRequest(
11300.2.35 by Stuart Bishop
Better parameter name
82
            [('script_name', script_name), ('path', sys.argv[0])])
11300.2.25 by Stuart Bishop
Context to cronscript OOPS reports
83
        self.setFormatter(LaunchpadFormatter())
84
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
85
    def emit(self, record):
86
        """Emit a record as an OOPS."""
87
        try:
14104.6.42 by Robert Collins
Fix some test failures.
88
            info = record.exc_info
14104.6.45 by Robert Collins
Fix thinko in OopsHandler.
89
            if info is None:
14104.6.42 by Robert Collins
Fix some test failures.
90
                info = sys.exc_info()
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
91
            msg = record.getMessage()
92
            with globalErrorUtility.oopsMessage(msg):
14104.6.42 by Robert Collins
Fix some test failures.
93
                globalErrorUtility.raising(info, self.request)
12070.1.4 by Tim Penhey
Move FakeLogger and BufferLogger to lp.services.log.logging and delete the QuietFakeLogger.
94
        except Exception:
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
95
            self.handleError(record)
96
97
12336.2.1 by Gavin Panella
Change LaunchpadFormatter to inherit from zope.exceptions.log.Formatter, so that __traceback_info__ is included in tracebacks.
98
class LaunchpadFormatter(Formatter):
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
99
    """logging.Formatter encoding our preferred output format."""
100
11300.2.47 by Stuart Bishop
Nicer
101
    def __init__(self, fmt=None, datefmt=None):
102
        if fmt is None:
11728.3.6 by Robert Collins
Convert another hard coded 'if testrunner' check, and factor out the duplication.
103
            if config.isTestRunner():
11300.2.47 by Stuart Bishop
Nicer
104
                # Don't output timestamps in the test environment
105
                fmt = '%(levelname)-7s %(message)s'
106
            else:
107
                fmt = '%(asctime)s %(levelname)-7s %(message)s'
108
        if datefmt is None:
109
            datefmt = "%Y-%m-%d %H:%M:%S"
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
110
        logging.Formatter.__init__(self, fmt, datefmt)
13515.2.1 by Jeroen Vermeulen
Allow scripts' logger initialization to be skipped.
111
        # Output should be UTC.
112
        self.converter = time.gmtime
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
113
114
115
class LibrarianFormatter(LaunchpadFormatter):
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
116
    """A logging.Formatter that stores tracebacks in the Librarian and emits
117
    a URL rather than emitting the traceback directly.
118
119
    The traceback will be emitted as a fallback if the Librarian cannot be
120
    contacted.
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
121
122
    XXX bug=641103 StuartBishop -- This class should die. Remove it and
123
    replace with LaunchpadFormatter, fixing the test fallout.
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
124
    """
11300.2.21 by Stuart Bishop
Delint
125
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
126
    def formatException(self, ei):
127
        """Format the exception and store it in the Librian.
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
128
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
129
        Returns the URL, or the formatted exception if the Librarian is
130
        not available.
131
        """
12336.2.1 by Gavin Panella
Change LaunchpadFormatter to inherit from zope.exceptions.log.Formatter, so that __traceback_info__ is included in tracebacks.
132
        traceback = LaunchpadFormatter.formatException(self, ei)
3691.267.31 by Stuart Bishop
Fix more tests
133
        # Uncomment this line to stop exception storage in the librarian.
134
        # Useful for debugging tests.
135
        # return traceback
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
136
        try:
137
            librarian = getUtility(ILibrarianClient)
138
        except LookupError:
139
            return traceback
1681.1.143 by Stuart Bishop
Improve script exception output and fix noisy log test
140
2684 by Canonical.com Patch Queue Manager
[trivial] Make logging to the librarian more informative when dealing with minimal or broken exceptions
141
        exception_string = ''
1681.1.143 by Stuart Bishop
Improve script exception output and fix noisy log test
142
        try:
143
            exception_string = str(ei[1]).encode('ascii')
144
        except:
2684 by Canonical.com Patch Queue Manager
[trivial] Make logging to the librarian more informative when dealing with minimal or broken exceptions
145
            pass
146
        if not exception_string:
9678.15.6 by Guilherme Salgado
Use the exception's class __name__ instead of extracting it from its __str__
147
            exception_string = ei[0].__name__
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
148
13333.5.3 by Jonathan Lange
Use the new utc_now()
149
        expiry = utc_now() + timedelta(days=90)
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
150
        try:
12398.2.6 by Jonathan Lange
Many of the uses of 'base' were actually for compressing hashes. Use a function
151
            filename = compress_hash(hashlib.sha1(traceback)) + '.txt'
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
152
            url = librarian.remoteAddFile(
153
                    filename, len(traceback), StringIO(traceback),
2816.1.17 by Stuart Bishop
Make librarian exception logger set file expiry date to 3 months in the future
154
                    'text/plain;charset=%s' % sys.getdefaultencoding(),
11300.2.21 by Stuart Bishop
Delint
155
                    expires=expiry)
1681.1.143 by Stuart Bishop
Improve script exception output and fix noisy log test
156
            return ' -> %s (%s)' % (url, exception_string)
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
157
        except UploadFailed:
158
            return traceback
11300.2.46 by Stuart Bishop
Fix test_scriptmonitor
159
        except Exception:
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
160
            # Exceptions raised by the Formatter get swallowed, but we want
161
            # to know about them. Since we are already spitting out exception
162
            # information, we can stuff our own problems in there too.
163
            return '%s\n\nException raised in formatter:\n%s\n' % (
164
                    traceback,
12336.2.1 by Gavin Panella
Change LaunchpadFormatter to inherit from zope.exceptions.log.Formatter, so that __traceback_info__ is included in tracebacks.
165
                    LaunchpadFormatter.formatException(self, sys.exc_info()))
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
166
167
13515.2.1 by Jeroen Vermeulen
Allow scripts' logger initialization to be skipped.
168
class LogLevelNudger:
169
    """Callable to adjust the global log level.
170
171
    Use instances as callbacks for `optparse`.
172
    """
173
174
    def __init__(self, default, increment=True):
175
        """Initialize nudger to increment or decrement log level.
176
177
        :param default: Default starting level.
178
        :param increment: Whether to increase the log level (as when
179
            handling the --verbose option).  If not, will decrease
180
            instead (as with the --quiet option).
181
        """
182
        self.default = default
183
        self.increment = increment
184
185
    def getIncrement(self, current_level):
186
        """Figure out how much to increment the log level.
187
188
        Increment is negative when decreasing log level, of course.
189
        """
190
        if self.increment:
191
            if current_level < 10:
192
                return 1
193
            else:
194
                return 10
195
        else:
196
            if current_level <= 10:
197
                return -1
198
            else:
199
                return -10
200
201
    def __call__(self, option, opt_str, value, parser):
202
        """Callback for `optparse` to handle --verbose or --quiet option."""
203
        current_level = getattr(parser.values, 'loglevel', self.default)
204
        increment = self.getIncrement(current_level)
205
        parser.values.loglevel = current_level + increment
206
        parser.values.verbose = (parser.values.loglevel < self.default)
207
        # Reset the global log.
208
        log._log = _logger(parser.values.loglevel, out_stream=sys.stderr)
209
210
211
def define_verbosity_options(parser, default, verbose_callback,
212
                             quiet_callback):
213
    """Define the -v and -q options on `parser`."""
13515.2.5 by Jeroen Vermeulen
OK so that last bit was needed after all. At least now we know.
214
    # Only one of these specifies dest and default.  That's because
215
    # that's enough to make the parser create the option value; there's
216
    # no need for the other option to specify them as well.
13515.2.1 by Jeroen Vermeulen
Allow scripts' logger initialization to be skipped.
217
    parser.add_option(
13515.2.5 by Jeroen Vermeulen
OK so that last bit was needed after all. At least now we know.
218
        "-v", "--verbose", dest="loglevel", default=default,
219
        action="callback", callback=verbose_callback,
13515.2.1 by Jeroen Vermeulen
Allow scripts' logger initialization to be skipped.
220
        help="Increase stderr verbosity. May be specified multiple times.")
221
    parser.add_option(
222
        "-q", "--quiet", action="callback", callback=quiet_callback,
223
        help="Decrease stderr verbosity. May be specified multiple times.")
224
225
226
def do_nothing(*args, **kwargs):
227
    """Do absolutely nothing."""
228
229
230
def dummy_logger_options(parser):
231
    """Add dummy --verbose and --quiet options to `parser`."""
232
    define_verbosity_options(parser, None, do_nothing, do_nothing)
233
234
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
235
def logger_options(parser, default=logging.INFO):
236
    """Add the --verbose and --quiet options to an optparse.OptionParser.
237
238
    The requested loglevel will end up in the option's loglevel attribute.
3410.4.1 by Stuart Bishop
Add options.verbose flag to script verbosity setup
239
    Note that loglevel is not clamped to any particular range.
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
240
241
    >>> from optparse import OptionParser
242
    >>> parser = OptionParser()
243
    >>> logger_options(parser)
3410.4.1 by Stuart Bishop
Add options.verbose flag to script verbosity setup
244
    >>> options, args = parser.parse_args(['-v', '-v', '-q', '-qqqqqqq'])
245
    >>> options.loglevel > logging.CRITICAL
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
246
    True
3410.4.1 by Stuart Bishop
Add options.verbose flag to script verbosity setup
247
    >>> options.verbose
248
    False
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
249
3410.4.1 by Stuart Bishop
Add options.verbose flag to script verbosity setup
250
    >>> parser = OptionParser()
251
    >>> logger_options(parser)
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
252
    >>> options, args = parser.parse_args([])
253
    >>> options.loglevel == logging.INFO
254
    True
3410.4.1 by Stuart Bishop
Add options.verbose flag to script verbosity setup
255
    >>> options.verbose
256
    False
257
258
    >>> from optparse import OptionParser
259
    >>> parser = OptionParser()
260
    >>> logger_options(parser, logging.WARNING)
261
    >>> options, args = parser.parse_args(['-v'])
262
    >>> options.loglevel == logging.INFO
263
    True
264
    >>> options.verbose
265
    True
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
266
2425.1.13 by James Henstridge
reset handlers on root logger in logger doctests
267
    Cleanup:
14604.1.1 by Curtis Hovey
Separate test-authoring classes from test-running classes.
268
    >>> from lp.testing import reset_logging
3691.57.16 by Stuart Bishop
Fix logger tests
269
    >>> reset_logging()
2425.1.13 by James Henstridge
reset handlers on root logger in logger doctests
270
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
271
    As part of the options parsing, the 'log' global variable is updated.
272
    This can be used by code too lazy to pass it around as a variable.
273
    """
274
275
    # Raise an exception if the constants have changed. If they change we
276
    # will need to fix the arithmetic
277
    assert logging.DEBUG == 10
278
    assert logging.INFO == 20
279
    assert logging.WARNING == 30
280
    assert logging.ERROR == 40
281
    assert logging.CRITICAL == 50
282
3410.4.1 by Stuart Bishop
Add options.verbose flag to script verbosity setup
283
    # Undocumented use of the optparse module
284
    parser.defaults['verbose'] = False
285
13515.2.1 by Jeroen Vermeulen
Allow scripts' logger initialization to be skipped.
286
    define_verbosity_options(
287
        parser, default,
13515.2.3 by Jeroen Vermeulen
Got nudgers wrong way around.
288
        LogLevelNudger(default, False), LogLevelNudger(default, True))
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
289
290
    debug_levels = ', '.join([
291
        v for k, v in sorted(logging._levelNames.items(), reverse=True)
292
            if isinstance(k, int)])
293
294
    def log_file(option, opt_str, value, parser):
295
        try:
296
            level, path = value.split(':', 1)
297
        except ValueError:
11300.2.42 by Stuart Bishop
Ensure global logger is up to date and set default log-file level to INFO
298
            level, path = logging.INFO, value
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
299
300
        if isinstance(level, int):
301
            pass
302
        elif level.upper() not in logging._levelNames:
303
            parser.error(
304
                "'%s' is not a valid logging level. Must be one of %s" % (
305
                    level, debug_levels))
306
        else:
307
            level = logging._levelNames[level.upper()]
308
309
        if not path:
310
            parser.error("Path to log file not specified")
311
312
        path = os.path.abspath(path)
313
        try:
314
            open(path, 'a')
315
        except Exception:
316
            parser.error("Unable to open log file %s" % path)
317
318
        parser.values.log_file = path
319
        parser.values.log_file_level = level
320
11300.2.42 by Stuart Bishop
Ensure global logger is up to date and set default log-file level to INFO
321
        # Reset the global log.
322
        log._log = _logger(parser.values.loglevel, out_stream=sys.stderr)
323
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
324
    parser.add_option(
325
        "--log-file", type="string", action="callback", callback=log_file,
11300.2.40 by Stuart Bishop
Initialize log_file_level with a default
326
        metavar="LVL:FILE", default=None,
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
327
        help="Send log messages to FILE. LVL is one of %s" % debug_levels)
11300.2.40 by Stuart Bishop
Initialize log_file_level with a default
328
    parser.set_default('log_file_level', None)
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
329
330
    # Set the global log
3147.2.63 by Celso Providelo
review comments (take 2)
331
    log._log = _logger(default, out_stream=sys.stderr)
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
332
333
334
def logger(options=None, name=None):
335
    """Return a logging instance with standard setup.
336
337
    options should be the options as returned by an option parser that
338
    has been initilized with logger_options(parser)
339
340
    >>> from optparse import OptionParser
341
    >>> parser = OptionParser()
342
    >>> logger_options(parser)
343
    >>> options, args = parser.parse_args(['-v', '-v', '-q', '-q', '-q'])
344
    >>> log = logger(options)
7483.1.34 by Jonathan Lange
I really don't like doctest.
345
    >>> log.debug('Not shown - too quiet')
2425.1.13 by James Henstridge
reset handlers on root logger in logger doctests
346
347
    Cleanup:
3691.57.28 by Stuart Bishop
Fix more tests
348
14604.1.1 by Curtis Hovey
Separate test-authoring classes from test-running classes.
349
    >>> from lp.testing import reset_logging
3691.57.42 by Stuart Bishop
More post review feedback updates
350
    >>> reset_logging()
3691.57.28 by Stuart Bishop
Fix more tests
351
    """
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
352
    if options is None:
353
        parser = OptionParser()
354
        logger_options(parser)
355
        options, args = parser.parse_args()
356
11300.2.46 by Stuart Bishop
Fix test_scriptmonitor
357
    log_file = getattr(options, 'log_file', None)
358
    log_file_level = getattr(options, 'log_file_level', None)
359
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
360
    return _logger(
361
        options.loglevel, out_stream=sys.stderr, name=name,
11300.2.46 by Stuart Bishop
Fix test_scriptmonitor
362
        log_file=log_file, log_file_level=log_file_level)
5204.5.4 by Barry Warsaw
Make LaunchpadScript a new-style class, and adjust a super() call accordingly.
363
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
364
2425.1.13 by James Henstridge
reset handlers on root logger in logger doctests
365
def reset_root_logger():
366
    root_logger = logging.getLogger()
367
    for hdlr in root_logger.handlers[:]:
368
        hdlr.flush()
3691.57.28 by Stuart Bishop
Fix more tests
369
        try:
370
            hdlr.close()
371
        except KeyError:
372
            pass
2425.1.13 by James Henstridge
reset handlers on root logger in logger doctests
373
        root_logger.removeHandler(hdlr)
374
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
375
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
376
def _logger(
377
    level, out_stream, name=None,
378
    log_file=None, log_file_level=logging.DEBUG):
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
379
    """Create the actual logger instance, logging at the given level
380
381
    if name is None, it will get args[0] without the extension (e.g. gina).
3147.2.63 by Celso Providelo
review comments (take 2)
382
    'out_stream must be passed, the recommended value is sys.stderr'
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
383
    """
384
    if name is None:
385
        # Determine the logger name from the script name
386
        name = sys.argv[0]
387
        name = re.sub('.py[oc]?$', '', name)
388
1681.1.126 by Stuart Bishop
Ensure specialized log handlers work for the root logger too
389
    # We install our custom handlers and formatters on the root logger.
390
    # This means that if the root logger is used, we still get correct
391
    # formatting. The root logger should probably not be used.
392
    root_logger = logging.getLogger()
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
393
2425.1.13 by James Henstridge
reset handlers on root logger in logger doctests
394
    # reset state of root logger
395
    reset_root_logger()
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
396
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
397
    # Make it print output in a standard format, suitable for
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
398
    # both command line tools and cron jobs (command line tools often end
399
    # up being run from inside cron, so this is a good thing).
12109.2.1 by William Grant
Fix canonical.launchpad.scripts.logger to work with Python 2.7.
400
    hdlr = logging.StreamHandler(out_stream)
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
401
    # We set the level on the handler rather than the logger, so other
402
    # handlers with different levels can be added for things like debug
403
    # logs.
11300.2.27 by Stuart Bishop
Fix log level bug and test WARN and above generate OOPS reports
404
    root_logger.setLevel(0)
11300.2.24 by Stuart Bishop
Make cronscripts emit OOPS reports when WARN or higher messages are logged
405
    hdlr.setLevel(level)
11300.2.48 by Stuart Bishop
Reenable the LibrarianFormatter
406
    formatter = LibrarianFormatter()
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
407
    hdlr.setFormatter(formatter)
1681.1.126 by Stuart Bishop
Ensure specialized log handlers work for the root logger too
408
    root_logger.addHandler(hdlr)
409
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
410
    # Add an optional aditional log file.
411
    if log_file is not None:
412
        handler = WatchedFileHandler(log_file, encoding="UTF8")
413
        handler.setFormatter(formatter)
414
        handler.setLevel(log_file_level)
415
        root_logger.addHandler(handler)
416
1681.1.126 by Stuart Bishop
Ensure specialized log handlers work for the root logger too
417
    # Create our logger
418
    logger = logging.getLogger(name)
419
5204.5.4 by Barry Warsaw
Make LaunchpadScript a new-style class, and adjust a super() call accordingly.
420
    # Set the global log
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
421
    log._log = logger
422
11300.2.38 by Stuart Bishop
Alter --log-file for scripts, allowing it to specify an aditional log destination with a log level seperate to the console output
423
    # Inform the user the extra log file is in operation.
424
    if log_file is not None:
425
        log.info(
426
            "Logging %s and higher messages to %s" % (
427
                logging.getLevelName(log_file_level), log_file))
428
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
429
    return logger
430
431
432
class _LogWrapper:
433
    """Changes the logger instance.
434
14565.2.15 by Curtis Hovey
Moved canonical.launchpad.scripts __init__ to lp.services.scripts.
435
    Other modules will do 'from lp.services.scripts import log'.
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
436
    This wrapper allows us to change the logger instance these other modules
437
    use, by replacing the _log attribute. This is done each call to logger()
438
    """
439
440
    def __init__(self, log):
441
        self._log = log
442
443
    def __getattr__(self, key):
444
        return getattr(self._log, key)
445
446
    def __setattr__(self, key, value):
447
        if key == '_log':
448
            self.__dict__['_log'] = value
449
            return value
450
        else:
451
            return setattr(self._log, key, value)
452
12394.1.3 by Gavin Panella
Context manager to temporarily override the script logger.
453
    @contextmanager
454
    def use(self, log):
455
        """Temporarily use a different `log`."""
456
        self._log, log = log, self._log
457
        try:
458
            yield
459
        finally:
460
            self._log = log
461
3109.3.3 by David Allouche
factor shortException into _LogWrapper
462
    def shortException(self, msg, *args):
463
        """Like Logger.exception, but does not print a traceback."""
464
        exctype, value = sys.exc_info()[:2]
12336.2.1 by Gavin Panella
Change LaunchpadFormatter to inherit from zope.exceptions.log.Formatter, so that __traceback_info__ is included in tracebacks.
465
        report = ''.join(format_exception_only(exctype, value))
3109.3.3 by David Allouche
factor shortException into _LogWrapper
466
        # _log.error interpolates msg, so we need to escape % chars
467
        msg += '\n' + report.rstrip('\n').replace('%', '%%')
468
        self._log.error(msg, *args)
469
470
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
471
log = _LogWrapper(logging.getLogger())