loguru.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import enum
  2. import sentry_sdk
  3. from sentry_sdk.integrations import Integration, DidNotEnable
  4. from sentry_sdk.integrations.logging import (
  5. BreadcrumbHandler,
  6. EventHandler,
  7. _BaseHandler,
  8. )
  9. from sentry_sdk.logger import _log_level_to_otel
  10. from sentry_sdk.utils import has_logs_enabled
  11. from typing import TYPE_CHECKING
  12. if TYPE_CHECKING:
  13. from logging import LogRecord
  14. from typing import Any, Optional
  15. try:
  16. import loguru
  17. from loguru import logger
  18. from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT
  19. if TYPE_CHECKING:
  20. from loguru import Message
  21. except ImportError:
  22. raise DidNotEnable("LOGURU is not installed")
  23. class LoggingLevels(enum.IntEnum):
  24. TRACE = 5
  25. DEBUG = 10
  26. INFO = 20
  27. SUCCESS = 25
  28. WARNING = 30
  29. ERROR = 40
  30. CRITICAL = 50
  31. DEFAULT_LEVEL = LoggingLevels.INFO.value
  32. DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
  33. SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
  34. "TRACE": "DEBUG",
  35. "DEBUG": "DEBUG",
  36. "INFO": "INFO",
  37. "SUCCESS": "INFO",
  38. "WARNING": "WARNING",
  39. "ERROR": "ERROR",
  40. "CRITICAL": "CRITICAL",
  41. }
  42. # Map Loguru level numbers to corresponding OTel level numbers
  43. SEVERITY_TO_OTEL_SEVERITY = {
  44. LoggingLevels.CRITICAL: 21, # fatal
  45. LoggingLevels.ERROR: 17, # error
  46. LoggingLevels.WARNING: 13, # warn
  47. LoggingLevels.SUCCESS: 11, # info
  48. LoggingLevels.INFO: 9, # info
  49. LoggingLevels.DEBUG: 5, # debug
  50. LoggingLevels.TRACE: 1, # trace
  51. }
  52. class LoguruIntegration(Integration):
  53. identifier = "loguru"
  54. level = DEFAULT_LEVEL # type: Optional[int]
  55. event_level = DEFAULT_EVENT_LEVEL # type: Optional[int]
  56. breadcrumb_format = DEFAULT_FORMAT
  57. event_format = DEFAULT_FORMAT
  58. sentry_logs_level = DEFAULT_LEVEL # type: Optional[int]
  59. def __init__(
  60. self,
  61. level=DEFAULT_LEVEL,
  62. event_level=DEFAULT_EVENT_LEVEL,
  63. breadcrumb_format=DEFAULT_FORMAT,
  64. event_format=DEFAULT_FORMAT,
  65. sentry_logs_level=DEFAULT_LEVEL,
  66. ):
  67. # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[int]) -> None
  68. LoguruIntegration.level = level
  69. LoguruIntegration.event_level = event_level
  70. LoguruIntegration.breadcrumb_format = breadcrumb_format
  71. LoguruIntegration.event_format = event_format
  72. LoguruIntegration.sentry_logs_level = sentry_logs_level
  73. @staticmethod
  74. def setup_once():
  75. # type: () -> None
  76. if LoguruIntegration.level is not None:
  77. logger.add(
  78. LoguruBreadcrumbHandler(level=LoguruIntegration.level),
  79. level=LoguruIntegration.level,
  80. format=LoguruIntegration.breadcrumb_format,
  81. )
  82. if LoguruIntegration.event_level is not None:
  83. logger.add(
  84. LoguruEventHandler(level=LoguruIntegration.event_level),
  85. level=LoguruIntegration.event_level,
  86. format=LoguruIntegration.event_format,
  87. )
  88. if LoguruIntegration.sentry_logs_level is not None:
  89. logger.add(
  90. loguru_sentry_logs_handler,
  91. level=LoguruIntegration.sentry_logs_level,
  92. )
  93. class _LoguruBaseHandler(_BaseHandler):
  94. def __init__(self, *args, **kwargs):
  95. # type: (*Any, **Any) -> None
  96. if kwargs.get("level"):
  97. kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
  98. kwargs.get("level", ""), DEFAULT_LEVEL
  99. )
  100. super().__init__(*args, **kwargs)
  101. def _logging_to_event_level(self, record):
  102. # type: (LogRecord) -> str
  103. try:
  104. return SENTRY_LEVEL_FROM_LOGURU_LEVEL[
  105. LoggingLevels(record.levelno).name
  106. ].lower()
  107. except (ValueError, KeyError):
  108. return record.levelname.lower() if record.levelname else ""
  109. class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
  110. """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
  111. pass
  112. class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
  113. """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
  114. pass
  115. def loguru_sentry_logs_handler(message):
  116. # type: (Message) -> None
  117. # This is intentionally a callable sink instead of a standard logging handler
  118. # since otherwise we wouldn't get direct access to message.record
  119. client = sentry_sdk.get_client()
  120. if not client.is_active():
  121. return
  122. if not has_logs_enabled(client.options):
  123. return
  124. record = message.record
  125. if (
  126. LoguruIntegration.sentry_logs_level is None
  127. or record["level"].no < LoguruIntegration.sentry_logs_level
  128. ):
  129. return
  130. otel_severity_number, otel_severity_text = _log_level_to_otel(
  131. record["level"].no, SEVERITY_TO_OTEL_SEVERITY
  132. )
  133. attrs = {"sentry.origin": "auto.logger.loguru"} # type: dict[str, Any]
  134. project_root = client.options["project_root"]
  135. if record.get("file"):
  136. if project_root is not None and record["file"].path.startswith(project_root):
  137. attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :]
  138. else:
  139. attrs["code.file.path"] = record["file"].path
  140. if record.get("line") is not None:
  141. attrs["code.line.number"] = record["line"]
  142. if record.get("function"):
  143. attrs["code.function.name"] = record["function"]
  144. if record.get("thread"):
  145. attrs["thread.name"] = record["thread"].name
  146. attrs["thread.id"] = record["thread"].id
  147. if record.get("process"):
  148. attrs["process.pid"] = record["process"].id
  149. attrs["process.executable.name"] = record["process"].name
  150. if record.get("name"):
  151. attrs["logger.name"] = record["name"]
  152. client._capture_log(
  153. {
  154. "severity_text": otel_severity_text,
  155. "severity_number": otel_severity_number,
  156. "body": record["message"],
  157. "attributes": attrs,
  158. "time_unix_nano": int(record["time"].timestamp() * 1e9),
  159. "trace_id": None,
  160. }
  161. )