~ubuntu-branches/ubuntu/lucid/pitivi/lucid

« back to all changes in this revision

Viewing changes to pitivi/log/log.py

  • Committer: Bazaar Package Importer
  • Author(s): Sebastian Dröge
  • Date: 2009-05-27 14:22:49 UTC
  • mfrom: (1.2.1 upstream) (3.1.13 experimental)
  • Revision ID: james.westby@ubuntu.com-20090527142249-tj0qnkc37320ylml
New upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Mode: Python; test-case-name: test_log -*-
 
2
# vi:si:et:sw=4:sts=4:ts=4
 
3
 
 
4
# This file is released under the standard PSF license.
 
5
 
 
6
"""
 
7
Logging module.
 
8
 
 
9
Five levels of log information are defined.
 
10
These are, in order of decreasing verbosity: log, debug, info, warning, error.
 
11
 
 
12
This module provides a Loggable class for objects, as well as various
 
13
convenience methods for logging in general, and for logging with Twisted
 
14
and failures in particular.
 
15
 
 
16
Maintainer: U{Thomas Vander Stichele <thomas at apestaart dot org>}
 
17
"""
 
18
 
 
19
import errno
 
20
import sys
 
21
import os
 
22
import fnmatch
 
23
import time
 
24
import types
 
25
import traceback
 
26
import thread
 
27
 
 
28
# environment variables controlling levels for each category
 
29
_DEBUG = "*:1"
 
30
# name of the environment variable controlling our logging
 
31
_ENV_VAR_NAME = None
 
32
# package names we should scrub filenames for
 
33
_PACKAGE_SCRUB_LIST = []
 
34
 
 
35
# dynamic dictionary of categories already seen and their level
 
36
_categories = {}
 
37
 
 
38
# log handlers registered
 
39
_log_handlers = []
 
40
_log_handlers_limited = []
 
41
 
 
42
_initialized = False
 
43
 
 
44
_stdout = None
 
45
_stderr = None
 
46
_old_hup_handler = None
 
47
 
 
48
 
 
49
# public log levels
 
50
(ERROR,
 
51
 WARN,
 
52
 INFO,
 
53
 DEBUG,
 
54
 LOG) = range(1, 6)
 
55
 
 
56
COLORS = {ERROR: 'RED',
 
57
          WARN: 'YELLOW',
 
58
          INFO: 'GREEN',
 
59
          DEBUG: 'BLUE',
 
60
          LOG: 'CYAN'}
 
61
 
 
62
_FORMATTED_LEVELS = []
 
63
_LEVEL_NAMES = ['ERROR', 'WARN', 'INFO', 'DEBUG', 'LOG']
 
64
 
 
65
 
 
66
def getLevelName(level):
 
67
    """
 
68
    Return the name of a log level.
 
69
    @param level: The level we want to know the name
 
70
    @type level: int
 
71
    @return: The name of the level
 
72
    @rtype: str
 
73
    """
 
74
    assert isinstance(level, int) and level > 0 and level < 6, \
 
75
           TypeError("Bad debug level")
 
76
    return getLevelNames()[level - 1]
 
77
 
 
78
 
 
79
def getLevelNames():
 
80
    """
 
81
    Return a list with the level names
 
82
    @return: A list with the level names
 
83
    @rtype: list of str
 
84
    """
 
85
    return _LEVEL_NAMES
 
86
 
 
87
 
 
88
def getLevelInt(levelName):
 
89
    """
 
90
    Return the integer value of the levelName.
 
91
    @param levelName: The string value of the level name
 
92
    @type levelName: str
 
93
    @return: The value of the level name we are interested in.
 
94
    @rtype: int
 
95
    """
 
96
    assert isinstance(levelName, str) and levelName in getLevelNames(), \
 
97
        "Bad debug level name"
 
98
    return  getLevelNames().index(levelName)+1
 
99
 
 
100
 
 
101
def getFormattedLevelName(level):
 
102
    assert isinstance(level, int) and level > 0 and level < 6, \
 
103
           TypeError("Bad debug level")
 
104
    return _FORMATTED_LEVELS[level - 1]
 
105
 
 
106
 
 
107
def registerCategory(category):
 
108
    """
 
109
    Register a given category in the debug system.
 
110
    A level will be assigned to it based on previous calls to setDebug.
 
111
    """
 
112
    # parse what level it is set to based on _DEBUG
 
113
    # example: *:2,admin:4
 
114
    global _DEBUG
 
115
    global _levels
 
116
    global _categories
 
117
 
 
118
    level = 0
 
119
    chunks = _DEBUG.split(',')
 
