| 1 | # -*- Mode: Python -*- |
|---|
| 2 | # vi:si:et:sw=4:sts=4:ts=4 |
|---|
| 3 | # |
|---|
| 4 | # moap - Maintenance Of A Project |
|---|
| 5 | # Copyright (C) 2006 Thomas Vander Stichele <thomas at apestaart dot org> |
|---|
| 6 | |
|---|
| 7 | # This program is free software; you can redistribute it and/or modify |
|---|
| 8 | # it under the terms of the GNU General Public License as published by |
|---|
| 9 | # the Free Software Foundation; either version 2 of the License, or |
|---|
| 10 | # (at your option) any later version. |
|---|
| 11 | # |
|---|
| 12 | # This program is distributed in the hope that it will be useful, |
|---|
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 15 | # GNU General Public License for more details. |
|---|
| 16 | # |
|---|
| 17 | # You should have received a copy of the GNU General Public License |
|---|
| 18 | # along with this program; if not, write to the Free Software |
|---|
| 19 | # Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA. |
|---|
| 20 | |
|---|
| 21 | # this file adapted from Flumotion, (C) Copyright Fluendo |
|---|
| 22 | |
|---|
| 23 | """ |
|---|
| 24 | Logging |
|---|
| 25 | """ |
|---|
| 26 | import sys |
|---|
| 27 | import os |
|---|
| 28 | import fnmatch |
|---|
| 29 | import time |
|---|
| 30 | import traceback |
|---|
| 31 | |
|---|
| 32 | # environment variables controlling levels for each category |
|---|
| 33 | _MOAP_DEBUG = "*:1" |
|---|
| 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 | # public log levels |
|---|
| 45 | ERROR = 1 |
|---|
| 46 | WARN = 2 |
|---|
| 47 | INFO = 3 |
|---|
| 48 | DEBUG = 4 |
|---|
| 49 | LOG = 5 |
|---|
| 50 | |
|---|
| 51 | def getLevelName(level): |
|---|
| 52 | """ |
|---|
| 53 | Return the name of a log level. |
|---|
| 54 | """ |
|---|
| 55 | assert isinstance(level, int) and level > 0 and level < 6, \ |
|---|
| 56 | "Bad debug level" |
|---|
| 57 | return ('ERROR', 'WARN', 'INFO', 'DEBUG', 'LOG')[level - 1] |
|---|
| 58 | |
|---|
| 59 | def registerCategory(category): |
|---|
| 60 | """ |
|---|
| 61 | Register a given category in the debug system. |
|---|
| 62 | A level will be assigned to it based on the setting of MOAP_DEBUG. |
|---|
| 63 | """ |
|---|
| 64 | # parse what level it is set to based on MOAP_DEBUG |
|---|
| 65 | # example: *:2,admin:4 |
|---|
| 66 | global _MOAP_DEBUG |
|---|
| 67 | global _levels |
|---|
| 68 | global _categories |
|---|
| 69 | |
|---|
| 70 | level = 0 |
|---|
| 71 | chunks = _MOAP_DEBUG.split(',') |
|---|
| 72 | for chunk in chunks: |
|---|
| 73 | if not chunk: |
|---|
| 74 | continue |
|---|
| 75 | if ':' in chunk: |
|---|
| 76 | spec, value = chunk.split(':') |
|---|
| 77 | else: |
|---|
| 78 | spec = '*' |
|---|
| 79 | value = chunk |
|---|
| 80 | |
|---|
| 81 | # our glob is unix filename style globbing, so cheat with fnmatch |
|---|
| 82 | # fnmatch.fnmatch didn't work for this, so don't use it |
|---|
| 83 | if category in fnmatch.filter((category, ), spec): |
|---|
| 84 | # we have a match, so set level based on string or int |
|---|
| 85 | if not value: |
|---|
| 86 | continue |
|---|
| 87 | try: |
|---|
| 88 | level = int(value) |
|---|
| 89 | except ValueError: # e.g. *; we default to most |
|---|
| 90 | level = 5 |
|---|
| 91 | # store it |
|---|
| 92 | _categories[category] = level |
|---|
| 93 | |
|---|
| 94 | def getCategoryLevel(category): |
|---|
| 95 | """ |
|---|
| 96 | @param category: string |
|---|
| 97 | |
|---|
| 98 | Get the debug level at which this category is being logged, adding it |
|---|
| 99 | if it wasn't registered yet. |
|---|
| 100 | """ |
|---|
| 101 | global _categories |
|---|
| 102 | if not _categories.has_key(category): |
|---|
| 103 | registerCategory(category) |
|---|
| 104 | return _categories[category] |
|---|
| 105 | |
|---|
| 106 | def _canShortcutLogging(category, level): |
|---|
| 107 | if _log_handlers: |
|---|
| 108 | # we have some loggers operating without filters, have to do |
|---|
| 109 | # everything |
|---|
| 110 | return False |
|---|
| 111 | else: |
|---|
| 112 | return level > getCategoryLevel(category) |
|---|
| 113 | |
|---|
| 114 | def scrubFilename(filename): |
|---|
| 115 | ''' |
|---|
| 116 | Scrub the filename of everything before 'moap' |
|---|
| 117 | ''' |
|---|
| 118 | i = filename.rfind('moap') |
|---|
| 119 | if i != -1: |
|---|
| 120 | filename = filename[i:] |
|---|
| 121 | |
|---|
| 122 | return filename |
|---|
| 123 | |
|---|
| 124 | def _handle(level, object, category, format, args): |
|---|
| 125 | def getFileLine(): |
|---|
| 126 | # Return a tuple of (file, line) for the first stack entry |
|---|
| 127 | # outside of log.py (which would be the caller of log) |
|---|
| 128 | frame = sys._getframe() |
|---|
| 129 | while frame: |
|---|
| 130 | co = frame.f_code |
|---|
| 131 | if not co.co_filename.endswith('log.py'): |
|---|
| 132 | return scrubFilename(co.co_filename), frame.f_lineno |
|---|
| 133 | frame = frame.f_back |
|---|
| 134 | |
|---|
| 135 | return "Not found", 0 |
|---|
| 136 | |
|---|
| 137 | if args: |
|---|
| 138 | message = format % args |
|---|
| 139 | else: |
|---|
| 140 | message = format |
|---|
| 141 | |
|---|
| 142 | # first all the unlimited ones |
|---|
| 143 | if _log_handlers: |
|---|
| 144 | (fileName, line) = getFileLine() |
|---|
| 145 | for handler in _log_handlers: |
|---|
| 146 | try: |
|---|
| 147 | handler(level, object, category, fileName, line, message) |
|---|
| 148 | except TypeError: |
|---|
| 149 | raise SystemError, "handler %r raised a TypeError" % handler |
|---|
| 150 | |
|---|
| 151 | if level > getCategoryLevel(category): |
|---|
| 152 | return |
|---|
| 153 | |
|---|
| 154 | # set this a second time, just in case there weren't unlimited |
|---|
| 155 | # loggers there before |
|---|
| 156 | (fileName, line) = getFileLine() |
|---|
| 157 | |
|---|
| 158 | for handler in _log_handlers_limited: |
|---|
| 159 | try: |
|---|
| 160 | handler(level, object, category, fileName, line, message) |
|---|
| 161 | except TypeError: |
|---|
| 162 | raise SystemError, "handler %r raised a TypeError" % handler |
|---|
| 163 | |
|---|
| 164 | def errorObject(object, cat, format, *args): |
|---|
| 165 | """ |
|---|
| 166 | Log a fatal error message in the given category. \ |
|---|
| 167 | This will also raise a L{SystemError}. |
|---|
| 168 | """ |
|---|
| 169 | _handle(ERROR, object, cat, format, args) |
|---|
| 170 | |
|---|
| 171 | # we do the import here because having it globally causes weird import |
|---|
| 172 | # errors if our gstreactor also imports .log, which brings in errors |
|---|
| 173 | # and pb stuff |
|---|
| 174 | if args: |
|---|
| 175 | raise SystemError(format % args) |
|---|
| 176 | else: |
|---|
| 177 | raise SystemError(format) |
|---|
| 178 | |
|---|
| 179 | def warningObject(object, cat, format, *args): |
|---|
| 180 | """ |
|---|
| 181 | Log a warning message in the given category. |
|---|
| 182 | This is used for non-fatal problems. |
|---|
| 183 | """ |
|---|
| 184 | _handle(WARN, object, cat, format, args) |
|---|
| 185 | |
|---|
| 186 | def infoObject(object, cat, format, *args): |
|---|
| 187 | """ |
|---|
| 188 | Log an informational message in the given category. |
|---|
| 189 | """ |
|---|
| 190 | _handle(INFO, object, cat, format, args) |
|---|
| 191 | |
|---|
| 192 | def debugObject(object, cat, format, *args): |
|---|
| 193 | """ |
|---|
| 194 | Log a debug message in the given category. |
|---|
| 195 | """ |
|---|
| 196 | _handle(DEBUG, object, cat, format, args) |
|---|
| 197 | |
|---|
| 198 | def logObject(object, cat, format, *args): |
|---|
| 199 | """ |
|---|
| 200 | Log a log message. Used for debugging recurring events. |
|---|
| 201 | """ |
|---|
| 202 | _handle(LOG, object, cat, format, args) |
|---|
| 203 | |
|---|
| 204 | def error(cat, format, *args): |
|---|
| 205 | errorObject(None, cat, format, *args) |
|---|
| 206 | |
|---|
| 207 | def warning(cat, format, *args): |
|---|
| 208 | warningObject(None, cat, format, *args) |
|---|
| 209 | |
|---|
| 210 | def info(cat, format, *args): |
|---|
| 211 | infoObject(None, cat, format, *args) |
|---|
| 212 | |
|---|
| 213 | def debug(cat, format, *args): |
|---|
| 214 | debugObject(None, cat, format, *args) |
|---|
| 215 | |
|---|
| 216 | def log(cat, format, *args): |
|---|
| 217 | logObject(None, cat, format, *args) |
|---|
| 218 | |
|---|
| 219 | class Loggable: |
|---|
| 220 | """ |
|---|
| 221 | Base class for objects that want to be able to log messages with |
|---|
| 222 | different level of severity. The levels are, in order from least |
|---|
| 223 | to most: log, debug, info, warning, error. |
|---|
| 224 | |
|---|
| 225 | @cvar logCategory: Implementors can provide a category to log their |
|---|
| 226 | messages under. |
|---|
| 227 | """ |
|---|
| 228 | |
|---|
| 229 | logCategory = 'default' |
|---|
| 230 | |
|---|
| 231 | def error(self, *args): |
|---|
| 232 | """Log an error. By default this will also raise an exception.""" |
|---|
| 233 | if _canShortcutLogging(self.logCategory, ERROR): |
|---|
| 234 | return |
|---|
| 235 | errorObject(self.logObjectName(), self.logCategory, |
|---|
| 236 | *self.logFunction(*args)) |
|---|
| 237 | |
|---|
| 238 | def warning(self, *args): |
|---|
| 239 | """Log a warning. Used for non-fatal problems.""" |
|---|
| 240 | if _canShortcutLogging(self.logCategory, WARN): |
|---|
| 241 | return |
|---|
| 242 | warningObject(self.logObjectName(), self.logCategory, |
|---|
| 243 | *self.logFunction(*args)) |
|---|
| 244 | |
|---|
| 245 | def info(self, *args): |
|---|
| 246 | """Log an informational message. Used for normal operation.""" |
|---|
| 247 | if _canShortcutLogging(self.logCategory, INFO): |
|---|
| 248 | return |
|---|
| 249 | infoObject(self.logObjectName(), self.logCategory, |
|---|
| 250 | *self.logFunction(*args)) |
|---|
| 251 | |
|---|
| 252 | def debug(self, *args): |
|---|
| 253 | """Log a debug message. Used for debugging.""" |
|---|
| 254 | if _canShortcutLogging(self.logCategory, DEBUG): |
|---|
| 255 | return |
|---|
| 256 | debugObject(self.logObjectName(), self.logCategory, |
|---|
| 257 | *self.logFunction(*args)) |
|---|
| 258 | |
|---|
| 259 | def log(self, *args): |
|---|
| 260 | """Log a log message. Used for debugging recurring events.""" |
|---|
| 261 | if _canShortcutLogging(self.logCategory, LOG): |
|---|
| 262 | return |
|---|
| 263 | logObject(self.logObjectName(), self.logCategory, |
|---|
| 264 | *self.logFunction(*args)) |
|---|
| 265 | |
|---|
| 266 | def logFunction(self, *args): |
|---|
| 267 | """Overridable log function. Default just returns passed message.""" |
|---|
| 268 | return args |
|---|
| 269 | |
|---|
| 270 | def logObjectName(self): |
|---|
| 271 | """Overridable object name function.""" |
|---|
| 272 | # cheat pychecker |
|---|
| 273 | for name in ['logName', 'name']: |
|---|
| 274 | if hasattr(self, name): |
|---|
| 275 | return getattr(self, name) |
|---|
| 276 | |
|---|
| 277 | return None |
|---|
| 278 | |
|---|
| 279 | def stderrHandler(level, object, category, file, line, message): |
|---|
| 280 | """ |
|---|
| 281 | A log handler that writes to stdout. |
|---|
| 282 | |
|---|
| 283 | @type level: string |
|---|
| 284 | @type object: string (or None) |
|---|
| 285 | @type category: string |
|---|
| 286 | @type message: string |
|---|
| 287 | """ |
|---|
| 288 | |
|---|
| 289 | o = "" |
|---|
| 290 | if object: |
|---|
| 291 | o = '"' + object + '"' |
|---|
| 292 | |
|---|
| 293 | where = "(%s:%d)" % (file, line) |
|---|
| 294 | |
|---|
| 295 | try: |
|---|
| 296 | # level pid object cat time |
|---|
| 297 | # 5 + 1 + 7 + 1 + 32 + 1 + 17 + 1 + 15 == 80 |
|---|
| 298 | sys.stderr.write('%-5s [%5d] %-32s %-17s %-15s ' % ( |
|---|
| 299 | getLevelName(level), os.getpid(), o, category, |
|---|
| 300 | time.strftime("%b %d %H:%M:%S"))) |
|---|
| 301 | sys.stderr.write('%-4s %s %s\n' % ("", message, where)) |
|---|
| 302 | |
|---|
| 303 | # old: 5 + 1 + 20 + 1 + 12 + 1 + 32 + 1 + 7 == 80 |
|---|
| 304 | #sys.stderr.write('%-5s %-20s %-12s %-32s [%5d] %-4s %-15s %s\n' % ( |
|---|
| 305 | # level, o, category, where, os.getpid(), |
|---|
| 306 | # "", time.strftime("%b %d %H:%M:%S"), message)) |
|---|
| 307 | sys.stderr.flush() |
|---|
| 308 | except IOError: |
|---|
| 309 | # happens in SIGCHLDHandler for example |
|---|
| 310 | pass |
|---|
| 311 | |
|---|
| 312 | def addLogHandler(func, limited=True): |
|---|
| 313 | """ |
|---|
| 314 | Add a custom log handler. |
|---|
| 315 | |
|---|
| 316 | @param func: a function object |
|---|
| 317 | with prototype (level, object, category, message) |
|---|
| 318 | where level is either ERROR, WARN, INFO, DEBUG, or |
|---|
| 319 | LOG, and the rest of the arguments are strings or |
|---|
| 320 | None. Use getLevelName(level) to get a printable |
|---|
| 321 | name for the log level. |
|---|
| 322 | @type func: a callable function |
|---|
| 323 | @type limited: boolean |
|---|
| 324 | @param limited: whether to automatically filter based on FLU_DEBUG |
|---|
| 325 | """ |
|---|
| 326 | |
|---|
| 327 | if not callable(func): |
|---|
| 328 | raise TypeError, "func must be callable" |
|---|
| 329 | |
|---|
| 330 | if limited: |
|---|
| 331 | _log_handlers_limited.append(func) |
|---|
| 332 | else: |
|---|
| 333 | _log_handlers.append(func) |
|---|
| 334 | |
|---|
| 335 | def init(): |
|---|
| 336 | """ |
|---|
| 337 | Initialize the logging system and parse the MOAP_DEBUG environment |
|---|
| 338 | variable. Needs to be called before starting the actual application. |
|---|
| 339 | """ |
|---|
| 340 | global _initialized |
|---|
| 341 | |
|---|
| 342 | if _initialized: |
|---|
| 343 | return |
|---|
| 344 | |
|---|
| 345 | if os.environ.has_key('MOAP_DEBUG'): |
|---|
| 346 | # install a log handler that uses the value of MOAP_DEBUG |
|---|
| 347 | setSavonDebug(os.environ['MOAP_DEBUG']) |
|---|
| 348 | addLogHandler(stderrHandler, limited=True) |
|---|
| 349 | |
|---|
| 350 | _initialized = True |
|---|
| 351 | |
|---|
| 352 | def reset(): |
|---|
| 353 | """ |
|---|
| 354 | Resets the logging system, removing all log handlers. |
|---|
| 355 | """ |
|---|
| 356 | global _log_handlers, _log_handlers_limited, _initialized |
|---|
| 357 | |
|---|
| 358 | _log_handlers = [] |
|---|
| 359 | _log_handlers_limited = [] |
|---|
| 360 | _initialized = False |
|---|
| 361 | |
|---|
| 362 | def setSavonDebug(string): |
|---|
| 363 | """Set the MOAP_DEBUG string. This controls the log output.""" |
|---|
| 364 | global _MOAP_DEBUG |
|---|
| 365 | global _categories |
|---|
| 366 | |
|---|
| 367 | _MOAP_DEBUG = string |
|---|
| 368 | debug('log', "MOAP_DEBUG set to %s" % _MOAP_DEBUG) |
|---|
| 369 | |
|---|
| 370 | # reparse all already registered category levels |
|---|
| 371 | for category in _categories: |
|---|
| 372 | registerCategory(category) |
|---|
| 373 | |
|---|
| 374 | def getExceptionMessage(exception, frame=-1, filename=None): |
|---|
| 375 | """ |
|---|
| 376 | Return a short message based on an exception, useful for debugging. |
|---|
| 377 | Tries to find where the exception was triggered. |
|---|
| 378 | """ |
|---|
| 379 | stack = traceback.extract_tb(sys.exc_info()[2]) |
|---|
| 380 | if filename: |
|---|
| 381 | stack = [f for f in stack if f[0].find(filename) > -1] |
|---|
| 382 | #import code; code.interact(local=locals()) |
|---|
| 383 | (filename, line, func, text) = stack[frame] |
|---|
| 384 | filename = scrubFilename(filename) |
|---|
| 385 | exc = exception.__class__.__name__ |
|---|
| 386 | msg = "" |
|---|
| 387 | # a shortcut to extract a useful message out of most flumotion errors |
|---|
| 388 | # for now |
|---|
| 389 | if len(exception.args) == 1 and isinstance(exception.args[0], str): |
|---|
| 390 | msg = ": %s" % exception.args[0] |
|---|
| 391 | return "exception %(exc)s at %(filename)s:%(line)s: %(func)s()%(msg)s" % locals() |
|---|