Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ for your feedback. How was the migration? Is everything working as expected? Is
sentry_sdk.init(
dsn="...",
_experiments={
"enable_sentry_logs": True
"enable_logs": True
}
integrations=[
LoggingIntegration(sentry_logs_level=logging.ERROR),
Expand Down
28 changes: 23 additions & 5 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from importlib import import_module
from typing import TYPE_CHECKING, List, Dict, cast, overload

import sentry_sdk
from sentry_sdk._compat import check_uwsgi_thread_support
from sentry_sdk.utils import (
AnnotatedValue,
Expand Down Expand Up @@ -190,8 +191,8 @@ def capture_event(self, *args, **kwargs):
# type: (*Any, **Any) -> Optional[str]
return None

def _capture_experimental_log(self, scope, log):
# type: (Scope, Log) -> None
def _capture_experimental_log(self, log):
# type: (Log) -> None
pass

def capture_session(self, *args, **kwargs):
Expand Down Expand Up @@ -846,12 +847,14 @@ def capture_event(

return return_value

def _capture_experimental_log(self, current_scope, log):
# type: (Scope, Log) -> None
def _capture_experimental_log(self, log):
# type: (Log) -> None
logs_enabled = self.options["_experiments"].get("enable_logs", False)
if not logs_enabled:
return
isolation_scope = current_scope.get_isolation_scope()

current_scope = sentry_sdk.get_current_scope()
isolation_scope = sentry_sdk.get_isolation_scope()

log["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
log["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
Expand Down Expand Up @@ -880,6 +883,21 @@ def _capture_experimental_log(self, current_scope, log):
elif propagation_context is not None:
log["trace_id"] = propagation_context.trace_id

# The user, if present, is always set on the isolation scope.
if isolation_scope._user is not None:
for log_attribute, user_attribute in (
("user.id", "id"),
("user.name", "username"),
("user.email", "email"),
):
if (
user_attribute in isolation_scope._user
and log_attribute not in log["attributes"]
):
log["attributes"][log_attribute] = isolation_scope._user[
user_attribute
]

# If debug is enabled, log the log to the console
debug = self.options.get("debug", False)
if debug:
Expand Down
46 changes: 21 additions & 25 deletions sentry_sdk/integrations/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import sentry_sdk
from sentry_sdk.client import BaseClient
from sentry_sdk.logger import _log_level_to_otel
from sentry_sdk.utils import (
safe_repr,
to_string,
Expand All @@ -14,7 +15,7 @@
)
from sentry_sdk.integrations import Integration

from typing import TYPE_CHECKING, Tuple
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import MutableMapping
Expand All @@ -36,6 +37,16 @@
logging.CRITICAL: "fatal", # CRITICAL is same as FATAL
}

# Map logging level numbers to corresponding OTel level numbers
SEVERITY_TO_OTEL_SEVERITY = {
logging.CRITICAL: 21, # fatal
logging.ERROR: 17, # error
logging.WARNING: 13, # warn
logging.INFO: 9, # info
logging.DEBUG: 5, # debug
}


# Capturing events from those loggers causes recursion errors. We cannot allow
# the user to unconditionally create events from those loggers under any
# circumstances.
Expand Down Expand Up @@ -124,7 +135,10 @@ def sentry_patched_callhandlers(self, record):
# the integration. Otherwise we have a high chance of getting
# into a recursion error when the integration is resolved
# (this also is slower).
if ignored_loggers is not None and record.name not in ignored_loggers:
if (
ignored_loggers is not None
and record.name.strip() not in ignored_loggers
):
integration = sentry_sdk.get_client().get_integration(
LoggingIntegration
)
Expand Down Expand Up @@ -169,7 +183,7 @@ def _can_record(self, record):
# type: (LogRecord) -> bool
"""Prevents ignored loggers from recording"""
for logger in _IGNORED_LOGGERS:
if fnmatch(record.name, logger):
if fnmatch(record.name.strip(), logger):
return False
return True

Expand Down Expand Up @@ -317,21 +331,6 @@ def _breadcrumb_from_record(self, record):
}


def _python_level_to_otel(record_level):
# type: (int) -> Tuple[int, str]
for py_level, otel_severity_number, otel_severity_text in [
(50, 21, "fatal"),
(40, 17, "error"),
(30, 13, "warn"),
(20, 9, "info"),
(10, 5, "debug"),
(5, 1, "trace"),
]:
if record_level >= py_level:
return otel_severity_number, otel_severity_text
return 0, "default"


class SentryLogsHandler(_BaseHandler):
"""
A logging handler that records Sentry logs for each Python log record.
Expand All @@ -357,8 +356,9 @@ def emit(self, record):

def _capture_log_from_record(self, client, record):
# type: (BaseClient, LogRecord) -> None
scope = sentry_sdk.get_current_scope()
otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno)
otel_severity_number, otel_severity_text = _log_level_to_otel(
record.levelno, SEVERITY_TO_OTEL_SEVERITY
)
project_root = client.options["project_root"]
attrs = self._extra_from_record(record) # type: Any
attrs["sentry.origin"] = "auto.logger.log"
Expand All @@ -369,10 +369,7 @@ def _capture_log_from_record(self, client, record):
for i, arg in enumerate(record.args):
attrs[f"sentry.message.parameter.{i}"] = (
arg
if isinstance(arg, str)
or isinstance(arg, float)
or isinstance(arg, int)
or isinstance(arg, bool)
if isinstance(arg, (str, float, int, bool))
else safe_repr(arg)
)
if record.lineno:
Expand All @@ -399,7 +396,6 @@ def _capture_log_from_record(self, client, record):

# noinspection PyProtectedMember
client._capture_experimental_log(
scope,
{
"severity_text": otel_severity_text,
"severity_number": otel_severity_number,
Expand Down
121 changes: 102 additions & 19 deletions sentry_sdk/integrations/loguru.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
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 typing import TYPE_CHECKING

if TYPE_CHECKING:
from logging import LogRecord
from typing import Optional, Any
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")

Expand All @@ -31,6 +36,10 @@ class LoggingLevels(enum.IntEnum):
CRITICAL = 50


DEFAULT_LEVEL = LoggingLevels.INFO.value
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value


SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
"TRACE": "DEBUG",
"DEBUG": "DEBUG",
Expand All @@ -41,8 +50,16 @@ class LoggingLevels(enum.IntEnum):
"CRITICAL": "CRITICAL",
}

DEFAULT_LEVEL = LoggingLevels.INFO.value
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
# 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):
Expand All @@ -52,19 +69,22 @@ class LoguruIntegration(Integration):
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
# 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():
Expand All @@ -83,8 +103,23 @@ def setup_once():
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:
Expand All @@ -98,24 +133,72 @@ def _logging_to_event_level(self, record):
class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""

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)
pass


class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
"""Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""

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
)
pass

super().__init__(*args, **kwargs)

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 client.options["_experiments"].get("enable_logs", False):
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_experimental_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,
}
)
Loading
Loading