120
    for chunk in chunks:
 
121
        if not chunk:
 
122
            continue
 
123
        if ':' in chunk:
 
124
            spec, value = chunk.split(':')
 
125
        else:
 
126
            spec = '*'
 
127
            value = chunk
 
128
 
 
129
        # our glob is unix filename style globbing, so cheat with fnmatch
 
130
        # fnmatch.fnmatch didn't work for this, so don't use it
 
131
        if category in fnmatch.filter((category, ), spec):
 
132
            # we have a match, so set level based on string or int
 
133
            if not value:
 
134
                continue
 
135
            try:
 
136
                level = int(value)
 
137
            except ValueError: # e.g. *; we default to most
 
138
                level = 5
 
139
    # store it
 
140
    _categories[category] = level
 
141
 
 
142
 
 
143
def getCategoryLevel(category):
 
144
    """
 
145
    @param category: string
 
146
 
 
147
    Get the debug level at which this category is being logged, adding it
 
148
    if it wasn't registered yet.
 
149
    """
 
150
    global _categories
 
151
    if not category in _categories:
 
152
        registerCategory(category)
 
153
    return _categories[category]
 
154
 
 
155
 
 
156
def setLogSettings(state):
 
157
    """Update the current log settings.
 
158
    This can restore an old saved log settings object returned by
 
159
    getLogSettings
 
160
    @param state: the settings to set
 
161
    """
 
162
 
 
163
    global _DEBUG
 
164
    global _log_handlers
 
165
    global _log_handlers_limited
 
166
 
 
167
    (_DEBUG,
 
168
     _categories,
 
169
     _log_handlers,
 
170
     _log_handlers_limited) = state
 
171
 
 
172
    for category in _categories:
 
173
        registerCategory(category)
 
174
 
 
175
 
 
176
def getLogSettings():
 
177
    """Fetches the current log settings.
 
178
    The returned object can be sent to setLogSettings to restore the
 
179
    returned settings
 
180
    @returns: the current settings
 
181
    """
 
182
    return (_DEBUG,
 
183
            _categories,
 
184
            _log_handlers,
 
185
            _log_handlers_limited)
 
186
 
 
187
 
 
188
def _canShortcutLogging(category, level):
 
189
    if _log_handlers:
 
190
        # we have some loggers operating without filters, have to do
 
191
        # everything
 
192
        return False
 
193
    else:
 
194
        return level > getCategoryLevel(category)
 
195
 
 
196
 
 
197
def scrubFilename(filename):
 
198
    '''
 
199
    Scrub the filename to a relative path for all packages in our scrub list.
 
200
    '''
 
201
    global _PACKAGE_SCRUB_LIST
 
202
    for package in _PACKAGE_SCRUB_LIST:
 
203
        i = filename.rfind(package)
 
204
        if i > -1:
 
205
            return filename[i:]
 
206
 
 
207
    return filename
 
208
 
 
209
 
 
210
def getFileLine(where=-1):
 
211
    """
 
212
    Return the filename and line number for the given location.
 
213
 
 
214
    If where is a negative integer, look for the code entry in the current
 
215
    stack that is the given number of frames above this module.
 
216
    If where is a function, look for the code entry of the function.
 
217
 
 
218
    @param where: how many frames to go back up, or function
 
219
    @type  where: int (negative) or function
 
220
 
 
221
    @return: tuple of (file, line)
 
222
    @rtype:  tuple of (str, int)
 
223
    """
 
224
    co = None
 
225
    lineno = None
 
226
    name = None
 
227
 
 
228
    if isinstance(where, types.FunctionType):
 
229
        co = where.func_code
 
230
        lineno = co.co_firstlineno
 
231
        name = co.co_name
 
232
    elif isinstance(where, types.MethodType):
 
233
        co = where.im_func.func_code
 
234
        lineno = co.co_firstlineno
 
235
        name = co.co_name
 
236
    else:
 
237
        stackFrame = sys._getframe()
 
238
        while stackFrame:
 
239
            co = stackFrame.f_code
 
240
            if not co.co_filename.endswith('log.py'):
 
241
                # wind up the stack according to frame
 
242
                while where < -1:
 
243
                    stackFrame = stackFrame.f_back
 
244
                    where += 1
 
245
                co = stackFrame.f_code
 
246
                lineno = stackFrame.f_lineno
 
247
                name = co.co_name
 
248
                break
 
249
            stackFrame = stackFrame.f_back
 
250
 
 
251
    if not co:
 
252
        return "<unknown file>", 0
 
253
 
 
254
    return scrubFilename(co.co_filename), lineno, name
 
