loguru.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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, safe_repr
  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: "Optional[int]" = DEFAULT_LEVEL
  55. event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL
  56. breadcrumb_format = DEFAULT_FORMAT
  57. event_format = DEFAULT_FORMAT
  58. sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL
  59. def __init__(
  60. self,
  61. level: "Optional[int]" = DEFAULT_LEVEL,
  62. event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL,
  63. breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT,
  64. event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT,
  65. sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL,
  66. ) -> None:
  67. LoguruIntegration.level = level
  68. LoguruIntegration.event_level = event_level
  69. LoguruIntegration.breadcrumb_format = breadcrumb_format
  70. LoguruIntegration.event_format = event_format
  71. LoguruIntegration.sentry_logs_level = sentry_logs_level
  72. @staticmethod
  73. def setup_once() -> None:
  74. if LoguruIntegration.level is not None:
  75. logger.add(
  76. LoguruBreadcrumbHandler(level=LoguruIntegration.level),
  77. level=LoguruIntegration.level,
  78. format=LoguruIntegration.breadcrumb_format,
  79. )
  80. if LoguruIntegration.event_level is not None:
  81. logger.add(
  82. LoguruEventHandler(level=LoguruIntegration.event_level),
  83. level=LoguruIntegration.event_level,
  84. format=LoguruIntegration.event_format,
  85. )
  86. if LoguruIntegration.sentry_logs_level is not None:
  87. logger.add(
  88. loguru_sentry_logs_handler,
  89. level=LoguruIntegration.sentry_logs_level,
  90. )
  91. class _LoguruBaseHandler(_BaseHandler):
  92. def __init__(self, *args: "Any", **kwargs: "Any") -> None:
  93. if kwargs.get("level"):
  94. kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
  95. kwargs.get("level", ""), DEFAULT_LEVEL
  96. )
  97. super().__init__(*args, **kwargs)
  98. def _logging_to_event_level(self, record: "LogRecord") -> str:
  99. try:
  100. return SENTRY_LEVEL_FROM_LOGURU_LEVEL[
  101. LoggingLevels(record.levelno).name
  102. ].lower()
  103. except (ValueError, KeyError):
  104. return record.levelname.lower() if record.levelname else ""
  105. class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
  106. """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
  107. pass
  108. class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
  109. """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
  110. pass
  111. def loguru_sentry_logs_handler(message: "Message") -> None:
  112. # This is intentionally a callable sink instead of a standard logging handler
  113. # since otherwise we wouldn't get direct access to message.record
  114. client = sentry_sdk.get_client()
  115. if not client.is_active():
  116. return
  117. if not has_logs_enabled(client.options):
  118. return
  119. record = message.record
  120. if (
  121. LoguruIntegration.sentry_logs_level is None
  122. or record["level"].no < LoguruIntegration.sentry_logs_level
  123. ):
  124. return
  125. otel_severity_number, otel_severity_text = _log_level_to_otel(
  126. record["level"].no, SEVERITY_TO_OTEL_SEVERITY
  127. )
  128. attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"}
  129. project_root = client.options["project_root"]
  130. if record.get("file"):
  131. if project_root is not None and record["file"].path.startswith(project_root):
  132. attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :]
  133. else:
  134. attrs["code.file.path"] = record["file"].path
  135. if record.get("line") is not None:
  136. attrs["code.line.number"] = record["line"]
  137. if record.get("function"):
  138. attrs["code.function.name"] = record["function"]
  139. if record.get("thread"):
  140. attrs["thread.name"] = record["thread"].name
  141. attrs["thread.id"] = record["thread"].id
  142. if record.get("process"):
  143. attrs["process.pid"] = record["process"].id
  144. attrs["process.executable.name"] = record["process"].name
  145. if record.get("name"):
  146. attrs["logger.name"] = record["name"]
  147. extra = record.get("extra")
  148. if isinstance(extra, dict):
  149. for key, value in extra.items():
  150. if isinstance(value, (str, int, float, bool)):
  151. attrs[f"sentry.message.parameter.{key}"] = value
  152. else:
  153. attrs[f"sentry.message.parameter.{key}"] = safe_repr(value)
  154. sentry_sdk.get_current_scope()._capture_log(
  155. {
  156. "severity_text": otel_severity_text,
  157. "severity_number": otel_severity_number,
  158. "body": record["message"],
  159. "attributes": attrs,
  160. "time_unix_nano": int(record["time"].timestamp() * 1e9),
  161. "trace_id": None,
  162. "span_id": None,
  163. }
  164. )