log.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. """celery.log"""
  2. import logging
  3. import threading
  4. import sys
  5. import traceback
  6. from multiprocessing import current_process
  7. from multiprocessing import util as mputil
  8. from celery import signals
  9. from celery import current_app
  10. from celery.utils import LOG_LEVELS, isatty
  11. from celery.utils.compat import LoggerAdapter
  12. from celery.utils.patch import ensure_process_aware_logger
  13. from celery.utils.term import colored
  14. class ColorFormatter(logging.Formatter):
  15. #: Loglevel -> Color mapping.
  16. COLORS = colored().names
  17. colors = {"DEBUG": COLORS["blue"], "WARNING": COLORS["yellow"],
  18. "ERROR": COLORS["red"], "CRITICAL": COLORS["magenta"]}
  19. def __init__(self, msg, use_color=True):
  20. logging.Formatter.__init__(self, msg)
  21. self.use_color = use_color
  22. def formatException(self, ei):
  23. r = logging.Formatter.formatException(self, ei)
  24. if isinstance(r, str):
  25. return r.decode("utf-8", "replace") # Convert to unicode
  26. return r
  27. def format(self, record):
  28. levelname = record.levelname
  29. color = self.colors.get(levelname)
  30. if self.use_color and color:
  31. record.msg = unicode(color(record.msg))
  32. # Very ugly, but have to make sure processName is supported
  33. # by foreign logger instances.
  34. # (processName is always supported by Python 2.7)
  35. if "processName" not in record.__dict__:
  36. record.__dict__["processName"] = current_process()._name
  37. t = logging.Formatter.format(self, record)
  38. if isinstance(t, unicode):
  39. return t.encode("utf-8", "replace")
  40. return t
  41. class Logging(object):
  42. #: The logging subsystem is only configured once per process.
  43. #: setup_logging_subsystem sets this flag, and subsequent calls
  44. #: will do nothing.
  45. _setup = False
  46. def __init__(self, app):
  47. self.app = app
  48. self.loglevel = self.app.conf.CELERYD_LOG_LEVEL
  49. self.format = self.app.conf.CELERYD_LOG_FORMAT
  50. self.colorize = self.app.conf.CELERYD_LOG_COLOR
  51. def supports_color(self, logfile=None):
  52. if self.app.IS_WINDOWS:
  53. # Windows does not support ANSI color codes.
  54. return False
  55. if self.colorize is None:
  56. # Only use color if there is no active log file
  57. # and stderr is an actual terminal.
  58. return logfile is None and isatty(sys.stderr)
  59. return self.colorize
  60. def colored(self, logfile=None):
  61. return colored(enabled=self.supports_color(logfile))
  62. def get_task_logger(self, loglevel=None, name=None):
  63. logger = logging.getLogger(name or "celery.task.default")
  64. if loglevel is not None:
  65. logger.setLevel(loglevel)
  66. return logger
  67. def setup_logging_subsystem(self, loglevel=None, logfile=None,
  68. format=None, colorize=None, **kwargs):
  69. if Logging._setup:
  70. return
  71. loglevel = loglevel or self.loglevel
  72. format = format or self.format
  73. if colorize is None:
  74. colorize = self.supports_color(logfile)
  75. try:
  76. mputil._logger = None
  77. except AttributeError:
  78. pass
  79. ensure_process_aware_logger()
  80. logging.Logger.manager.loggerDict.clear()
  81. receivers = signals.setup_logging.send(sender=None,
  82. loglevel=loglevel,
  83. logfile=logfile,
  84. format=format,
  85. colorize=colorize)
  86. if not receivers:
  87. root = logging.getLogger()
  88. if self.app.conf.CELERYD_HIJACK_ROOT_LOGGER:
  89. root.handlers = []
  90. mp = mputil.get_logger()
  91. for logger in (root, mp):
  92. self._setup_logger(logger, logfile, format, colorize, **kwargs)
  93. logger.setLevel(loglevel)
  94. Logging._setup = True
  95. return receivers
  96. def _detect_handler(self, logfile=None):
  97. """Create log handler with either a filename, an open stream
  98. or :const:`None` (stderr)."""
  99. if logfile is None:
  100. logfile = sys.__stderr__
  101. if hasattr(logfile, "write"):
  102. return logging.StreamHandler(logfile)
  103. return logging.FileHandler(logfile)
  104. def get_default_logger(self, loglevel=None, name="celery"):
  105. """Get default logger instance.
  106. :keyword loglevel: Initial log level.
  107. """
  108. logger = logging.getLogger(name)
  109. if loglevel is not None:
  110. logger.setLevel(loglevel)
  111. return logger
  112. def setup_logger(self, loglevel=None, logfile=None,
  113. format=None, colorize=None, name="celery", root=True,
  114. app=None, **kwargs):
  115. """Setup the :mod:`multiprocessing` logger.
  116. If `logfile` is not specified, then `sys.stderr` is used.
  117. Returns logger object.
  118. """
  119. loglevel = loglevel or self.loglevel
  120. format = format or self.format
  121. if colorize is None:
  122. colorize = self.supports_color(logfile)
  123. if not root or self.app.conf.CELERYD_HIJACK_ROOT_LOGGER:
  124. return self._setup_logger(self.get_default_logger(loglevel, name),
  125. logfile, format, colorize, **kwargs)
  126. self.setup_logging_subsystem(loglevel, logfile,
  127. format, colorize, **kwargs)
  128. return self.get_default_logger(name=name)
  129. def setup_task_logger(self, loglevel=None, logfile=None, format=None,
  130. colorize=None, task_kwargs=None, propagate=False, app=None,
  131. **kwargs):
  132. """Setup the task logger.
  133. If `logfile` is not specified, then `sys.stderr` is used.
  134. Returns logger object.
  135. """
  136. loglevel = loglevel or self.loglevel
  137. format = format or self.format
  138. if colorize is None:
  139. colorize = self.supports_color(logfile)
  140. if task_kwargs is None:
  141. task_kwargs = {}
  142. task_kwargs.setdefault("task_id", "-?-")
  143. task_name = task_kwargs.get("task_name")
  144. task_kwargs.setdefault("task_name", "-?-")
  145. logger = self._setup_logger(self.get_task_logger(loglevel, task_name),
  146. logfile, format, colorize, **kwargs)
  147. logger.propagate = int(propagate) # this is an int for some reason.
  148. # better to not question why.
  149. return LoggerAdapter(logger, task_kwargs)
  150. def redirect_stdouts_to_logger(self, logger, loglevel=None):
  151. """Redirect :class:`sys.stdout` and :class:`sys.stderr` to a
  152. logging instance.
  153. :param logger: The :class:`logging.Logger` instance to redirect to.
  154. :param loglevel: The loglevel redirected messages will be logged as.
  155. """
  156. proxy = LoggingProxy(logger, loglevel)
  157. sys.stdout = sys.stderr = proxy
  158. return proxy
  159. def _setup_logger(self, logger, logfile, format, colorize,
  160. formatter=ColorFormatter, **kwargs):
  161. if logger.handlers: # Logger already configured
  162. return logger
  163. handler = self._detect_handler(logfile)
  164. handler.setFormatter(formatter(format, use_color=colorize))
  165. logger.addHandler(handler)
  166. return logger
  167. setup_logging_subsystem = current_app.log.setup_logging_subsystem
  168. get_default_logger = current_app.log.get_default_logger
  169. setup_logger = current_app.log.setup_logger
  170. setup_task_logger = current_app.log.setup_task_logger
  171. get_task_logger = current_app.log.get_task_logger
  172. redirect_stdouts_to_logger = current_app.log.redirect_stdouts_to_logger
  173. class LoggingProxy(object):
  174. """Forward file object to :class:`logging.Logger` instance.
  175. :param logger: The :class:`logging.Logger` instance to forward to.
  176. :param loglevel: Loglevel to use when writing messages.
  177. """
  178. mode = "w"
  179. name = None
  180. closed = False
  181. loglevel = logging.ERROR
  182. _thread = threading.local()
  183. def __init__(self, logger, loglevel=None):
  184. self.logger = logger
  185. self.loglevel = loglevel or self.logger.level or self.loglevel
  186. if not isinstance(self.loglevel, int):
  187. self.loglevel = LOG_LEVELS[self.loglevel.upper()]
  188. self._safewrap_handlers()
  189. def _safewrap_handlers(self):
  190. """Make the logger handlers dump internal errors to
  191. `sys.__stderr__` instead of `sys.stderr` to circumvent
  192. infinite loops."""
  193. def wrap_handler(handler): # pragma: no cover
  194. class WithSafeHandleError(logging.Handler):
  195. def handleError(self, record):
  196. exc_info = sys.exc_info()
  197. try:
  198. try:
  199. traceback.print_exception(exc_info[0],
  200. exc_info[1],
  201. exc_info[2],
  202. None, sys.__stderr__)
  203. except IOError:
  204. pass # see python issue 5971
  205. finally:
  206. del(exc_info)
  207. handler.handleError = WithSafeHandleError().handleError
  208. return map(wrap_handler, self.logger.handlers)
  209. def write(self, data):
  210. if getattr(self._thread, "recurse_protection", False):
  211. # Logger is logging back to this file, so stop recursing.
  212. return
  213. """Write message to logging object."""
  214. data = data.strip()
  215. if data and not self.closed:
  216. self._thread.recurse_protection = True
  217. try:
  218. self.logger.log(self.loglevel, data)
  219. finally:
  220. self._thread.recurse_protection = False
  221. def writelines(self, sequence):
  222. """`writelines(sequence_of_strings) -> None`.
  223. Write the strings to the file.
  224. The sequence can be any iterable object producing strings.
  225. This is equivalent to calling :meth:`write` for each string.
  226. """
  227. for part in sequence:
  228. self.write(part)
  229. def flush(self):
  230. """This object is not buffered so any :meth:`flush` requests
  231. are ignored."""
  232. pass
  233. def close(self):
  234. """When the object is closed, no write requests are forwarded to
  235. the logging object anymore."""
  236. self.closed = True
  237. def isatty(self):
  238. """Always returns :const:`False`. Just here for file support."""
  239. return False
  240. def fileno(self):
  241. return None
  242. class SilenceRepeated(object):
  243. """Only log action every n iterations."""
  244. def __init__(self, action, max_iterations=10):
  245. self.action = action
  246. self.max_iterations = max_iterations
  247. self._iterations = 0
  248. def __call__(self, *msgs):
  249. if self._iterations >= self.max_iterations:
  250. for msg in msgs:
  251. self.action(msg)
  252. self._iterations = 0
  253. else:
  254. self._iterations += 1