255
 
 
256
 
 
257
def ellipsize(o):
 
258
    """
 
259
    Ellipsize the representation of the given object.
 
260
    """
 
261
    r = repr(o)
 
262
    if len(r) < 800:
 
263
        return r
 
264
 
 
265
    r = r[:60] + ' ... ' + r[-15:]
 
266
    return r
 
267
 
 
268
 
 
269
def getFormatArgs(startFormat, startArgs, endFormat, endArgs, args, kwargs):
 
270
    """
 
271
    Helper function to create a format and args to use for logging.
 
272
    This avoids needlessly interpolating variables.
 
273
    """
 
274
    debugArgs = startArgs[:]
 
275
    for a in args:
 
276
        debugArgs.append(ellipsize(a))
 
277
 
 
278
    for items in kwargs.items():
 
279
        debugArgs.extend(items)
 
280
    debugArgs.extend(endArgs)
 
281
    format = startFormat \
 
282
              + ', '.join(('%s', ) * len(args)) \
 
283
              + (kwargs and ', ' or '') \
 
284
              + ', '.join(('%s=%r', ) * len(kwargs)) \
 
285
              + endFormat
 
286
    return format, debugArgs
 
287
 
 
288
 
 
289
def doLog(level, object, category, format, args, where=-1,
 
290
    filePath=None, line=None):
 
291
    """
 
292
    @param where:     what to log file and line number for;
 
293
                      -1 for one frame above log.py; -2 and down for higher up;
 
294
                      a function for a (future) code object
 
295
    @type  where:     int or callable
 
296
    @param filePath:  file to show the message as coming from, if caller
 
297
                      knows best
 
298
    @type  filePath:  str
 
299
    @param line:      line to show the message as coming from, if caller
 
300
                      knows best
 
301
    @type  line:      int
 
302
 
 
303
    @return: dict of calculated variables, if they needed calculating.
 
304
             currently contains file and line; this prevents us from
 
305
             doing this work in the caller when it isn't needed because
 
306
             of the debug level
 
307
    """
 
308
    ret = {}
 
309
 
 
310
    if args:
 
311
        message = format % args
 
312
    else:
 
313
        message = format
 
314
 
 
315
    # first all the unlimited ones
 
316
    if _log_handlers:
 
317
        if filePath is None and line is None:
 
318
            (filePath, line, funcname) = getFileLine(where=where)
 
319
        ret['filePath'] = filePath
 
320
        ret['line'] = line
 
321
        if funcname:
 
322
            message = "\033[00m\033[32;01m%s:\033[00m %s" % (funcname, message)
 
323
        for handler in _log_handlers:
 
324
            try:
 
325
                handler(level, object, category, file, line, message)
 
326
            except TypeError, e:
 
327
                raise SystemError("handler %r raised a TypeError: %s" % (
 
328
                    handler, getExceptionMessage(e)))
 
329
 
 
330
    if level > getCategoryLevel(category):
 
331
        return ret
 
332
 
 
333
    if _log_handlers_limited:
 
334
        if filePath is None and line is None:
 
335
            (filePath, line, funcname) = getFileLine(where=where)
 
336
        ret['filePath'] = filePath
 
337
        ret['line'] = line
 
338
        if funcname:
 
339
            message = "\033[00m\033[32;01m%s:\033[00m %s" % (funcname, message)
 
340
        for handler in _log_handlers_limited:
 
341
            # set this a second time, just in case there weren't unlimited
 
342
            # loggers there before
 
343
            try:
 
344
                handler(level, object, category, filePath, line, message)
 
345
            except TypeError:
 
346
                raise SystemError("handler %r raised a TypeError" % handler)
 
347
 
 
348
        return ret
 
349
 
 
350
 
 
351
def errorObject(object, cat, format, *args):
 
352
    """
 
353
    Log a fatal error message in the given category.
 
354
    This will also raise a L{SystemExit}.
 
355
    """
 
356
    doLog(ERROR, object, cat, format, args)
 
357
 
 
358
    # we do the import here because having it globally causes weird import
 
359
    # errors if our gstreactor also imports .log, which brings in errors
 
360
    # and pb stuff
 
361
    if args:
 
362
        raise SystemExit(format % args)
 
363
    else:
 
364
        raise SystemExit(format)
 
365
 
 
366
 
 
367
def warningObject(object, cat, format, *args):
 
368
    """
 
369
    Log a warning message in the given category.
 
370
    This is used for non-fatal problems.
 
371
    """
 
372
    doLog(WARN, object, cat, format, args)
 
373
 
 
374
 
 
375
def infoObject(object, cat, format, *args):
 
