-
Notifications
You must be signed in to change notification settings - Fork 549
feat(loguru): Sentry logs for Loguru #4445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 22 commits
f992680
1be8d9a
3824524
96a0f2b
062de97
31b868d
1d74c18
f48d293
a15716e
0b15f29
a377c54
e66c0c2
ac6dd2d
b8bd5ef
88cb4d7
05ccc59
2c8be6c
fab0fba
a6b083b
1a35e02
f26fc8c
8397b10
e3670f9
eb8e5a4
fc2f1bd
cfac8d7
61f6395
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import enum | ||
|
||
import sentry_sdk | ||
from sentry_sdk.integrations import Integration, DidNotEnable | ||
from sentry_sdk.integrations.logging import ( | ||
BreadcrumbHandler, | ||
|
@@ -11,12 +12,15 @@ | |
|
||
if TYPE_CHECKING: | ||
from logging import LogRecord | ||
from typing import Optional, Any | ||
from typing import Any, Optional, Tuple | ||
sentrivana marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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") | ||
|
||
|
@@ -31,6 +35,10 @@ class LoggingLevels(enum.IntEnum): | |
CRITICAL = 50 | ||
|
||
|
||
DEFAULT_LEVEL = LoggingLevels.INFO.value | ||
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value | ||
|
||
|
||
sentrivana marked this conversation as resolved.
Show resolved
Hide resolved
|
||
SENTRY_LEVEL_FROM_LOGURU_LEVEL = { | ||
"TRACE": "DEBUG", | ||
"DEBUG": "DEBUG", | ||
|
@@ -41,8 +49,22 @@ class LoggingLevels(enum.IntEnum): | |
"CRITICAL": "CRITICAL", | ||
} | ||
|
||
DEFAULT_LEVEL = LoggingLevels.INFO.value | ||
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value | ||
|
||
def _loguru_level_to_otel(record_level): | ||
# type: (int) -> Tuple[int, str] | ||
sentrivana marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for py_level, otel_severity_number, otel_severity_text in [ | ||
(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"), | ||
]: | ||
if record_level >= py_level: | ||
return otel_severity_number, otel_severity_text | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion for future PR] might be worth extracting this logic into a separate funciton, especially if we expect to add more logging integrations that would use it; seems to be verbatim repeated from the We may also or alternatively want to make constants for the OTel severity numbers, since these are hardcoded in both locations (this can also be done in separate PR) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not exactly the same as in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The brunt of the changes is in this commit |
||
return 0, "default" | ||
|
||
|
||
class LoguruIntegration(Integration): | ||
|
@@ -52,19 +74,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] | ||
sentrivana marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
sentrivana marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(): | ||
|
@@ -83,8 +108,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: | ||
|
@@ -98,24 +138,70 @@ 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 = _loguru_level_to_otel(record["level"].no) | ||
|
||
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 | ||
szokeasaurusrex marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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, | ||
} | ||
) |
Uh oh!
There was an error while loading. Please reload this page.