log.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2011-2017 Guillaume Ayoub
  3. # Copyright © 2017-2018 Unrud<unrud@outlook.com>
  4. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library 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 General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. Radicale logging module.
  19. Manage logging from a configuration file. For more information, see:
  20. http://docs.python.org/library/logging.config.html
  21. """
  22. import contextlib
  23. import io
  24. import logging
  25. import multiprocessing
  26. import os
  27. import sys
  28. import tempfile
  29. import threading
  30. from radicale import pathutils
  31. try:
  32. import systemd.journal
  33. except ImportError:
  34. systemd = None
  35. LOGGER_NAME = "radicale"
  36. LOGGER_FORMAT = "[%(ident)s] %(levelname)s: %(message)s"
  37. logger = logging.getLogger(LOGGER_NAME)
  38. class RemoveTracebackFilter(logging.Filter):
  39. def filter(self, record):
  40. record.exc_info = None
  41. return True
  42. removeTracebackFilter = RemoveTracebackFilter()
  43. class IdentLogRecordFactory:
  44. """LogRecordFactory that adds ``ident`` attribute."""
  45. def __init__(self, upstream_factory):
  46. self.upstream_factory = upstream_factory
  47. self.main_pid = os.getpid()
  48. def __call__(self, *args, **kwargs):
  49. record = self.upstream_factory(*args, **kwargs)
  50. pid = os.getpid()
  51. ident = "%x" % self.main_pid
  52. if pid != self.main_pid:
  53. ident += "%+x" % (pid - self.main_pid)
  54. main_thread = threading.main_thread()
  55. current_thread = threading.current_thread()
  56. if current_thread.name and main_thread != current_thread:
  57. ident += "/%s" % current_thread.name
  58. record.ident = ident
  59. return record
  60. class RwLockWrapper():
  61. def __init__(self):
  62. self._file = tempfile.NamedTemporaryFile()
  63. self._lock = pathutils.RwLock(self._file.name)
  64. self._cm = None
  65. def acquire(self, blocking=True):
  66. assert self._cm is None
  67. if not blocking:
  68. raise NotImplementedError
  69. cm = self._lock.acquire("w")
  70. cm.__enter__()
  71. self._cm = cm
  72. def release(self):
  73. assert self._cm is not None
  74. self._cm.__exit__(None, None, None)
  75. self._cm = None
  76. class ThreadStreamsHandler(logging.Handler):
  77. terminator = "\n"
  78. def __init__(self, fallback_stream, fallback_handler):
  79. super().__init__()
  80. self._streams = {}
  81. self.fallback_stream = fallback_stream
  82. self.fallback_handler = fallback_handler
  83. def createLock(self):
  84. try:
  85. self.lock = multiprocessing.Lock()
  86. except Exception:
  87. # HACK: Workaround for Android
  88. self.lock = RwLockWrapper()
  89. def setFormatter(self, form):
  90. super().setFormatter(form)
  91. self.fallback_handler.setFormatter(form)
  92. def emit(self, record):
  93. try:
  94. stream = self._streams.get(threading.get_ident())
  95. if stream is None:
  96. self.fallback_handler.emit(record)
  97. else:
  98. msg = self.format(record)
  99. stream.write(msg)
  100. stream.write(self.terminator)
  101. if hasattr(stream, "flush"):
  102. stream.flush()
  103. except Exception:
  104. self.handleError(record)
  105. @contextlib.contextmanager
  106. def register_stream(self, stream):
  107. if stream == self.fallback_stream:
  108. yield
  109. return
  110. key = threading.get_ident()
  111. self._streams[key] = stream
  112. try:
  113. yield
  114. finally:
  115. del self._streams[key]
  116. def get_default_handler():
  117. handler = logging.StreamHandler(sys.stderr)
  118. # Detect systemd journal
  119. with contextlib.suppress(ValueError, io.UnsupportedOperation):
  120. journal_dev, journal_ino = map(
  121. int, os.environ.get("JOURNAL_STREAM", "").split(":"))
  122. st = os.fstat(sys.stderr.fileno())
  123. if (systemd and
  124. st.st_dev == journal_dev and st.st_ino == journal_ino):
  125. handler = systemd.journal.JournalHandler(
  126. SYSLOG_IDENTIFIER=LOGGER_NAME)
  127. return handler
  128. @contextlib.contextmanager
  129. def register_stream(stream):
  130. """Register global errors stream for the current thread."""
  131. yield
  132. def setup():
  133. """Set global logging up."""
  134. global register_stream
  135. handler = ThreadStreamsHandler(sys.stderr, get_default_handler())
  136. logging.basicConfig(format=LOGGER_FORMAT, handlers=[handler])
  137. register_stream = handler.register_stream
  138. log_record_factory = IdentLogRecordFactory(logging.getLogRecordFactory())
  139. logging.setLogRecordFactory(log_record_factory)
  140. set_level(logging.DEBUG)
  141. def set_level(level):
  142. """Set logging level for global logger."""
  143. if isinstance(level, str):
  144. level = getattr(logging, level.upper())
  145. logger.setLevel(level)
  146. if level == logging.DEBUG:
  147. logger.removeFilter(removeTracebackFilter)
  148. else:
  149. logger.addFilter(removeTracebackFilter)