376
    """
 
377
    Log an informational message in the given category.
 
378
    """
 
379
    doLog(INFO, object, cat, format, args)
 
380
 
 
381
 
 
382
def debugObject(object, cat, format, *args):
 
383
    """
 
384
    Log a debug message in the given category.
 
385
    """
 
386
    doLog(DEBUG, object, cat, format, args)
 
387
 
 
388
 
 
389
def logObject(object, cat, format, *args):
 
390
    """
 
391
    Log a log message.  Used for debugging recurring events.
 
392
    """
 
393
    doLog(LOG, object, cat, format, args)
 
394
 
 
395
 
 
396
def safeprintf(file, format, *args):
 
397
    """Write to a file object, ignoring errors.
 
398
    """
 
399
    try:
 
400
        if args:
 
401
            file.write(format % args)
 
402
        else:
 
403
            file.write(format)
 
404
    except IOError, e:
 
405
        if e.errno == errno.EPIPE:
 
406
            # if our output is closed, exit; e.g. when logging over an
 
407
            # ssh connection and the ssh connection is closed
 
408
            os._exit(os.EX_OSERR)
 
409
        # otherwise ignore it, there's nothing you can do
 
410
 
 
411
 
 
412
def stderrHandler(level, object, category, file, line, message):
 
413
    """
 
414
    A log handler that writes to stderr.
 
415
 
 
416
    @type level:    string
 
417
    @type object:   string (or None)
 
418
    @type category: string
 
419
    @type message:  string
 
420
    """
 
421
 
 
422
    o = ""
 
423
    if object:
 
424
        o = '"' + object + '"'
 
425
 
 
426
    where = "(%s:%d)" % (file, line)
 
427
 
 
428
    # level   pid     object   cat      time
 
429
    # 5 + 1 + 7 + 1 + 32 + 1 + 17 + 1 + 15 == 80
 
430
    safeprintf(sys.stderr, '%s [%5d] [0x%12x] %-32s %-17s %-15s %-4s %s %s\n',
 
431
               getFormattedLevelName(level), os.getpid(), thread.get_ident(),
 
432
               o[:32], category,
 
433
               time.strftime("%b %d %H:%M:%S")
 
434
               , "", message, where)
 
435
    sys.stderr.flush()
 
436
 
 
437
 
 
438
def _preformatLevels(noColorEnvVarName):
 
439
    format = '%-5s'
 
440
 
 
441
    try:
 
442
        import termcolor
 
443
    except ImportError:
 
444
        # we don't need to catch this if termcolor is in same package as
 
445
        # log.py
 
446
        termcolor = None
 
447
 
 
448
    if (noColorEnvVarName is not None
 
449
        and termcolor is not None
 
450
        and (noColorEnvVarName not in os.environ
 
451
             or not os.environ[noColorEnvVarName])):
 
452
 
 
453
        t = termcolor.TerminalController()
 
454
        formatter = lambda level: ''.join((t.BOLD, getattr(t, COLORS[level]),
 
455
                            format % (_LEVEL_NAMES[level-1], ), t.NORMAL))
 
456
    else:
 
457
        formatter = lambda level: format % (_LEVEL_NAMES[level-1], )
 
458
 
 
459
    for level in ERROR, WARN, INFO, DEBUG, LOG:
 
460
        _FORMATTED_LEVELS.append(formatter(level))
 
461
 
 
462
### "public" useful API
 
463
 
 
464
# setup functions
 
465
 
 
466
 
 
467
def init(envVarName, enableColorOutput=False):
 
468
    """
 
469
    Initialize the logging system and parse the environment variable
 
470
    of the given name.
 
471
    Needs to be called before starting the actual application.
 
472
    """
 
473
    global _initialized
 
474
 
 
475
    if _initialized:
 
476
        return
 
477
 
 
478
    global _ENV_VAR_NAME
 
479
    _ENV_VAR_NAME = envVarName
 
480
 
 
481
    if enableColorOutput:
 
482
        _preformatLevels(envVarName + "_NO_COLOR")
 
483
    else:
 
484
        _preformatLevels(None)
 
485
 
 
486
    if envVarName in os.environ:
 
487
        # install a log handler that uses the value of the environment var
 
488
        setDebug(os.environ[envVarName])
 
489
    addLimitedLogHandler(stderrHandler)
 
490
 
 
491
    _initialized = True
 
492
 
 
493
 
 
494
def setDebug(string):
 
495
    """Set the DEBUG string.  This controls the log output."""
 
496
    global _DEBUG
 
497
    global _ENV_VAR_NAME
 
