2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -1,22 +1,28 @@
import enum
import sentry_sdk
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations.logging import (
BreadcrumbHandler,
EventHandler,
_BaseHandler,
)
from sentry_sdk.logger import _log_level_to_otel
from sentry_sdk.utils import has_logs_enabled
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from logging import LogRecord
from typing import Optional, Tuple
from typing import Any, Optional
try:
import loguru
from loguru import logger
from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT
if TYPE_CHECKING:
from loguru import Message
except ImportError:
raise DidNotEnable("LOGURU is not installed")
@@ -33,68 +39,167 @@ class LoggingLevels(enum.IntEnum):
DEFAULT_LEVEL = LoggingLevels.INFO.value
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
# We need to save the handlers to be able to remove them later
# in tests (they call `LoguruIntegration.__init__` multiple times,
# and we can't use `setup_once` because it's called before
# than we get configuration).
_ADDED_HANDLERS = (None, None) # type: Tuple[Optional[int], Optional[int]]
SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
"TRACE": "DEBUG",
"DEBUG": "DEBUG",
"INFO": "INFO",
"SUCCESS": "INFO",
"WARNING": "WARNING",
"ERROR": "ERROR",
"CRITICAL": "CRITICAL",
}
# Map Loguru level numbers to corresponding OTel level numbers
SEVERITY_TO_OTEL_SEVERITY = {
LoggingLevels.CRITICAL: 21, # fatal
LoggingLevels.ERROR: 17, # error
LoggingLevels.WARNING: 13, # warn
LoggingLevels.SUCCESS: 11, # info
LoggingLevels.INFO: 9, # info
LoggingLevels.DEBUG: 5, # debug
LoggingLevels.TRACE: 1, # trace
}
class LoguruIntegration(Integration):
identifier = "loguru"
level = DEFAULT_LEVEL # type: Optional[int]
event_level = DEFAULT_EVENT_LEVEL # type: Optional[int]
breadcrumb_format = DEFAULT_FORMAT
event_format = DEFAULT_FORMAT
sentry_logs_level = DEFAULT_LEVEL # type: Optional[int]
def __init__(
self,
level=DEFAULT_LEVEL,
event_level=DEFAULT_EVENT_LEVEL,
breadcrumb_format=DEFAULT_FORMAT,
event_format=DEFAULT_FORMAT,
sentry_logs_level=DEFAULT_LEVEL,
):
# type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction) -> None
global _ADDED_HANDLERS
breadcrumb_handler, event_handler = _ADDED_HANDLERS
if breadcrumb_handler is not None:
logger.remove(breadcrumb_handler)
breadcrumb_handler = None
if event_handler is not None:
logger.remove(event_handler)
event_handler = None
if level is not None:
breadcrumb_handler = logger.add(
LoguruBreadcrumbHandler(level=level),
level=level,
format=breadcrumb_format,
)
if event_level is not None:
event_handler = logger.add(
LoguruEventHandler(level=event_level),
level=event_level,
format=event_format,
)
_ADDED_HANDLERS = (breadcrumb_handler, event_handler)
# type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[int]) -> None
LoguruIntegration.level = level
LoguruIntegration.event_level = event_level
LoguruIntegration.breadcrumb_format = breadcrumb_format
LoguruIntegration.event_format = event_format
LoguruIntegration.sentry_logs_level = sentry_logs_level
@staticmethod
def setup_once():
# type: () -> None
pass # we do everything in __init__
if LoguruIntegration.level is not None:
logger.add(
LoguruBreadcrumbHandler(level=LoguruIntegration.level),
level=LoguruIntegration.level,
format=LoguruIntegration.breadcrumb_format,
)
if LoguruIntegration.event_level is not None:
logger.add(
LoguruEventHandler(level=LoguruIntegration.event_level),
level=LoguruIntegration.event_level,
format=LoguruIntegration.event_format,
)
if LoguruIntegration.sentry_logs_level is not None:
logger.add(
loguru_sentry_logs_handler,
level=LoguruIntegration.sentry_logs_level,
)
class _LoguruBaseHandler(_BaseHandler):
def __init__(self, *args, **kwargs):
# type: (*Any, **Any) -> None
if kwargs.get("level"):
kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
kwargs.get("level", ""), DEFAULT_LEVEL
)
super().__init__(*args, **kwargs)
def _logging_to_event_level(self, record):
# type: (LogRecord) -> str
try:
return LoggingLevels(record.levelno).name.lower()
except ValueError:
return SENTRY_LEVEL_FROM_LOGURU_LEVEL[
LoggingLevels(record.levelno).name
].lower()
except (ValueError, KeyError):
return record.levelname.lower() if record.levelname else ""
class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
pass
class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
pass
def loguru_sentry_logs_handler(message):
# type: (Message) -> None
# This is intentionally a callable sink instead of a standard logging handler
# since otherwise we wouldn't get direct access to message.record
client = sentry_sdk.get_client()
if not client.is_active():
return
if not has_logs_enabled(client.options):
return
record = message.record
if (
LoguruIntegration.sentry_logs_level is None
or record["level"].no < LoguruIntegration.sentry_logs_level
):
return
otel_severity_number, otel_severity_text = _log_level_to_otel(
record["level"].no, SEVERITY_TO_OTEL_SEVERITY
)
attrs = {"sentry.origin": "auto.logger.loguru"} # type: dict[str, Any]
project_root = client.options["project_root"]
if record.get("file"):
if project_root is not None and record["file"].path.startswith(project_root):
attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :]
else:
attrs["code.file.path"] = record["file"].path
if record.get("line") is not None:
attrs["code.line.number"] = record["line"]
if record.get("function"):
attrs["code.function.name"] = record["function"]
if record.get("thread"):
attrs["thread.name"] = record["thread"].name
attrs["thread.id"] = record["thread"].id
if record.get("process"):
attrs["process.pid"] = record["process"].id
attrs["process.executable.name"] = record["process"].name
if record.get("name"):
attrs["logger.name"] = record["name"]
client._capture_log(
{
"severity_text": otel_severity_text,
"severity_number": otel_severity_number,
"body": record["message"],
"attributes": attrs,
"time_unix_nano": int(record["time"].timestamp() * 1e9),
"trace_id": None,
}
)