Logger.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2011-2016 Luca Wehrstedt <luca.wehrstedt@gmail.com>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. import curses
  18. import logging
  19. import os.path
  20. import sys
  21. import time
  22. from traceback import format_tb
  23. import gevent.lock
  24. class StreamHandler(logging.StreamHandler):
  25. """Subclass to make gevent-aware.
  26. Use a gevent lock instead of a threading one to block only the
  27. current greenlet.
  28. """
  29. def createLock(self):
  30. """Set self.lock to a new gevent RLock.
  31. """
  32. self.lock = gevent.lock.RLock()
  33. class FileHandler(logging.FileHandler):
  34. """Subclass to make gevent-aware.
  35. Use a gevent lock instead of a threading one to block only the
  36. current greenlet.
  37. """
  38. def createLock(self):
  39. """Set self.lock to a new gevent RLock.
  40. """
  41. self.lock = gevent.lock.RLock()
  42. def has_color_support(stream):
  43. """Try to determine if the given stream supports colored output.
  44. Return True only if the stream declares to be a TTY, if it has a
  45. file descriptor on which ncurses can initialize a terminal and if
  46. that terminal's entry in terminfo declares support for colors.
  47. stream (fileobj): a file-like object (that adheres to the API
  48. declared in the `io' package).
  49. return (bool): True if we're sure that colors are supported, False
  50. if they aren't or if we can't tell.
  51. """
  52. if stream.isatty():
  53. try:
  54. curses.setupterm(fd=stream.fileno())
  55. # See `man terminfo` for capabilities' names and meanings.
  56. if curses.tigetnum("colors") > 0:
  57. return True
  58. # fileno() can raise OSError.
  59. except Exception:
  60. pass
  61. return False
  62. ## ANSI utilities. See for reference:
  63. # http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
  64. # http://en.wikipedia.org/wiki/ANSI_escape_code
  65. #
  66. ANSI_FG_COLORS = {'black': 30,
  67. 'red': 31,
  68. 'green': 32,
  69. 'yellow': 33,
  70. 'blue': 34,
  71. 'magenta': 35,
  72. 'cyan': 36,
  73. 'white': 37}
  74. ANSI_BG_COLORS = {'black': 40,
  75. 'red': 41,
  76. 'green': 42,
  77. 'yellow': 43,
  78. 'blue': 44,
  79. 'magenta': 45,
  80. 'cyan': 46,
  81. 'white': 47}
  82. ANSI_RESET_CMD = 0
  83. ANSI_FG_DEFAULT_CMD = 39
  84. ANSI_BG_DEFAULT_CMD = 49
  85. ANSI_BOLD_ON_CMD = 1
  86. ANSI_BOLD_OFF_CMD = 22
  87. ANSI_FAINT_ON_CMD = 2
  88. ANSI_FAINT_OFF_CMD = 22
  89. ANSI_ITALICS_ON_CMD = 3
  90. ANSI_ITALICS_OFF_CMD = 23
  91. ANSI_UNDERLINE_ON_CMD = 4
  92. ANSI_UNDERLINE_OFF_CMD = 24
  93. ANSI_STRIKETHROUGH_ON_CMD = 9
  94. ANSI_STRIKETHROUGH_OFF_CMD = 29
  95. ANSI_INVERSE_ON_CMD = 7
  96. ANSI_INVERSE_OFF_CMD = 27
  97. # TODO missing:
  98. # - distinction between single and double underline
  99. # - "slow blink on", "rapid blink on" and "blink off"
  100. # - "conceal on" and "conceal off" (also called reveal)
  101. class CustomFormatter(logging.Formatter):
  102. """A custom Formatter for our logs.
  103. """
  104. def __init__(self, color=True, *args, **kwargs):
  105. """Initialize the formatter.
  106. Based on the 'color' parameter we set the tags for many
  107. elements of our formatted output.
  108. """
  109. logging.Formatter.__init__(self, *args, **kwargs)
  110. self.color = color
  111. self.time_prefix = self.ansi_command(ANSI_BOLD_ON_CMD)
  112. self.time_suffix = self.ansi_command(ANSI_BOLD_OFF_CMD)
  113. self.cri_prefix = self.ansi_command(ANSI_BOLD_ON_CMD,
  114. ANSI_FG_COLORS['white'],
  115. ANSI_BG_COLORS['red'])
  116. self.cri_suffix = self.ansi_command(ANSI_BOLD_OFF_CMD,
  117. ANSI_FG_DEFAULT_CMD,
  118. ANSI_BG_DEFAULT_CMD)
  119. self.err_prefix = self.ansi_command(ANSI_BOLD_ON_CMD,
  120. ANSI_FG_COLORS['red'])
  121. self.err_suffix = self.ansi_command(ANSI_BOLD_OFF_CMD,
  122. ANSI_FG_DEFAULT_CMD)
  123. self.wrn_prefix = self.ansi_command(ANSI_BOLD_ON_CMD,
  124. ANSI_FG_COLORS['yellow'])
  125. self.wrn_suffix = self.ansi_command(ANSI_BOLD_OFF_CMD,
  126. ANSI_FG_DEFAULT_CMD)
  127. self.inf_prefix = self.ansi_command(ANSI_BOLD_ON_CMD,
  128. ANSI_FG_COLORS['green'])
  129. self.inf_suffix = self.ansi_command(ANSI_BOLD_OFF_CMD,
  130. ANSI_FG_DEFAULT_CMD)
  131. self.dbg_prefix = self.ansi_command(ANSI_BOLD_ON_CMD,
  132. ANSI_FG_COLORS['blue'])
  133. self.dbg_suffix = self.ansi_command(ANSI_BOLD_OFF_CMD,
  134. ANSI_FG_DEFAULT_CMD)
  135. def ansi_command(self, *args):
  136. """Produce the escape string that corresponds to the given
  137. ANSI command.
  138. """
  139. return "\033[%sm" % ';'.join(["%s" % x
  140. for x in args]) if self.color else ''
  141. def formatException(self, exc_info):
  142. exc_type, exc_value, traceback = exc_info
  143. result = "%sException%s %s.%s%s%s:\n\n %s\n\n" % \
  144. (self.ansi_command(ANSI_BOLD_ON_CMD),
  145. self.ansi_command(ANSI_BOLD_OFF_CMD),
  146. exc_type.__module__,
  147. self.ansi_command(ANSI_BOLD_ON_CMD),
  148. exc_type.__name__,
  149. self.ansi_command(ANSI_BOLD_OFF_CMD),
  150. exc_value)
  151. result += "%sTraceback (most recent call last):%s\n%s" % \
  152. (self.ansi_command(ANSI_FAINT_ON_CMD),
  153. self.ansi_command(ANSI_FAINT_OFF_CMD),
  154. '\n'.join(map(lambda a: self.ansi_command(ANSI_FAINT_ON_CMD) +
  155. a + self.ansi_command(ANSI_FAINT_OFF_CMD),
  156. ''.join(format_tb(traceback)).strip().split('\n'))))
  157. return result
  158. def format(self, record):
  159. """Do the actual formatting.
  160. Prepend a timestamp and an abbreviation of the logging level,
  161. followed by the message, the request_body (if present) and the
  162. exception details (if present).
  163. """
  164. result = '%s%s.%03d%s' % \
  165. (self.time_prefix,
  166. self.formatTime(record, '%Y-%m-%d %H:%M:%S'), record.msecs,
  167. self.time_suffix)
  168. if record.levelno == logging.CRITICAL:
  169. result += ' %s CRI %s ' % (self.cri_prefix, self.cri_suffix)
  170. elif record.levelno == logging.ERROR:
  171. result += ' %s ERR %s ' % (self.err_prefix, self.err_suffix)
  172. elif record.levelno == logging.WARNING:
  173. result += ' %s WRN %s ' % (self.wrn_prefix, self.wrn_suffix)
  174. elif record.levelno == logging.INFO:
  175. result += ' %s INF %s ' % (self.inf_prefix, self.inf_suffix)
  176. else: # DEBUG
  177. result += ' %s DBG %s ' % (self.dbg_prefix, self.dbg_suffix)
  178. try:
  179. message = record.getMessage()
  180. except Exception as exc:
  181. message = 'Bad message (%r): %r' % (exc, record.__dict__)
  182. result += message.strip()
  183. if "location" in record.__dict__:
  184. result += "\n%s" % record.location.strip()
  185. if "details" in record.__dict__:
  186. result += "\n\n%s" % record.details.strip()
  187. if record.exc_info:
  188. result += "\n\n%s" % self.formatException(record.exc_info).strip()
  189. return result.replace("\n", "\n ") + '\n'
  190. # Create a global reference to the root logger.
  191. root_logger = logging.getLogger()
  192. # Catch all logging messages (we'll filter them on the handlers).
  193. root_logger.setLevel(logging.DEBUG)
  194. # Define the stream handler to output on stderr.
  195. shell_handler = StreamHandler(sys.stdout)
  196. shell_handler.setLevel(logging.INFO)
  197. shell_handler.setFormatter(CustomFormatter(has_color_support(sys.stdout)))
  198. root_logger.addHandler(shell_handler)
  199. def add_file_handler(log_dir):
  200. """Install a handler that writes in files in the given directory.
  201. log_dir (str): a path to a directory.
  202. """
  203. log_filename = time.strftime("%Y-%m-%d-%H-%M-%S.log")
  204. file_handler = FileHandler(os.path.join(log_dir, log_filename),
  205. mode='w', encoding='utf-8')
  206. file_handler.setLevel(logging.INFO)
  207. file_handler.setFormatter(CustomFormatter(False))
  208. root_logger.addHandler(file_handler)