498
    global _categories
 
499
 
 
500
    _DEBUG = string
 
501
    debug('log', "%s set to %s" % (_ENV_VAR_NAME, _DEBUG))
 
502
 
 
503
    # reparse all already registered category levels
 
504
    for category in _categories:
 
505
        registerCategory(category)
 
506
 
 
507
 
 
508
def getDebug():
 
509
    """
 
510
    Returns the currently active DEBUG string.
 
511
    @rtype: str
 
512
    """
 
513
    global _DEBUG
 
514
    return _DEBUG
 
515
 
 
516
 
 
517
def setPackageScrubList(*packages):
 
518
    """
 
519
    Set the package names to scrub from filenames.
 
520
    Filenames from these paths in log messages will be scrubbed to their
 
521
    relative file path instead of the full absolute path.
 
522
 
 
523
    @type packages: list of str
 
524
    """
 
525
    global _PACKAGE_SCRUB_LIST
 
526
    _PACKAGE_SCRUB_LIST = packages
 
527
 
 
528
 
 
529
def reset():
 
530
    """
 
531
    Resets the logging system, removing all log handlers.
 
532
    """
 
533
    global _log_handlers, _log_handlers_limited, _initialized
 
534
 
 
535
    _log_handlers = []
 
536
    _log_handlers_limited = []
 
537
    _initialized = False
 
538
 
 
539
 
 
540
def addLogHandler(func):
 
541
    """
 
542
    Add a custom log handler.
 
543
 
 
544
    @param func: a function object with prototype (level, object, category,
 
545
                 message) where level is either ERROR, WARN, INFO, DEBUG, or
 
546
                 LOG, and the rest of the arguments are strings or None. Use
 
547
                 getLevelName(level) to get a printable name for the log level.
 
548
    @type func:  a callable function
 
549
 
 
550
    @raises TypeError: if func is not a callable
 
551
    """
 
552
 
 
553
    if not callable(func):
 
554
        raise TypeError("func must be callable")
 
555
 
 
556
    if func not in _log_handlers:
 
557
        _log_handlers.append(func)
 
558
 
 
559
 
 
560
def addLimitedLogHandler(func):
 
561
    """
 
562
    Add a custom log handler.
 
563
 
 
564
    @param func: a function object with prototype (level, object, category,
 
565
                 message) where level is either ERROR, WARN, INFO, DEBUG, or
 
566
                 LOG, and the rest of the arguments are strings or None. Use
 
567
                 getLevelName(level) to get a printable name for the log level.
 
568
    @type func:  a callable function
 
569
 
 
570
    @raises TypeError: TypeError if func is not a callable
 
571
    """
 
572
    if not callable(func):
 
573
        raise TypeError("func must be callable")
 
574
 
 
575
    if func not in _log_handlers_limited:
 
576
        _log_handlers_limited.append(func)
 
577
 
 
578
 
 
579
def removeLogHandler(func):
 
580
    """
 
581
    Remove a registered log handler.
 
582
 
 
583
    @param func: a function object with prototype (level, object, category,
 
584
                 message) where level is either ERROR, WARN, INFO, DEBUG, or
 
585
                 LOG, and the rest of the arguments are strings or None. Use
 
586
                 getLevelName(level) to get a printable name for the log level.
 
587
    @type func:  a callable function
 
588
 
 
589
    @raises ValueError: if func is not registered
 
590
    """
 
591
    _log_handlers.remove(func)
 
592
 
 
593
 
 
594
def removeLimitedLogHandler(func):
 
595
    """
 
596
    Remove a registered limited log handler.
 
597
 
 
598
    @param func: a function object with prototype (level, object, category,
 
599
                 message) where level is either ERROR, WARN, INFO, DEBUG, or
 
600
                 LOG, and the rest of the arguments are strings or None. Use
 
601
                 getLevelName(level) to get a printable name for the log level.
 
602
    @type func:  a callable function
 
603
 
 
604
    @raises ValueError: if func is not registered
 
605
    """
 
606
    _log_handlers_limited.remove(func)
 
607
 
 
608
# public log functions
 
609
 
 
610
 
 
611
def error(cat, format, *args):
 
612
    errorObject(None, cat, format, *args)
 
613
 
 
614
 
 
615
def warning(cat, format, *args):
 
616
    warningObject(None, cat, format, *args)
 
617
 
 
618
 
 
619
def info(cat, format, *args):
 
620
    infoObject(None, cat, format, *args)
 
621
 
 
622
 
 
623
def debug(cat, format, *args):
 
624
    debugObject(None, cat, format, *args)
 
