loggers.py 9.9 KB

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