debug.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. # Copyright (c) "Neo4j"
  2. # Neo4j Sweden AB [https://neo4j.com]
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # https://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from __future__ import annotations
  16. import asyncio
  17. import typing as t
  18. from contextlib import suppress as _suppress
  19. from logging import (
  20. CRITICAL,
  21. DEBUG,
  22. ERROR,
  23. Filter,
  24. Formatter,
  25. getLogger,
  26. INFO,
  27. StreamHandler,
  28. WARNING,
  29. )
  30. from sys import stderr
  31. __all__ = [
  32. "Watcher",
  33. "watch",
  34. ]
  35. class ColourFormatter(Formatter):
  36. """Colour formatter for pretty log output."""
  37. def format(self, record):
  38. s = super().format(record)
  39. if record.levelno == CRITICAL:
  40. return f"\x1b[31;1m{s}\x1b[0m" # bright red
  41. elif record.levelno == ERROR:
  42. return f"\x1b[33;1m{s}\x1b[0m" # bright yellow
  43. elif record.levelno == WARNING:
  44. return f"\x1b[33m{s}\x1b[0m" # yellow
  45. elif record.levelno == INFO:
  46. return f"\x1b[37m{s}\x1b[0m" # white
  47. elif record.levelno == DEBUG:
  48. return f"\x1b[36m{s}\x1b[0m" # cyan
  49. else:
  50. return s
  51. class TaskIdFilter(Filter):
  52. """Injecting async task id into log records."""
  53. def filter(self, record):
  54. try:
  55. record.task = id(asyncio.current_task())
  56. except RuntimeError:
  57. record.task = None
  58. return True
  59. class Watcher:
  60. """
  61. Log watcher for easier logging setup.
  62. Example::
  63. from neo4j.debug import Watcher
  64. with Watcher("neo4j"):
  65. # DEBUG logging to stderr enabled within this context
  66. ... # do something
  67. .. note:: The Watcher class is not thread-safe. Having Watchers in multiple
  68. threads can lead to duplicate log messages as the context manager will
  69. enable logging for all threads.
  70. .. note::
  71. The exact logging format and messages are not part of the API contract
  72. and might change at any time without notice. They are meant for
  73. debugging purposes and human consumption only.
  74. :param logger_names: Names of loggers to watch.
  75. :param default_level: Default minimum log level to show.
  76. The level can be overridden by setting ``level`` when calling
  77. :meth:`.watch`.
  78. :param default_out: Default output stream for all loggers.
  79. The level can be overridden by setting ``out`` when calling
  80. :meth:`.watch`.
  81. :type default_out: stream or file-like object
  82. :param colour: Whether the log levels should be indicated with ANSI colour
  83. codes.
  84. :param thread_info: whether to include information about the current
  85. thread in the log message. Defaults to :data:`True`.
  86. :param task_info: whether to include information about the current
  87. async task in the log message. Defaults to :data:`True`.
  88. .. versionchanged:: 5.3
  89. * Added ``thread_info`` and ``task_info`` parameters.
  90. * Logging format around thread and task information changed.
  91. """
  92. def __init__(
  93. self,
  94. *logger_names: str | None,
  95. default_level: int = DEBUG,
  96. default_out: t.TextIO = stderr,
  97. colour: bool = False,
  98. thread_info: bool = True,
  99. task_info: bool = True,
  100. ) -> None:
  101. super().__init__()
  102. self.logger_names = logger_names
  103. self._loggers = [getLogger(name) for name in self.logger_names]
  104. self.default_level = default_level
  105. self.default_out = default_out
  106. self._handlers: dict[str, StreamHandler] = {}
  107. self._task_info = task_info
  108. format_ = "%(asctime)s %(message)s"
  109. if task_info:
  110. format_ = "[Task %(task)-15s] " + format_
  111. if thread_info:
  112. format_ = "[Thread %(thread)d] " + format_
  113. if not colour:
  114. format_ = "[%(levelname)-8s] " + format_
  115. formatter_cls = ColourFormatter if colour else Formatter
  116. self.formatter = formatter_cls(format_)
  117. def __enter__(self) -> Watcher:
  118. """Enable logging for all loggers."""
  119. self.watch()
  120. return self
  121. def __exit__(self, exc_type, exc_val, exc_tb):
  122. """Disable logging for all loggers."""
  123. self.stop()
  124. def watch(
  125. self, level: int | None = None, out: t.TextIO | None = None
  126. ) -> None:
  127. """
  128. Enable logging for all loggers.
  129. :param level: Minimum log level to show.
  130. If :data:`None`, the ``default_level`` is used.
  131. :param out: Output stream for all loggers.
  132. If :data:`None`, the ``default_out`` is used.
  133. :type out: stream or file-like object
  134. """
  135. if level is None:
  136. level = self.default_level
  137. if out is None:
  138. out = self.default_out
  139. self.stop()
  140. handler = StreamHandler(out)
  141. handler.setFormatter(self.formatter)
  142. handler.setLevel(level)
  143. if self._task_info:
  144. handler.addFilter(TaskIdFilter())
  145. for logger in self._loggers:
  146. self._handlers[logger.name] = handler
  147. logger.addHandler(handler)
  148. if logger.getEffectiveLevel() > level:
  149. logger.setLevel(level)
  150. def stop(self) -> None:
  151. """Disable logging for all loggers."""
  152. for logger in self._loggers:
  153. with _suppress(KeyError):
  154. logger.removeHandler(self._handlers.pop(logger.name))
  155. def watch(
  156. *logger_names: str | None,
  157. level: int = DEBUG,
  158. out: t.TextIO = stderr,
  159. colour: bool = False,
  160. thread_info: bool = True,
  161. task_info: bool = True,
  162. ) -> Watcher:
  163. """
  164. Quick wrapper for using :class:`.Watcher`.
  165. Create a Watcher with the given configuration, enable watching and return
  166. it.
  167. Example::
  168. from neo4j.debug import watch
  169. watch("neo4j")
  170. # from now on, DEBUG logging to stderr is enabled in the driver
  171. .. note::
  172. The exact logging format and messages are not part of the API contract
  173. and might change at any time without notice. They are meant for
  174. debugging purposes and human consumption only.
  175. :param logger_names: Names of loggers to watch.
  176. :param level: see ``default_level`` of :class:`.Watcher`.
  177. :param out: see ``default_out`` of :class:`.Watcher`.
  178. :type out: stream or file-like object
  179. :param colour: see ``colour`` of :class:`.Watcher`.
  180. :param thread_info: see ``thread_info`` of :class:`.Watcher`.
  181. :param task_info: see ``task_info`` of :class:`.Watcher`.
  182. :returns: Watcher instance
  183. :rtype: :class:`.Watcher`
  184. .. versionchanged:: 5.3
  185. * Added ``thread_info`` and ``task_info`` parameters.
  186. * Logging format around thread and task information changed.
  187. """
  188. watcher = Watcher(
  189. *logger_names,
  190. default_level=level,
  191. default_out=out,
  192. colour=colour,
  193. thread_info=thread_info,
  194. task_info=task_info,
  195. )
  196. watcher.watch()
  197. return watcher