625
 
 
626
 
 
627
def log(cat, format, *args):
 
628
    logObject(None, cat, format, *args)
 
629
 
 
630
# public utility functions
 
631
 
 
632
 
 
633
def getExceptionMessage(exception, frame=-1, filename=None):
 
634
    """
 
635
    Return a short message based on an exception, useful for debugging.
 
636
    Tries to find where the exception was triggered.
 
637
    """
 
638
    stack = traceback.extract_tb(sys.exc_info()[2])
 
639
    if filename:
 
640
        stack = [f for f in stack if f[0].find(filename) > -1]
 
641
    #import code; code.interact(local=locals())
 
642
    (filename, line, func, text) = stack[frame]
 
643
    filename = scrubFilename(filename)
 
644
    exc = exception.__class__.__name__
 
645
    msg = ""
 
646
    # a shortcut to extract a useful message out of most exceptions
 
647
    # for now
 
648
    if str(exception):
 
649
        msg = ": %s" % str(exception)
 
650
    return "exception %(exc)s at %(filename)s:%(line)s: %(func)s()%(msg)s" \
 
651
        % locals()
 
652
 
 
653
 
 
654
def reopenOutputFiles():
 
655
    """
 
656
    Reopens the stdout and stderr output files, as set by
 
657
    L{outputToFiles}.
 
658
    """
 
659
    if not _stdout and not _stderr:
 
660
        debug('log', 'told to reopen log files, but log files not set')
 
661
        return
 
662
 
 
663
    def reopen(name, fileno, *args):
 
664
        oldmask = os.umask(0026)
 
665
        try:
 
666
            f = open(name, 'a+', *args)
 
667
        finally:
 
668
            os.umask(oldmask)
 
669
 
 
670
        os.dup2(f.fileno(), fileno)
 
671
 
 
672
    if _stdout:
 
673
        reopen(_stdout, sys.stdout.fileno())
 
674
 
 
675
    if _stderr:
 
676
        reopen(_stderr, sys.stderr.fileno(), 0)
 
677
        debug('log', 'opened log %r', _stderr)
 
678
 
 
679
 
 
680
def outputToFiles(stdout=None, stderr=None):
 
681
    """
 
682
    Redirect stdout and stderr to named files.
 
683
 
 
684
    Records the file names so that a future call to reopenOutputFiles()
 
685
    can open the same files. Installs a SIGHUP handler that will reopen
 
686
    the output files.
 
687
 
 
688
    Note that stderr is opened unbuffered, so if it shares a file with
 
689
    stdout then interleaved output may not appear in the order that you
 
690
    expect.
 
691
    """
 
692
    global _stdout, _stderr, _old_hup_handler
 
693
    _stdout, _stderr = stdout, stderr
 
694
    reopenOutputFiles()
 
695
 
 
696
    def sighup(signum, frame):
 
697
        info('log', "Received SIGHUP, reopening logs")
 
698
        reopenOutputFiles()
 
699
        if _old_hup_handler:
 
700
            info('log', "Calling old SIGHUP hander")
 
701
            _old_hup_handler(signum, frame)
 
702
 
 
703
    debug('log', 'installing SIGHUP handler')
 
704
    import signal
 
705
    handler = signal.signal(signal.SIGHUP, sighup)
 
706
    if handler == signal.SIG_DFL or handler == signal.SIG_IGN:
 
707
        _old_hup_handler = None
 
708
    else:
 
709
        _old_hup_handler = handler
 
710
 
 
711
 
 
712
# base class for loggable objects
 
713
 
 
714
 
 
715
class Loggable(object):
 
716
    """
 
717
    Base class for objects that want to be able to log messages with
 
718
    different level of severity.  The levels are, in order from least
 
719
    to most: log, debug, info, warning, error.
 
720
 
 
721
    @cvar logCategory: Implementors can provide a category to log their
 
722
       messages under.
 
723
    """
 
724
 
 
725
 
 
726
    def writeMarker(self, marker, level):
 
727
        """
 
728
        Sets a marker that written to the logs. Setting this
 
729
        marker to multiple elements at a time helps debugging.
 
730
        @param marker: A string write to the log.
 
731
        @type marker: str
 
732
        @param level: The log level. It can be log.WARN, log.INFO,
 
733
        log.DEBUG, log.ERROR or log.LOG.
 
734
        @type  level: int
 
735
        """
 
736
        logHandlers = {WARN: self.warning,
 
737
                       INFO: self.info,
 
738
                       DEBUG: self.debug,
 
739
                       ERROR: self.error,
 
740
                       LOG: self.log}
 
741
        logHandler = logHandlers.get(level)
 
