loggers.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. """
  2. Logger implementation loosely modeled on PEP 282. We don't use the
  3. PEP 282 logger implementation in the stdlib ('logging') because it's
  4. idiosyncratic and a bit slow for our purposes (we don't use threads).
  5. """
  6. # This module must not depend on any non-stdlib modules to
  7. # avoid circular import problems
  8. import os
  9. import errno
  10. import sys
  11. import time
  12. import traceback
  13. try:
  14. import syslog
  15. except ImportError:
  16. # only required when 'syslog' is specified as the log filename
  17. pass
  18. class LevelsByName:
  19. CRIT = 50 # messages that probably require immediate user attention
  20. ERRO = 40 # messages that indicate a potentially ignorable error condition
  21. WARN = 30 # messages that indicate issues which aren't errors
  22. INFO = 20 # normal informational output
  23. DEBG = 10 # messages useful for users trying to debug configurations
  24. TRAC = 5 # messages useful to developers trying to debug plugins
  25. BLAT = 3 # messages useful for developers trying to debug supervisor
  26. class LevelsByDescription:
  27. critical = LevelsByName.CRIT
  28. error = LevelsByName.ERRO
  29. warn = LevelsByName.WARN
  30. info = LevelsByName.INFO
  31. debug = LevelsByName.DEBG
  32. trace = LevelsByName.TRAC
  33. blather = LevelsByName.BLAT
  34. def _levelNumbers():
  35. bynumber = {}
  36. for name, number in LevelsByName.__dict__.items():
  37. if not name.startswith('_'):
  38. bynumber[number] = name
  39. return bynumber
  40. LOG_LEVELS_BY_NUM = _levelNumbers()
  41. def getLevelNumByDescription(description):
  42. num = getattr(LevelsByDescription, description, None)
  43. return num
  44. class Handler:
  45. fmt = '%(message)s'
  46. level = LevelsByName.INFO
  47. def setFormat(self, fmt):
  48. self.fmt = fmt
  49. def setLevel(self, level):
  50. self.level = level
  51. def flush(self):
  52. try:
  53. self.stream.flush()
  54. except IOError, why:
  55. # if supervisor output is piped, EPIPE can be raised at exit
  56. if why.args[0] != errno.EPIPE:
  57. raise
  58. def close(self):
  59. if hasattr(self.stream, 'fileno'):
  60. fd = self.stream.fileno()
  61. if fd < 3: # don't ever close stdout or stderr
  62. return
  63. self.stream.close()
  64. def emit(self, record):
  65. try:
  66. msg = self.fmt % record.asdict()
  67. try:
  68. self.stream.write(msg)
  69. except UnicodeError:
  70. self.stream.write(msg.encode("UTF-8"))
  71. self.flush()
  72. except:
  73. self.handleError()
  74. def handleError(self):
  75. ei = sys.exc_info()
  76. traceback.print_exception(ei[0], ei[1], ei[2], None, sys.stderr)
  77. del ei
  78. class FileHandler(Handler):
  79. """File handler which supports reopening of logs.
  80. """
  81. def __init__(self, filename, mode="a"):
  82. self.stream = open(filename, mode)
  83. self.baseFilename = filename
  84. self.mode = mode
  85. def reopen(self):
  86. self.close()
  87. self.stream = open(self.baseFilename, self.mode)
  88. def remove(self):
  89. try:
  90. os.remove(self.baseFilename)
  91. except OSError, why:
  92. if why.args[0] != errno.ENOENT:
  93. raise
  94. class StreamHandler(Handler):
  95. def __init__(self, strm=None):
  96. self.stream = strm
  97. def remove(self):
  98. if hasattr(self.stream, 'clear'):
  99. self.stream.clear()
  100. def reopen(self):
  101. pass
  102. class BoundIO:
  103. def __init__(self, maxbytes, buf=''):
  104. self.maxbytes = maxbytes
  105. self.buf = buf
  106. def flush(self):
  107. pass
  108. def close(self):
  109. self.clear()
  110. def write(self, s):
  111. slen = len(s)
  112. if len(self.buf) + slen > self.maxbytes:
  113. self.buf = self.buf[slen:]
  114. self.buf += s
  115. def getvalue(self):
  116. return self.buf
  117. def clear(self):
  118. self.buf = ''
  119. class RotatingFileHandler(FileHandler):
  120. def __init__(self, filename, mode='a', maxBytes=512*1024*1024,
  121. backupCount=10):
  122. """
  123. Open the specified file and use it as the stream for logging.
  124. By default, the file grows indefinitely. You can specify particular
  125. values of maxBytes and backupCount to allow the file to rollover at
  126. a predetermined size.
  127. Rollover occurs whenever the current log file is nearly maxBytes in
  128. length. If backupCount is >= 1, the system will successively create
  129. new files with the same pathname as the base file, but with extensions
  130. ".1", ".2" etc. appended to it. For example, with a backupCount of 5
  131. and a base file name of "app.log", you would get "app.log",
  132. "app.log.1", "app.log.2", ... through to "app.log.5". The file being
  133. written to is always "app.log" - when it gets filled up, it is closed
  134. and renamed to "app.log.1", and if files "app.log.1", "app.log.2" etc.
  135. exist, then they are renamed to "app.log.2", "app.log.3" etc.
  136. respectively.
  137. If maxBytes is zero, rollover never occurs.
  138. """
  139. if maxBytes > 0:
  140. mode = 'a' # doesn't make sense otherwise!
  141. FileHandler.__init__(self, filename, mode)
  142. self.maxBytes = maxBytes
  143. self.backupCount = backupCount
  144. self.counter = 0
  145. self.every = 10
  146. def emit(self, record):
  147. """
  148. Emit a record.
  149. Output the record to the file, catering for rollover as described
  150. in doRollover().
  151. """
  152. FileHandler.emit(self, record)
  153. self.doRollover()
  154. def removeAndRename(self, sfn, dfn):
  155. if os.path.exists(dfn):
  156. try:
  157. os.remove(dfn)
  158. except OSError, why:
  159. # catch race condition (destination already deleted)
  160. if why.args[0] != errno.ENOENT:
  161. raise
  162. try:
  163. os.rename(sfn, dfn)
  164. except OSError, why:
  165. # catch exceptional condition (source deleted)
  166. # E.g. cleanup script removes active log.
  167. if why.args[0] != errno.ENOENT:
  168. raise
  169. def doRollover(self):
  170. """
  171. Do a rollover, as described in __init__().
  172. """
  173. if self.maxBytes <= 0:
  174. return
  175. if not (self.stream.tell() >= self.maxBytes):
  176. return
  177. self.stream.close()
  178. if self.backupCount > 0:
  179. for i in range(self.backupCount - 1, 0, -1):
  180. sfn = "%s.%d" % (self.baseFilename, i)
  181. dfn = "%s.%d" % (self.baseFilename, i + 1)
  182. if os.path.exists(sfn):
  183. self.removeAndRename(sfn, dfn)
  184. dfn = self.baseFilename + ".1"
  185. self.removeAndRename(self.baseFilename, dfn)
  186. self.stream = open(self.baseFilename, 'w')
  187. class LogRecord:
  188. def __init__(self, level, msg, **kw):
  189. self.level = level
  190. self.msg = msg
  191. self.kw = kw
  192. self.dictrepr = None
  193. def asdict(self):
  194. if self.dictrepr is None:
  195. now = time.time()
  196. msecs = (now - long(now)) * 1000
  197. part1 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now))
  198. asctime = '%s,%03d' % (part1, msecs)
  199. levelname = LOG_LEVELS_BY_NUM[self.level]
  200. if self.kw:
  201. msg = self.msg % self.kw
  202. else:
  203. msg = self.msg
  204. self.dictrepr = {'message':msg, 'levelname':levelname,
  205. 'asctime':asctime}
  206. return self.dictrepr
  207. class Logger:
  208. def __init__(self, level=None, handlers=None):
  209. if level is None:
  210. level = LevelsByName.INFO
  211. self.level = level
  212. if handlers is None:
  213. handlers = []
  214. self.handlers = handlers
  215. def close(self):
  216. for handler in self.handlers:
  217. handler.close()
  218. def blather(self, msg, **kw):
  219. if LevelsByName.BLAT >= self.level:
  220. self.log(LevelsByName.BLAT, msg, **kw)
  221. def trace(self, msg, **kw):
  222. if LevelsByName.TRAC >= self.level:
  223. self.log(LevelsByName.TRAC, msg, **kw)
  224. def debug(self, msg, **kw):
  225. if LevelsByName.DEBG >= self.level:
  226. self.log(LevelsByName.DEBG, msg, **kw)
  227. def info(self, msg, **kw):
  228. if LevelsByName.INFO >= self.level:
  229. self.log(LevelsByName.INFO, msg, **kw)
  230. def warn(self, msg, **kw):
  231. if LevelsByName.WARN >= self.level:
  232. self.log(LevelsByName.WARN, msg, **kw)
  233. def error(self, msg, **kw):
  234. if LevelsByName.ERRO >= self.level:
  235. self.log(LevelsByName.ERRO, msg, **kw)
  236. def critical(self, msg, **kw):
  237. if LevelsByName.CRIT >= self.level:
  238. self.log(LevelsByName.CRIT, msg, **kw)
  239. def log(self, level, msg, **kw):
  240. record = LogRecord(level, msg, **kw)
  241. for handler in self.handlers:
  242. if level >= handler.level:
  243. handler.emit(record)
  244. def addHandler(self, hdlr):
  245. self.handlers.append(hdlr)
  246. def getvalue(self):
  247. raise NotImplementedError
  248. class SyslogHandler(Handler):
  249. def __init__(self):
  250. assert 'syslog' in globals(), "Syslog module not present"
  251. def close(self):
  252. pass
  253. def reopen(self):
  254. pass
  255. def emit(self, record):
  256. try:
  257. params = record.asdict()
  258. message = params['message']
  259. for line in message.rstrip('\n').split('\n'):
  260. params['message'] = line
  261. msg = self.fmt % params
  262. try:
  263. syslog.syslog(msg)
  264. except UnicodeError:
  265. syslog.syslog(msg.encode("UTF-8"))
  266. except:
  267. self.handleError()
  268. def getLogger(filename, level, fmt, rotating=False, maxbytes=0, backups=0,
  269. stdout=False):
  270. handlers = []
  271. logger = Logger(level)
  272. if filename is None:
  273. if not maxbytes:
  274. maxbytes = 1<<21 #2MB
  275. io = BoundIO(maxbytes)
  276. handlers.append(StreamHandler(io))
  277. logger.getvalue = io.getvalue
  278. elif filename == 'syslog':
  279. handlers.append(SyslogHandler())
  280. else:
  281. if rotating is False:
  282. handlers.append(FileHandler(filename))
  283. else:
  284. handlers.append(RotatingFileHandler(filename,'a',maxbytes,backups))
  285. if stdout:
  286. handlers.append(StreamHandler(sys.stdout))
  287. for handler in handlers:
  288. handler.setFormat(fmt)
  289. handler.setLevel(level)
  290. logger.addHandler(handler)
  291. return logger