logger.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. # logger.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
  2. # This copyright was auto-generated on Thu Aug 24 17:08:14 UTC 2023
  3. """Logger abstraction above default python logging"""
  4. from __future__ import annotations
  5. import http.client as http_client
  6. import logging
  7. from pathlib import Path
  8. from typing import Any, Final
  9. from rich import traceback
  10. from rich.logging import RichHandler
  11. class Logger:
  12. """A singleton class to manage logging for the Open GoPro internal modules
  13. Attributes:
  14. ARROW_HEAD_COUNT (Final[int]): Length of leading arrow
  15. ARROW_TAIL_COUNT (Final[int]): Length of trailing arrow
  16. Args:
  17. logger (logging.Logger): input logger that will be modified and then returned
  18. output (Path | None): Path of log file for file stream handler. If not set, will not log to file.
  19. modules (dict[str, int] | None): Optional override of modules / levels. Will be merged into default modules.
  20. """
  21. _instances: dict[type[Logger], Logger] = {}
  22. ARROW_HEAD_COUNT: Final[int] = 8
  23. ARROW_TAIL_COUNT: Final[int] = 14
  24. def __new__(cls, *_: Any) -> Any: # noqa https://github.com/PyCQA/pydocstyle/issues/515
  25. if cls not in cls._instances:
  26. c = object.__new__(cls)
  27. cls._instances[cls] = c
  28. return c
  29. raise RuntimeError("The logger can only be setup once and this should be done at the top level.")
  30. def __init__(
  31. self,
  32. logger: logging.Logger,
  33. output: Path | None = None,
  34. modules: dict[str, int] | None = None,
  35. ) -> None:
  36. self.modules: dict[str, int] = {
  37. "open_gopro.gopro_base": logging.DEBUG,
  38. "open_gopro.gopro_wired": logging.DEBUG,
  39. "open_gopro.gopro_wireless": logging.DEBUG, # TRACE for concurrency debugging
  40. "open_gopro.api.builders": logging.DEBUG,
  41. "open_gopro.api.http_commands": logging.DEBUG,
  42. "open_gopro.api.ble_commands": logging.DEBUG,
  43. "open_gopro.domain.communicator_interface": logging.DEBUG,
  44. "open_gopro.network.ble.adapters.bleak_wrapper": logging.DEBUG,
  45. "open_gopro.network.ble.client": logging.DEBUG,
  46. "open_gopro.parsers.bytes": logging.DEBUG,
  47. "open_gopro.parsers.json": logging.DEBUG,
  48. "open_gopro.parsers.response": logging.DEBUG,
  49. "open_gopro.parsers.general": logging.DEBUG,
  50. "open_gopro.network.wifi.adapters.wireless": logging.DEBUG,
  51. "open_gopro.network.wifi.mdns_scanner": logging.DEBUG,
  52. "open_gopro.domain.observable": logging.DEBUG, # TRACE for concurrency debugging
  53. "open_gopro.domain.gopro_observable": logging.DEBUG, # TRACE for observable debugging
  54. "open_gopro.models.response": logging.DEBUG,
  55. "open_gopro.models.network_scan_response": logging.DEBUG,
  56. "open_gopro.features.cohn_feature": logging.DEBUG,
  57. "open_gopro.features.access_point_feature": logging.DEBUG,
  58. "open_gopro.features.streaming.stream_feature": logging.DEBUG,
  59. "open_gopro.features.streaming.webcam_stream": logging.DEBUG,
  60. "open_gopro.features.streaming.livestream": logging.DEBUG,
  61. "open_gopro.features.streaming.preview_stream": logging.DEBUG,
  62. "open_gopro.util.util": logging.DEBUG,
  63. "open_gopro.demos.gui.video_display": logging.DEBUG,
  64. "open_gopro.database.db": logging.DEBUG,
  65. "bleak": logging.DEBUG,
  66. "urllib3": logging.DEBUG,
  67. "http.client": logging.DEBUG,
  68. }
  69. self.logger = logger
  70. self.modules = modules or self.modules
  71. self.handlers: list[logging.Handler] = []
  72. # monkey-patch a `print` global into the http.client module; all calls to
  73. # print() in that module will then use our logger's debug method
  74. http_client.HTTPConnection.debuglevel = 1
  75. http_client.print = lambda *args: logging.getLogger("http.client").debug(" ".join(args)) # type: ignore
  76. self.file_handler: logging.Handler | None
  77. if output:
  78. # Logging to file with millisecond timing
  79. self.file_handler = logging.FileHandler(output, mode="w")
  80. file_formatter = logging.Formatter(
  81. fmt="%(threadName)13s:%(asctime)s.%(msecs)03d %(filename)-40s %(lineno)4s %(levelname)-8s | %(message)s",
  82. datefmt="%H:%M:%S",
  83. )
  84. self.file_handler.setFormatter(file_formatter)
  85. self.file_handler.setLevel(logging.TRACE) # type: ignore
  86. logger.addHandler(self.file_handler)
  87. self.addLoggingHandler(self.file_handler)
  88. else:
  89. self.file_handler = None
  90. # Use Rich for colorful console logging
  91. self.stream_handler = RichHandler(rich_tracebacks=True, enable_link_path=True, show_time=False)
  92. stream_formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(message)s", datefmt="%H:%M:%S")
  93. self.stream_handler.setFormatter(stream_formatter)
  94. self.stream_handler.setLevel(logging.INFO)
  95. logger.addHandler(self.stream_handler)
  96. self.addLoggingHandler(self.stream_handler)
  97. self.addLoggingLevel("TRACE", logging.DEBUG - 5)
  98. logger.setLevel(logging.TRACE) # type: ignore # pylint: disable=no-member
  99. traceback.install() # Enable exception tracebacks in rich logger
  100. @classmethod
  101. def get_instance(cls) -> Logger:
  102. """Get the singleton instance
  103. Raises:
  104. RuntimeError: Has not yet been instantiated
  105. Returns:
  106. Logger: singleton instance
  107. """
  108. if not (logger := cls._instances.get(Logger, None)):
  109. raise RuntimeError("Logging must first be setup")
  110. return logger
  111. def addLoggingHandler(self, handler: logging.Handler) -> None:
  112. """Add a handler for all of the internal GoPro modules
  113. Args:
  114. handler (logging.Handler): handler to add
  115. """
  116. self.logger.addHandler(handler)
  117. self.handlers.append(handler)
  118. # Enable / disable logging in modules
  119. for module, level in self.modules.items():
  120. l = logging.getLogger(module)
  121. l.setLevel(level)
  122. l.addHandler(handler)
  123. # From https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
  124. @staticmethod
  125. def addLoggingLevel(levelName: str, levelNum: int) -> None:
  126. """Comprehensively adds a new logging level to the `logging` module and the currently configured logging class.
  127. `levelName` becomes an attribute of the `logging` module with the value
  128. `levelNum`. `methodName` becomes a convenience method for both `logging`
  129. itself and the class returned by `logging.getLoggerClass()` (usually just
  130. `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
  131. used.
  132. To avoid accidental clobberings of existing attributes, this method will
  133. raise an `AttributeError` if the level name is already an attribute of the
  134. `logging` module or if the method name is already present
  135. Example:
  136. --------
  137. >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
  138. >>> logging.getLogger(__name__).setLevel("TRACE")
  139. >>> logging.getLogger(__name__).trace('that worked')
  140. >>> logging.trace('so did this')
  141. >>> logging.TRACE
  142. 5
  143. Args:
  144. levelName (str): name of level (i.e. TRACE)
  145. levelNum (int): integer level of new logging level
  146. """
  147. methodName = levelName.lower()
  148. def logForLevel(self: Any, message: str, *args: Any, **kwargs: Any) -> None:
  149. if self.isEnabledFor(levelNum):
  150. self._log(levelNum, message, args, **kwargs)
  151. def logToRoot(message: str, *args: Any, **kwargs: Any) -> None:
  152. logging.log(levelNum, message, *args, **kwargs)
  153. logging.addLevelName(levelNum, levelName)
  154. setattr(logging, levelName, levelNum)
  155. setattr(logging.getLoggerClass(), methodName, logForLevel)
  156. setattr(logging, methodName, logToRoot)
  157. @staticmethod
  158. def build_log_tx_str(stringable: Any) -> str:
  159. """Build a string with Tx arrows
  160. Args:
  161. stringable (Any): stringable object to surround with arrows
  162. Returns:
  163. str: string surrounded by Tx arrows
  164. """
  165. s = str(stringable).strip(r"{}")
  166. arrow = f"{'<'*Logger.ARROW_HEAD_COUNT}{'-'*Logger.ARROW_TAIL_COUNT}"
  167. return f"\n{arrow}{s}{arrow}\n"
  168. @staticmethod
  169. def build_log_rx_str(stringable: Any, asynchronous: bool = False) -> str:
  170. """Build a string with Rx arrows
  171. Args:
  172. stringable (Any): stringable object to surround with arrows
  173. asynchronous (bool): Should the arrows contain ASYNC?. Defaults to False.
  174. Returns:
  175. str: string surrounded by Rx arrows
  176. """
  177. s = str(stringable).strip(r"{}")
  178. assert Logger.ARROW_TAIL_COUNT > 5
  179. if asynchronous:
  180. arrow = f"{'-'*(Logger.ARROW_TAIL_COUNT//2-3)}ASYNC{'-'*(Logger.ARROW_TAIL_COUNT//2-2)}{'>'*Logger.ARROW_HEAD_COUNT}"
  181. else:
  182. arrow = f"{'-'*Logger.ARROW_TAIL_COUNT}{'>'*Logger.ARROW_HEAD_COUNT}"
  183. return f"\n{arrow}{s}{arrow}\n"
  184. def setup_logging(
  185. base: logging.Logger | str,
  186. output: Path | None = None,
  187. modules: dict[str, int] | None = None,
  188. ) -> logging.Logger:
  189. """Configure the GoPro modules for logging and get a logger that can be used by the application
  190. This can only be called once and should be done at the top level of the application.
  191. Args:
  192. base (logging.Logger | str): Name of application (i.e. __name__) or preconfigured logger to use as base
  193. output (Path | None): Path of log file for file stream handler. If not set, will not log to file.
  194. modules (dict[str, int] | None): Optional override of modules / levels. Will be merged into default modules.
  195. Raises:
  196. TypeError: Base logger is not of correct type
  197. Returns:
  198. logging.Logger: updated logger that the application can use for logging
  199. """
  200. if isinstance(base, str):
  201. base = logging.getLogger(base)
  202. elif not isinstance(base, logging.Logger):
  203. raise TypeError("Base must be of type logging.Logger or str")
  204. l = Logger(base, output, modules)
  205. return l.logger
  206. def set_file_logging_level(level: int) -> None:
  207. """Change the global logging level for the default file output handler
  208. Args:
  209. level (int): level to set
  210. """
  211. if fh := Logger.get_instance().file_handler:
  212. fh.setLevel(level)
  213. def set_stream_logging_level(level: int) -> None:
  214. """Change the global logging level for the default stream output handler
  215. Args:
  216. level (int): level to set
  217. """
  218. Logger.get_instance().stream_handler.setLevel(level)
  219. def set_logging_level(level: int) -> None:
  220. """Change the global logging level for the default file and stream output handlers
  221. Args:
  222. level (int): level to set
  223. """
  224. set_file_logging_level(level)
  225. set_stream_logging_level(level)
  226. def add_logging_handler(handler: logging.Handler) -> None:
  227. """Add a handler to all of the GoPro internal modules
  228. Args:
  229. handler (logging.Handler): handler to add
  230. """
  231. Logger.get_instance().addLoggingHandler(handler)