742
        if logHandler:
 
743
            logHandler('%s', marker)
 
744
 
 
745
    def error(self, *args):
 
746
        """Log an error.  By default this will also raise an exception."""
 
747
        if _canShortcutLogging(self.logCategory, ERROR):
 
748
            return
 
749
        errorObject(self.logObjectName(), self.logCategory,
 
750
            *self.logFunction(*args))
 
751
 
 
752
    def warning(self, *args):
 
753
        """Log a warning.  Used for non-fatal problems."""
 
754
        if _canShortcutLogging(self.logCategory, WARN):
 
755
            return
 
756
        warningObject(self.logObjectName(), self.logCategory,
 
757
            *self.logFunction(*args))
 
758
 
 
759
    def info(self, *args):
 
760
        """Log an informational message.  Used for normal operation."""
 
761
        if _canShortcutLogging(self.logCategory, INFO):
 
762
            return
 
763
        infoObject(self.logObjectName(), self.logCategory,
 
764
            *self.logFunction(*args))
 
765
 
 
766
    def debug(self, *args):
 
767
        """Log a debug message.  Used for debugging."""
 
768
        if _canShortcutLogging(self.logCategory, DEBUG):
 
769
            return
 
770
        debugObject(self.logObjectName(), self.logCategory,
 
771
            *self.logFunction(*args))
 
772
 
 
773
    def log(self, *args):
 
774
        """Log a log message.  Used for debugging recurring events."""
 
775
        if _canShortcutLogging(self.logCategory, LOG):
 
776
            return
 
777
        logObject(self.logObjectName(), self.logCategory,
 
778
            *self.logFunction(*args))
 
779
 
 
780
    def doLog(self, level, where, format, *args, **kwargs):
 
781
        """
 
782
        Log a message at the given level, with the possibility of going
 
783
        higher up in the stack.
 
784
 
 
785
        @param level: log level
 
786
        @type  level: int
 
787
        @param where: how many frames to go back from the last log frame;
 
788
                      or a function (to log for a future call)
 
789
        @type  where: int (negative), or function
 
790
 
 
791
        @param kwargs: a dict of pre-calculated values from a previous
 
792
                       doLog call
 
793
 
 
794
        @return: a dict of calculated variables, to be reused in a
 
795
                 call to doLog that should show the same location
 
796
        @rtype:  dict
 
797
        """
 
798
        if _canShortcutLogging(self.logCategory, level):
 
799
            return {}
 
800
        args = self.logFunction(*args)
 
801
        return doLog(level, self.logObjectName(), self.logCategory,
 
802
            format, args, where=where, **kwargs)
 
803
 
 
804
    def warningFailure(self, failure, swallow=True):
 
805
        """
 
806
        Log a warning about a Twisted Failure. Useful as an errback handler:
 
807
        d.addErrback(self.warningFailure)
 
808
 
 
809
        @param swallow: whether to swallow the failure or not
 
810
        @type  swallow: bool
 
811
        """
 
812
        if _canShortcutLogging(self.logCategory, WARN):
 
813
            if swallow:
 
814
                return
 
815
            return failure
 
816
        warningObject(self.logObjectName(), self.logCategory,
 
817
            *self.logFunction(getFailureMessage(failure)))
 
818
        if not swallow:
 
819
            return failure
 
820
 
 
821
    def logFunction(self, *args):
 
822
        """Overridable log function.  Default just returns passed message."""
 
823
        return args
 
824
 
 
825
    def logObjectName(self):
 
826
        """Overridable object name function."""
 
827
        # cheat pychecker
 
828
        for name in ['logName', 'name']:
 
829
            if hasattr(self, name):
 
830
                return getattr(self, name)
 
831
 
 
832
        return None
 
833
 
 
834
    def handleException(self, exc):
 
835
        self.warning(getExceptionMessage(exc))
 
836
 
 
837
# Twisted helper stuff
 
838
 
 
839
# private stuff
 
840
_initializedTwisted = False
 
841
 
 
842
# make a singleton
 
843
__theTwistedLogObserver = None
 
844
 
 
845
 
 
846
def _getTheTwistedLogObserver():
 
847
    # used internally and in test
 
848
    global __theTwistedLogObserver
 
849
 
 
850
    if not __theTwistedLogObserver:
 
851
        __theTwistedLogObserver = TwistedLogObserver()
 
852
 
 
853
    return __theTwistedLogObserver
 
854
 
 
855
 
 
856
# public helper methods
 
857
 
 
858
 
 
859
def getFailureMessage(failure):
 
