log.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2011-2017 Guillaume Ayoub
  3. # Copyright © 2017-2019 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. Functions to set up Python's logging facility for Radicale's WSGI application.
  19. Log messages are sent to the first available target of:
  20. - Error stream specified by the WSGI server in wsgi.errors
  21. - systemd-journald
  22. - stderr
  23. The logger is thread-safe and fork-safe.
  24. """
  25. import contextlib
  26. import io
  27. import logging
  28. import multiprocessing
  29. import os
  30. import sys
  31. import tempfile
  32. import threading
  33. from radicale import pathutils
  34. try:
  35. import systemd.journal
  36. except ImportError:
  37. systemd = None
  38. LOGGER_NAME = "radicale"
  39. LOGGER_FORMAT = "[%(ident)s] %(levelname)s: %(message)s"
  40. logger = logging.getLogger(LOGGER_NAME)
  41. class RemoveTracebackFilter(logging.Filter):
  42. def filter(self, record):
  43. record.exc_info = None
  44. return True
  45. removeTracebackFilter = RemoveTracebackFilter()
  46. class IdentLogRecordFactory:
  47. """LogRecordFactory that adds ``ident`` attribute."""
  48. def __init__(self, upstream_factory):
  49. self.upstream_factory = upstream_factory
  50. self.main_pid = os.getpid()
  51. def __call__(self, *args, **kwargs):
  52. record = self.upstream_factory(*args, **kwargs)
  53. pid = os.getpid()
  54. ident = "%x" % self.main_pid
  55. if pid != self.main_pid:
  56. ident += "%+x" % (pid - self.main_pid)
  57. main_thread = threading.main_thread()
  58. current_thread = threading.current_thread()
  59. if current_thread.name and main_thread != current_thread:
  60. ident += "/%s" % current_thread.name
  61. record.ident = ident
  62. return record
  63. class RwLockWrapper():
  64. def __init__(self):
  65. self._file = tempfile.NamedTemporaryFile()
  66. self._lock = pathutils.RwLock(self._file.name)
  67. self._cm = None
  68. def acquire(self, blocking=True):
  69. assert self._cm is None
  70. if not blocking:
  71. raise NotImplementedError
  72. cm = self._lock.acquire("w")
  73. cm.__enter__()
  74. self._cm = cm
  75. def release(self):
  76. assert self._cm is not None
  77. self._cm.__exit__(None, None, None)
  78. self._cm = None
  79. class ThreadStreamsHandler(logging.Handler):
  80. terminator = "\n"
  81. def __init__(self, fallback_stream, fallback_handler):
  82. super().__init__()
  83. self._streams = {}
  84. self.fallback_stream = fallback_stream
  85. self.fallback_handler = fallback_handler
  86. def createLock(self):
  87. try:
  88. self.lock = multiprocessing.Lock()
  89. except Exception:
  90. # HACK: Workaround for Android
  91. self.lock = RwLockWrapper()
  92. def setFormatter(self, form):
  93. super().setFormatter(form)
  94. self.fallback_handler.setFormatter(form)
  95. def emit(self, record):
  96. try:
  97. stream = self._streams.get(threading.get_ident())
  98. if stream is None:
  99. self.fallback_handler.emit(record)
  100. else:
  101. msg = self.format(record)
  102. stream.write(msg)
  103. stream.write(self.terminator)
  104. if hasattr(stream, "flush"):
  105. stream.flush()
  106. except Exception:
  107. self.handleError(record)
  108. @contextlib.contextmanager
  109. def register_stream(self, stream):
  110. if stream == self.fallback_stream:
  111. yield
  112. return
  113. key = threading.get_ident()
  114. self._streams[key] = stream
  115. try:
  116. yield
  117. finally:
  118. del self._streams[key]
  119. def get_default_handler():
  120. handler = logging.StreamHandler(sys.stderr)
  121. # Detect systemd journal
  122. with contextlib.suppress(ValueError, io.UnsupportedOperation):
  123. journal_dev, journal_ino = map(
  124. int, os.environ.get("JOURNAL_STREAM", "").split(":"))
  125. st = os.fstat(sys.stderr.fileno())
  126. if (systemd and
  127. st.st_dev == journal_dev and st.st_ino == journal_ino):
  128. handler = systemd.journal.JournalHandler(
  129. SYSLOG_IDENTIFIER=LOGGER_NAME)
  130. return handler
  131. @contextlib.contextmanager
  132. def register_stream(stream):
  133. """Register global errors stream for the current thread."""
  134. yield
  135. def setup():
  136. """Set global logging up."""
  137. global register_stream
  138. handler = ThreadStreamsHandler(sys.stderr, get_default_handler())
  139. logging.basicConfig(format=LOGGER_FORMAT, handlers=[handler])
  140. register_stream = handler.register_stream
  141. log_record_factory = IdentLogRecordFactory(logging.getLogRecordFactory())
  142. logging.setLogRecordFactory(log_record_factory)
  143. set_level(logging.WARNING)
  144. def set_level(level):
  145. """Set logging level for global logger."""
  146. if isinstance(level, str):
  147. level = getattr(logging, level.upper())
  148. logger.setLevel(level)
  149. if level == logging.DEBUG:
  150. logger.removeFilter(removeTracebackFilter)
  151. else:
  152. logger.addFilter(removeTracebackFilter)