log.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. #!/usr/bin/env python
  2. # $Id: log.py 1171 2013-02-19 10:13:09Z g.rodola $
  3. # ======================================================================
  4. # Copyright (C) 2007-2013 Giampaolo Rodola' <g.rodola@gmail.com>
  5. #
  6. # All Rights Reserved
  7. #
  8. # Permission is hereby granted, free of charge, to any person
  9. # obtaining a copy of this software and associated documentation
  10. # files (the "Software"), to deal in the Software without
  11. # restriction, including without limitation the rights to use,
  12. # copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. # copies of the Software, and to permit persons to whom the
  14. # Software is furnished to do so, subject to the following
  15. # conditions:
  16. #
  17. # The above copyright notice and this permission notice shall be
  18. # included in all copies or substantial portions of the Software.
  19. #
  20. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  21. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  22. # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  23. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  24. # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  25. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  26. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  27. # OTHER DEALINGS IN THE SOFTWARE.
  28. #
  29. """
  30. Logging support for pyftpdlib, inspired from Tornado's
  31. (http://www.tornadoweb.org/).
  32. This is not supposed to be imported/used directly.
  33. Instead you should use logging.basicConfig before serve_forever().
  34. """
  35. import logging
  36. import sys
  37. import time
  38. try:
  39. import curses
  40. except ImportError:
  41. curses = None
  42. from pyftpdlib._compat import unicode
  43. # default logger
  44. logger = logging.getLogger('pyftpdlib')
  45. def _stderr_supports_color():
  46. color = False
  47. if curses is not None and sys.stderr.isatty():
  48. try:
  49. curses.setupterm()
  50. if curses.tigetnum("colors") > 0:
  51. color = True
  52. except Exception:
  53. pass
  54. return color
  55. # configurable options
  56. LEVEL = logging.INFO
  57. PREFIX = '[%(levelname)1.1s %(asctime)s]'
  58. COLOURED = _stderr_supports_color()
  59. TIME_FORMAT = "%y-%m-%d %H:%M:%S"
  60. # taken and adapted from Tornado
  61. class LogFormatter(logging.Formatter):
  62. """Log formatter used in pyftpdlib.
  63. Key features of this formatter are:
  64. * Color support when logging to a terminal that supports it.
  65. * Timestamps on every log line.
  66. * Robust against str/bytes encoding problems.
  67. """
  68. def __init__(self, *args, **kwargs):
  69. logging.Formatter.__init__(self, *args, **kwargs)
  70. self._coloured = COLOURED and _stderr_supports_color()
  71. if self._coloured:
  72. curses.setupterm()
  73. # The curses module has some str/bytes confusion in
  74. # python3. Until version 3.2.3, most methods return
  75. # bytes, but only accept strings. In addition, we want to
  76. # output these strings with the logging module, which
  77. # works with unicode strings. The explicit calls to
  78. # unicode() below are harmless in python2 but will do the
  79. # right conversion in python 3.
  80. fg_color = (curses.tigetstr("setaf") or curses.tigetstr("setf") or "")
  81. if (3, 0) < sys.version_info < (3, 2, 3):
  82. fg_color = unicode(fg_color, "ascii")
  83. self._colors = {
  84. logging.DEBUG: unicode(curses.tparm(fg_color, 4), "ascii"), # blue
  85. logging.INFO: unicode(curses.tparm(fg_color, 2), "ascii"), # green
  86. logging.WARNING: unicode(curses.tparm(fg_color, 3), "ascii"), # yellow
  87. logging.ERROR: unicode(curses.tparm(fg_color, 1), "ascii") # red
  88. }
  89. self._normal = unicode(curses.tigetstr("sgr0"), "ascii")
  90. def format(self, record):
  91. try:
  92. record.message = record.getMessage()
  93. except Exception:
  94. err = sys.exc_info()[1]
  95. record.message = "Bad message (%r): %r" % (err, record.__dict__)
  96. record.asctime = time.strftime(TIME_FORMAT,
  97. self.converter(record.created))
  98. prefix = PREFIX % record.__dict__
  99. if self._coloured:
  100. prefix = (self._colors.get(record.levelno, self._normal) +
  101. prefix + self._normal)
  102. # Encoding notes: The logging module prefers to work with character
  103. # strings, but only enforces that log messages are instances of
  104. # basestring. In python 2, non-ascii bytestrings will make
  105. # their way through the logging framework until they blow up with
  106. # an unhelpful decoding error (with this formatter it happens
  107. # when we attach the prefix, but there are other opportunities for
  108. # exceptions further along in the framework).
  109. #
  110. # If a byte string makes it this far, convert it to unicode to
  111. # ensure it will make it out to the logs. Use repr() as a fallback
  112. # to ensure that all byte strings can be converted successfully,
  113. # but don't do it by default so we don't add extra quotes to ascii
  114. # bytestrings. This is a bit of a hacky place to do this, but
  115. # it's worth it since the encoding errors that would otherwise
  116. # result are so useless (and tornado is fond of using utf8-encoded
  117. # byte strings wherever possible).
  118. try:
  119. message = unicode(record.message)
  120. except UnicodeDecodeError:
  121. message = repr(record.message)
  122. formatted = prefix + " " + message
  123. if record.exc_info:
  124. if not record.exc_text:
  125. record.exc_text = self.formatException(record.exc_info)
  126. if record.exc_text:
  127. formatted = formatted.rstrip() + "\n" + record.exc_text
  128. return formatted.replace("\n", "\n ")
  129. def _config_logging():
  130. channel = logging.StreamHandler()
  131. channel.setFormatter(LogFormatter())
  132. logger = logging.getLogger()
  133. logger.setLevel(LEVEL)
  134. logger.addHandler(channel)