860
    """
 
861
    Return a short message based on L{twisted.python.failure.Failure}.
 
862
    Tries to find where the exception was triggered.
 
863
    """
 
864
    exc = str(failure.type)
 
865
    msg = failure.getErrorMessage()
 
866
    if len(failure.frames) == 0:
 
867
        return "failure %(exc)s: %(msg)s" % locals()
 
868
 
 
869
    (func, filename, line, some, other) = failure.frames[-1]
 
870
    filename = scrubFilename(filename)
 
871
    return "failure %(exc)s at %(filename)s:%(line)s: %(func)s(): %(msg)s" \
 
872
        % locals()
 
873
 
 
874
 
 
875
def warningFailure(failure, swallow=True):
 
876
    """
 
877
    Log a warning about a Failure. Useful as an errback handler:
 
878
    d.addErrback(warningFailure)
 
879
 
 
880
    @param swallow: whether to swallow the failure or not
 
881
    @type  swallow: bool
 
882
    """
 
883
    warning('', getFailureMessage(failure))
 
884
    if not swallow:
 
885
        return failure
 
886
 
 
887
 
 
888
def logTwisted():
 
889
    """
 
890
    Integrate twisted's logger with our logger.
 
891
 
 
892
    This is done in a separate method because calling this imports and sets
 
893
    up a reactor.  Since we want basic logging working before choosing a
 
894
    reactor, we need to separate these.
 
895
    """
 
896
    global _initializedTwisted
 
897
 
 
898
    if _initializedTwisted:
 
899
        return
 
900
 
 
901
    debug('log', 'Integrating twisted logger')
 
902
 
 
903
    # integrate twisted's logging with us
 
904
    from twisted.python import log as tlog
 
905
 
 
906
    # this call imports the reactor
 
907
    # that is why we do this in a separate method
 
908
    from twisted.spread import pb
 
909
 
 
910
    # we don't want logs for pb.Error types since they
 
911
    # are specifically raised to be handled on the other side
 
912
    observer = _getTheTwistedLogObserver()
 
913
    observer.ignoreErrors([pb.Error, ])
 
914
    tlog.startLoggingWithObserver(observer.emit, False)
 
915
 
 
916
    _initializedTwisted = True
 
917
 
 
918
 
 
919
# we need an object as the observer because startLoggingWithObserver
 
920
# expects a bound method
 
921
 
 
922
 
 
923
class TwistedLogObserver(Loggable):
 
924
    """
 
925
    Twisted log observer that integrates with our logging.
 
926
    """
 
927
    logCategory = "logobserver"
 
928
 
 
929
    def __init__(self):
 
930
        self._ignoreErrors = [] # Failure types
 
931
 
 
932
    def emit(self, eventDict):
 
933
        method = log # by default, lowest level
 
934
        edm = eventDict['message']
 
935
        if not edm:
 
936
            if eventDict['isError'] and 'failure' in eventDict:
 
937
                f = eventDict['failure']
 
938
                for failureType in self._ignoreErrors:
 
939
                    r = f.check(failureType)
 
940
                    if r:
 
941
                        self.debug("Failure of type %r, ignoring" %
 
942
                                   failureType)
 
943
                        return
 
944
 
 
945
                self.log("Failure %r" % f)
 
946
 
 
947
                method = debug # tracebacks from errors at debug level
 
948
                msg = "A twisted traceback occurred."
 
949
                if getCategoryLevel("twisted") < WARN:
 
950
                    msg += "  Run with debug level >= 2 to see the traceback."
 
951
                # and an additional warning
 
952
                warning('twisted', msg)
 
953
                text = f.getTraceback()
 
954
                safeprintf(sys.stderr, "\nTwisted traceback:\n")
 
955
                safeprintf(sys.stderr, text + '\n')
 
956
            elif 'format' in eventDict:
 
957
                text = eventDict['format'] % eventDict
 
958
            else:
 
959
                # we don't know how to log this
 
960
                return
 
961
        else:
 
962
            text = ' '.join(map(str, edm))
 
963
 
 
964
        fmtDict = {'system': eventDict['system'],
 
965
                   'text': text.replace("\n", "\n\t")}
 
966
        msgStr = " [%(system)s] %(text)s\n" % fmtDict
 
967
        # because msgstr can contain %, as in a backtrace, make sure we
 
968
        # don't try to splice it
 
969
        method('twisted', msgStr)
 
970
 
 
971
    def ignoreErrors(self, *types):
 
972
        for failureType in types:
 
973
            self._ignoreErrors.append(failureType)
 
974
 
 
975
    def clearIgnores(self):
 
976
        self._ignoreErrors = []