log.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. # This file is part of Radicale - CalDAV and CardDAV 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. - ``sys.stderr``
  22. """
  23. import contextlib
  24. import logging
  25. import os
  26. import socket
  27. import struct
  28. import sys
  29. import threading
  30. import time
  31. from typing import (Any, Callable, ClassVar, Dict, Iterator, Optional, Tuple,
  32. Union)
  33. from radicale import types
  34. LOGGER_NAME: str = "radicale"
  35. LOGGER_FORMAT: str = "[%(asctime)s] [%(ident)s] [%(levelname)s] %(message)s"
  36. DATE_FORMAT: str = "%Y-%m-%d %H:%M:%S %z"
  37. logger: logging.Logger = logging.getLogger(LOGGER_NAME)
  38. class RemoveTracebackFilter(logging.Filter):
  39. def filter(self, record: logging.LogRecord) -> bool:
  40. record.exc_info = None
  41. return True
  42. REMOVE_TRACEBACK_FILTER: logging.Filter = RemoveTracebackFilter()
  43. class IdentLogRecordFactory:
  44. """LogRecordFactory that adds ``ident`` attribute."""
  45. def __init__(self, upstream_factory: Callable[..., logging.LogRecord]
  46. ) -> None:
  47. self._upstream_factory = upstream_factory
  48. def __call__(self, *args: Any, **kwargs: Any) -> logging.LogRecord:
  49. record = self._upstream_factory(*args, **kwargs)
  50. ident = "%d" % os.getpid()
  51. main_thread = threading.main_thread()
  52. current_thread = threading.current_thread()
  53. if current_thread.name and main_thread != current_thread:
  54. ident += "/%s" % current_thread.name
  55. record.ident = ident # type:ignore[attr-defined]
  56. record.tid = None # type:ignore[attr-defined]
  57. if sys.version_info >= (3, 8):
  58. record.tid = current_thread.native_id
  59. return record
  60. class ThreadedStreamHandler(logging.Handler):
  61. """Sends logging output to the stream registered for the current thread or
  62. ``sys.stderr`` when no stream was registered."""
  63. terminator: ClassVar[str] = "\n"
  64. _streams: Dict[int, types.ErrorStream]
  65. _journal_stream_id: Optional[Tuple[int, int]]
  66. _journal_socket: Optional[socket.socket]
  67. def __init__(self) -> None:
  68. super().__init__()
  69. self._streams = {}
  70. self._journal_stream_id = None
  71. with contextlib.suppress(TypeError, ValueError):
  72. dev, inode = os.environ.get("JOURNAL_STREAM", "").split(":", 1)
  73. self._journal_stream_id = int(dev), int(inode)
  74. self._journal_socket = None
  75. if self._journal_stream_id and hasattr(socket, "AF_UNIX"):
  76. journal_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
  77. try:
  78. journal_socket.connect("/run/systemd/journal/socket")
  79. except OSError:
  80. journal_socket.close()
  81. else:
  82. self._journal_socket = journal_socket
  83. def _detect_journal(self, stream):
  84. if not self._journal_stream_id:
  85. return False
  86. try:
  87. stat = os.fstat(stream.fileno())
  88. except Exception:
  89. return False
  90. return self._journal_stream_id == (stat.st_dev, stat.st_ino)
  91. @staticmethod
  92. def _encode_journal(data):
  93. msg = b""
  94. for key, value in data.items():
  95. if key is None:
  96. continue
  97. key = key.encode()
  98. value = str(value).encode()
  99. if b"\n" in value:
  100. msg += (key + b"\n" +
  101. struct.pack("<Q", len(value)) + value + b"\n")
  102. else:
  103. msg += key + b"=" + value + b"\n"
  104. return msg
  105. def _emit_journal(self, record):
  106. priority = {"DEBUG": 7,
  107. "INFO": 6,
  108. "WARNING": 4,
  109. "ERROR": 3,
  110. "CRITICAL": 2}.get(record.levelname, 4)
  111. timestamp = time.strftime("%Y-%m-%dT%H:%M:%S.%%03dZ",
  112. time.gmtime(record.created)) % record.msecs
  113. data = {"PRIORITY": priority,
  114. "TID": record.tid,
  115. "SYSLOG_IDENTIFIER": record.name,
  116. "SYSLOG_FACILITY": 1,
  117. "SYSLOG_PID": record.process,
  118. "SYSLOG_TIMESTAMP": timestamp,
  119. "CODE_FILE": record.pathname,
  120. "CODE_LINE": record.lineno,
  121. "CODE_FUNC": record.funcName,
  122. "MESSAGE": self.format(record)}
  123. self._journal_socket.sendall(self._encode_journal(data))
  124. def emit(self, record: logging.LogRecord) -> None:
  125. try:
  126. stream = self._streams.get(threading.get_ident(), sys.stderr)
  127. if self._journal_socket and self._detect_journal(stream):
  128. self._emit_journal(record)
  129. return
  130. msg = self.format(record)
  131. stream.write(msg)
  132. stream.write(self.terminator)
  133. if hasattr(stream, "flush"):
  134. stream.flush()
  135. except Exception:
  136. self.handleError(record)
  137. @types.contextmanager
  138. def register_stream(self, stream: types.ErrorStream) -> Iterator[None]:
  139. """Register stream for logging output of the current thread."""
  140. key = threading.get_ident()
  141. self._streams[key] = stream
  142. try:
  143. yield
  144. finally:
  145. del self._streams[key]
  146. @types.contextmanager
  147. def register_stream(stream: types.ErrorStream) -> Iterator[None]:
  148. """Register stream for logging output of the current thread."""
  149. yield
  150. def setup() -> None:
  151. """Set global logging up."""
  152. global register_stream
  153. handler = ThreadedStreamHandler()
  154. logging.basicConfig(format=LOGGER_FORMAT, datefmt=DATE_FORMAT,
  155. handlers=[handler])
  156. register_stream = handler.register_stream
  157. log_record_factory = IdentLogRecordFactory(logging.getLogRecordFactory())
  158. logging.setLogRecordFactory(log_record_factory)
  159. set_level(logging.WARNING)
  160. def set_level(level: Union[int, str]) -> None:
  161. """Set logging level for global logger."""
  162. if isinstance(level, str):
  163. level = getattr(logging, level.upper())
  164. assert isinstance(level, int)
  165. logger.setLevel(level)
  166. logger.removeFilter(REMOVE_TRACEBACK_FILTER)
  167. if level > logging.DEBUG:
  168. logger.addFilter(REMOVE_TRACEBACK_FILTER)