From f9926800d986ab275229412b900399ba99dd5907 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 2 Jun 2025 15:09:58 +0200 Subject: [PATCH 01/24] feat(loguru): Support Sentry logs --- CHANGELOG.md | 2 +- sentry_sdk/integrations/loguru.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d73e1bdd0..8dd7763121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,7 +122,7 @@ sentry_sdk.init( dsn="...", _experiments={ - "enable_sentry_logs": True + "enable_logs": True } integrations=[ LoggingIntegration(sentry_logs_level=logging.ERROR), diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index a71c4ac87f..bf69646cc7 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -4,6 +4,7 @@ from sentry_sdk.integrations.logging import ( BreadcrumbHandler, EventHandler, + SentryLogsHandler, _BaseHandler, ) @@ -59,12 +60,14 @@ def __init__( 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(): @@ -83,6 +86,9 @@ def setup_once(): format=LoguruIntegration.event_format, ) + if LoguruIntegration.sentry_logs_level is not None: + logger.add(SentryLogsHandler(level=LoguruIntegration.sentry_logs_level)) + class _LoguruBaseHandler(_BaseHandler): def _logging_to_event_level(self, record): From 1be8d9a63bbade83f0edbb752196c5977f6db451 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 3 Jun 2025 07:58:39 +0200 Subject: [PATCH 02/24] . --- sentry_sdk/integrations/logging.py | 5 +- sentry_sdk/integrations/loguru.py | 38 ++-- tests/integrations/logging/test_logging.py | 200 +++++++++++++++++++++ tests/test_logs.py | 197 -------------------- 4 files changed, 222 insertions(+), 218 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 449a05fbf7..6508c861e2 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -364,10 +364,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: diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index bf69646cc7..a019bd3b04 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -87,10 +87,23 @@ def setup_once(): ) if LoguruIntegration.sentry_logs_level is not None: - logger.add(SentryLogsHandler(level=LoguruIntegration.sentry_logs_level)) + logger.add( + LoguruSentryLogsHandler(level=LoguruIntegration.sentry_logs_level), + level=LoguruIntegration.sentry_logs_level, + format=LoguruIntegration.event_format, + ) 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: @@ -103,25 +116,16 @@ 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.""" + pass - 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) +class LoguruSentryLogsHandler(_LoguruBaseHandler, SentryLogsHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.SentryLogsHandler` to use loguru's level names.""" + def _capture_log_from_record(self, client, record): + # type: (BaseClient, LogRecord) + return super()._capture_log_from_record(client, record) diff --git a/tests/integrations/logging/test_logging.py b/tests/integrations/logging/test_logging.py index c08e960c00..fa8a53d759 100644 --- a/tests/integrations/logging/test_logging.py +++ b/tests/integrations/logging/test_logging.py @@ -3,7 +3,10 @@ import pytest +from sentry_sdk import get_client +from sentry_sdk.consts import VERSION from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger +from tests.test_logs import envelopes_to_logs other_logger = logging.getLogger("testfoo") logger = logging.getLogger(__name__) @@ -283,3 +286,200 @@ def test_logging_dictionary_args(sentry_init, capture_events): == "the value of foo is bar, and the value of bar is baz" ) assert event["logentry"]["params"] == {"foo": "bar", "bar": "baz"} + + +@minimum_python_37 +def test_sentry_logs_warning(sentry_init, capture_envelopes): + """ + The python logger module should create 'warn' sentry logs if the flag is on. + """ + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.warning("this is %s a template %s", "1", "2") + + get_client().flush() + logs = envelopes_to_logs(envelopes) + attrs = logs[0]["attributes"] + assert attrs["sentry.message.template"] == "this is %s a template %s" + assert "code.file.path" in attrs + assert "code.line.number" in attrs + assert attrs["logger.name"] == "test-logger" + assert attrs["sentry.environment"] == "production" + assert attrs["sentry.message.parameter.0"] == "1" + assert attrs["sentry.message.parameter.1"] == "2" + assert attrs["sentry.origin"] == "auto.logger.log" + assert logs[0]["severity_number"] == 13 + assert logs[0]["severity_text"] == "warn" + + +@minimum_python_37 +def test_sentry_logs_debug(sentry_init, capture_envelopes): + """ + The python logger module should not create 'debug' sentry logs if the flag is on by default + """ + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.debug("this is %s a template %s", "1", "2") + get_client().flush() + + assert len(envelopes) == 0 + + +@minimum_python_37 +def test_no_log_infinite_loop(sentry_init, capture_envelopes): + """ + If 'debug' mode is true, and you set a low log level in the logging integration, there should be no infinite loops. + """ + sentry_init( + _experiments={"enable_logs": True}, + integrations=[LoggingIntegration(sentry_logs_level=logging.DEBUG)], + debug=True, + ) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.debug("this is %s a template %s", "1", "2") + get_client().flush() + + assert len(envelopes) == 1 + + +@minimum_python_37 +def test_logging_errors(sentry_init, capture_envelopes): + """ + The python logger module should be able to log errors without erroring + """ + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.error(Exception("test exc 1")) + python_logger.error("error is %s", Exception("test exc 2")) + get_client().flush() + + error_event_1 = envelopes[0].items[0].payload.json + assert error_event_1["level"] == "error" + error_event_2 = envelopes[1].items[0].payload.json + assert error_event_2["level"] == "error" + + logs = envelopes_to_logs(envelopes) + assert logs[0]["severity_text"] == "error" + assert "sentry.message.template" not in logs[0]["attributes"] + assert "sentry.message.parameter.0" not in logs[0]["attributes"] + assert "code.line.number" in logs[0]["attributes"] + + assert logs[1]["severity_text"] == "error" + assert logs[1]["attributes"]["sentry.message.template"] == "error is %s" + assert ( + logs[1]["attributes"]["sentry.message.parameter.0"] == "Exception('test exc 2')" + ) + assert "code.line.number" in logs[1]["attributes"] + + assert len(logs) == 2 + + +def test_log_strips_project_root(sentry_init, capture_envelopes): + """ + The python logger should strip project roots from the log record path + """ + sentry_init( + _experiments={"enable_logs": True}, + project_root="/custom/test", + ) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.handle( + logging.LogRecord( + name="test-logger", + level=logging.WARN, + pathname="/custom/test/blah/path.py", + lineno=123, + msg="This is a test log with a custom pathname", + args=(), + exc_info=None, + ) + ) + get_client().flush() + + logs = envelopes_to_logs(envelopes) + assert len(logs) == 1 + attrs = logs[0]["attributes"] + assert attrs["code.file.path"] == "blah/path.py" + + +def test_logger_with_all_attributes(sentry_init, capture_envelopes): + """ + The python logger should be able to log all attributes, including extra data. + """ + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.warning( + "log #%d", + 1, + extra={"foo": "bar", "numeric": 42, "more_complex": {"nested": "data"}}, + ) + get_client().flush() + + logs = envelopes_to_logs(envelopes) + + attributes = logs[0]["attributes"] + + assert "process.pid" in attributes + assert isinstance(attributes["process.pid"], int) + del attributes["process.pid"] + + assert "sentry.release" in attributes + assert isinstance(attributes["sentry.release"], str) + del attributes["sentry.release"] + + assert "server.address" in attributes + assert isinstance(attributes["server.address"], str) + del attributes["server.address"] + + assert "thread.id" in attributes + assert isinstance(attributes["thread.id"], int) + del attributes["thread.id"] + + assert "code.file.path" in attributes + assert isinstance(attributes["code.file.path"], str) + del attributes["code.file.path"] + + assert "code.function.name" in attributes + assert isinstance(attributes["code.function.name"], str) + del attributes["code.function.name"] + + assert "code.line.number" in attributes + assert isinstance(attributes["code.line.number"], int) + del attributes["code.line.number"] + + assert "process.executable.name" in attributes + assert isinstance(attributes["process.executable.name"], str) + del attributes["process.executable.name"] + + assert "thread.name" in attributes + assert isinstance(attributes["thread.name"], str) + del attributes["thread.name"] + + assert attributes.pop("sentry.sdk.name").startswith("sentry.python") + + # Assert on the remaining non-dynamic attributes. + assert attributes == { + "foo": "bar", + "numeric": 42, + "more_complex": "{'nested': 'data'}", + "logger.name": "test-logger", + "sentry.origin": "auto.logger.log", + "sentry.message.template": "log #%d", + "sentry.message.parameter.0": 1, + "sentry.environment": "production", + "sentry.sdk.version": VERSION, + "sentry.severity_number": 13, + "sentry.severity_text": "warn", + } diff --git a/tests/test_logs.py b/tests/test_logs.py index 7e8a72d30a..3f3d069fde 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -268,203 +268,6 @@ def test_logs_tied_to_spans(sentry_init, capture_envelopes): assert logs[0]["attributes"]["sentry.trace.parent_span_id"] == span.span_id -@minimum_python_37 -def test_logger_integration_warning(sentry_init, capture_envelopes): - """ - The python logger module should create 'warn' sentry logs if the flag is on. - """ - sentry_init(_experiments={"enable_logs": True}) - envelopes = capture_envelopes() - - python_logger = logging.Logger("test-logger") - python_logger.warning("this is %s a template %s", "1", "2") - - get_client().flush() - logs = envelopes_to_logs(envelopes) - attrs = logs[0]["attributes"] - assert attrs["sentry.message.template"] == "this is %s a template %s" - assert "code.file.path" in attrs - assert "code.line.number" in attrs - assert attrs["logger.name"] == "test-logger" - assert attrs["sentry.environment"] == "production" - assert attrs["sentry.message.parameter.0"] == "1" - assert attrs["sentry.message.parameter.1"] == "2" - assert attrs["sentry.origin"] == "auto.logger.log" - assert logs[0]["severity_number"] == 13 - assert logs[0]["severity_text"] == "warn" - - -@minimum_python_37 -def test_logger_integration_debug(sentry_init, capture_envelopes): - """ - The python logger module should not create 'debug' sentry logs if the flag is on by default - """ - sentry_init(_experiments={"enable_logs": True}) - envelopes = capture_envelopes() - - python_logger = logging.Logger("test-logger") - python_logger.debug("this is %s a template %s", "1", "2") - get_client().flush() - - assert len(envelopes) == 0 - - -@minimum_python_37 -def test_no_log_infinite_loop(sentry_init, capture_envelopes): - """ - If 'debug' mode is true, and you set a low log level in the logging integration, there should be no infinite loops. - """ - sentry_init( - _experiments={"enable_logs": True}, - integrations=[LoggingIntegration(sentry_logs_level=logging.DEBUG)], - debug=True, - ) - envelopes = capture_envelopes() - - python_logger = logging.Logger("test-logger") - python_logger.debug("this is %s a template %s", "1", "2") - get_client().flush() - - assert len(envelopes) == 1 - - -@minimum_python_37 -def test_logging_errors(sentry_init, capture_envelopes): - """ - The python logger module should be able to log errors without erroring - """ - sentry_init(_experiments={"enable_logs": True}) - envelopes = capture_envelopes() - - python_logger = logging.Logger("test-logger") - python_logger.error(Exception("test exc 1")) - python_logger.error("error is %s", Exception("test exc 2")) - get_client().flush() - - error_event_1 = envelopes[0].items[0].payload.json - assert error_event_1["level"] == "error" - error_event_2 = envelopes[1].items[0].payload.json - assert error_event_2["level"] == "error" - - logs = envelopes_to_logs(envelopes) - assert logs[0]["severity_text"] == "error" - assert "sentry.message.template" not in logs[0]["attributes"] - assert "sentry.message.parameter.0" not in logs[0]["attributes"] - assert "code.line.number" in logs[0]["attributes"] - - assert logs[1]["severity_text"] == "error" - assert logs[1]["attributes"]["sentry.message.template"] == "error is %s" - assert ( - logs[1]["attributes"]["sentry.message.parameter.0"] == "Exception('test exc 2')" - ) - assert "code.line.number" in logs[1]["attributes"] - - assert len(logs) == 2 - - -def test_log_strips_project_root(sentry_init, capture_envelopes): - """ - The python logger should strip project roots from the log record path - """ - sentry_init( - _experiments={"enable_logs": True}, - project_root="/custom/test", - ) - envelopes = capture_envelopes() - - python_logger = logging.Logger("test-logger") - python_logger.handle( - logging.LogRecord( - name="test-logger", - level=logging.WARN, - pathname="/custom/test/blah/path.py", - lineno=123, - msg="This is a test log with a custom pathname", - args=(), - exc_info=None, - ) - ) - get_client().flush() - - logs = envelopes_to_logs(envelopes) - assert len(logs) == 1 - attrs = logs[0]["attributes"] - assert attrs["code.file.path"] == "blah/path.py" - - -def test_logger_with_all_attributes(sentry_init, capture_envelopes): - """ - The python logger should be able to log all attributes, including extra data. - """ - sentry_init(_experiments={"enable_logs": True}) - envelopes = capture_envelopes() - - python_logger = logging.Logger("test-logger") - python_logger.warning( - "log #%d", - 1, - extra={"foo": "bar", "numeric": 42, "more_complex": {"nested": "data"}}, - ) - get_client().flush() - - logs = envelopes_to_logs(envelopes) - - attributes = logs[0]["attributes"] - - assert "process.pid" in attributes - assert isinstance(attributes["process.pid"], int) - del attributes["process.pid"] - - assert "sentry.release" in attributes - assert isinstance(attributes["sentry.release"], str) - del attributes["sentry.release"] - - assert "server.address" in attributes - assert isinstance(attributes["server.address"], str) - del attributes["server.address"] - - assert "thread.id" in attributes - assert isinstance(attributes["thread.id"], int) - del attributes["thread.id"] - - assert "code.file.path" in attributes - assert isinstance(attributes["code.file.path"], str) - del attributes["code.file.path"] - - assert "code.function.name" in attributes - assert isinstance(attributes["code.function.name"], str) - del attributes["code.function.name"] - - assert "code.line.number" in attributes - assert isinstance(attributes["code.line.number"], int) - del attributes["code.line.number"] - - assert "process.executable.name" in attributes - assert isinstance(attributes["process.executable.name"], str) - del attributes["process.executable.name"] - - assert "thread.name" in attributes - assert isinstance(attributes["thread.name"], str) - del attributes["thread.name"] - - assert attributes.pop("sentry.sdk.name").startswith("sentry.python") - - # Assert on the remaining non-dynamic attributes. - assert attributes == { - "foo": "bar", - "numeric": 42, - "more_complex": "{'nested': 'data'}", - "logger.name": "test-logger", - "sentry.origin": "auto.logger.log", - "sentry.message.template": "log #%d", - "sentry.message.parameter.0": 1, - "sentry.environment": "production", - "sentry.sdk.version": VERSION, - "sentry.severity_number": 13, - "sentry.severity_text": "warn", - } - - def test_auto_flush_logs_after_100(sentry_init, capture_envelopes): """ If you log >100 logs, it should automatically trigger a flush. From 3824524975fa67e5cfcb4dcf3b69a1e3eef92db0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 3 Jun 2025 12:37:28 +0200 Subject: [PATCH 03/24] . --- sentry_sdk/integrations/loguru.py | 52 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index a019bd3b04..d2d5d4880b 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -1,11 +1,13 @@ import enum +import sentry_sdk from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ( BreadcrumbHandler, EventHandler, SentryLogsHandler, _BaseHandler, + _python_level_to_otel, ) from typing import TYPE_CHECKING @@ -124,8 +126,54 @@ class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): pass -class LoguruSentryLogsHandler(_LoguruBaseHandler, SentryLogsHandler): +def _loguru_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"), + (25, 11, "info"), # Loguru's success + (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 LoguruSentryLogsHandler(_LoguruBaseHandler): """Modified version of :class:`sentry_sdk.integrations.logging.SentryLogsHandler` to use loguru's level names.""" + def emit(self, record): + client = sentry_sdk.get_client() + + if not client.is_active(): + return + + if not client.options["_experiments"].get("enable_logs", False): + return + + print('rec', record) + print('strofrec', str(record)) + + self._capture_log_from_record(client, record) + def _capture_log_from_record(self, client, record): # type: (BaseClient, LogRecord) - return super()._capture_log_from_record(client, record) + scope = sentry_sdk.get_current_scope() + + otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno) + + attrs = {} + + client._capture_experimental_log( + scope, + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record.msg, + "attributes": attrs, + "time_unix_nano": int(record.created * 1e9), + "trace_id": None, + }, + ) From 96a0f2bdc1f4d545629619b9cfd0ee90e833b0bd Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 08:15:41 +0200 Subject: [PATCH 04/24] . --- sentry_sdk/integrations/loguru.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index d2d5d4880b..e854eaefc9 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -1,4 +1,5 @@ import enum +import functools import sentry_sdk from sentry_sdk.integrations import Integration, DidNotEnable @@ -20,6 +21,7 @@ import loguru from loguru import logger from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT + from loguru._logger import Logger except ImportError: raise DidNotEnable("LOGURU is not installed") @@ -89,11 +91,24 @@ def setup_once(): ) if LoguruIntegration.sentry_logs_level is not None: - logger.add( - LoguruSentryLogsHandler(level=LoguruIntegration.sentry_logs_level), - level=LoguruIntegration.sentry_logs_level, - format=LoguruIntegration.event_format, - ) + #logger.add( + # LoguruSentryLogsHandler(level=LoguruIntegration.sentry_logs_level), + # level=LoguruIntegration.sentry_logs_level, + # format=LoguruIntegration.event_format, + #) + + original_log = Logger._log + + @functools.wraps(original_log) + def _sentry_patched_log(self, *args, **kwargs): + print('hello from senry patched') + log_args = args[4] + if log_args: + pass + result = original_log(self, *args, **kwargs) + return result + + Logger._log = _sentry_patched_log class _LoguruBaseHandler(_BaseHandler): From 062de9712b568542880af55f76fe0f9af83747eb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 13:01:23 +0200 Subject: [PATCH 05/24] . --- sentry_sdk/integrations/loguru.py | 142 ++++++++++++++++-------------- 1 file changed, 77 insertions(+), 65 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index e854eaefc9..1cc9d43e0e 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -1,12 +1,10 @@ import enum -import functools import sentry_sdk from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ( BreadcrumbHandler, EventHandler, - SentryLogsHandler, _BaseHandler, _python_level_to_otel, ) @@ -15,13 +13,12 @@ if TYPE_CHECKING: from logging import LogRecord - from typing import Optional, Any + from typing import Optional, Any, Tuple try: import loguru from loguru import logger from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT - from loguru._logger import Logger except ImportError: raise DidNotEnable("LOGURU is not installed") @@ -36,6 +33,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", @@ -46,8 +47,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] + 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 + + return 0, "default" class LoguruIntegration(Integration): @@ -91,24 +106,10 @@ def setup_once(): ) if LoguruIntegration.sentry_logs_level is not None: - #logger.add( - # LoguruSentryLogsHandler(level=LoguruIntegration.sentry_logs_level), - # level=LoguruIntegration.sentry_logs_level, - # format=LoguruIntegration.event_format, - #) - - original_log = Logger._log - - @functools.wraps(original_log) - def _sentry_patched_log(self, *args, **kwargs): - print('hello from senry patched') - log_args = args[4] - if log_args: - pass - result = original_log(self, *args, **kwargs) - return result - - Logger._log = _sentry_patched_log + logger.add( + loguru_sentry_logs_handler, + level=LoguruIntegration.sentry_logs_level, + ) class _LoguruBaseHandler(_BaseHandler): @@ -133,62 +134,73 @@ 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.""" + pass class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" + pass -def _loguru_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"), - (25, 11, "info"), # Loguru's success - (20, 9, "info"), - (10, 5, "debug"), - (5, 1, "trace"), - ]: - if record_level >= py_level: - return otel_severity_number, otel_severity_text - return 0, "default" +def loguru_sentry_logs_handler(message): + 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 record["level"].no < LoguruIntegration.sentry_logs_level: + return + + scope = sentry_sdk.get_current_scope() + otel_severity_number, otel_severity_text = _python_level_to_otel(record["level"].no) -class LoguruSentryLogsHandler(_LoguruBaseHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.SentryLogsHandler` to use loguru's level names.""" - def emit(self, record): - client = sentry_sdk.get_client() + attrs = { + "sentry.origin": "auto.logger.loguru", + } - if not client.is_active(): - return + 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 + else: + attrs["code.file.path"] = record["file"].path - if not client.options["_experiments"].get("enable_logs", False): - return + if record.get("line") is not None: + attrs["code.line.number"] = record["line"] - print('rec', record) - print('strofrec', str(record)) + if record.get("function"): + attrs["code.function.name"] = record["function"] - self._capture_log_from_record(client, record) + if record.get("thread"): + attrs["thread.name"] = record["thread"].name + attrs["thread.id"] = record["thread"].id - def _capture_log_from_record(self, client, record): - # type: (BaseClient, LogRecord) - scope = sentry_sdk.get_current_scope() + if record.get("process"): + attrs["process.pid"] = record["process"].id + attrs["process.executable.name"] = record["process"].name - otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno) + if record.get("name"): + attrs["logger.name"] = record["name"] - attrs = {} + if record["message"]: + attrs["sentry.message.template"] = record["message"] - client._capture_experimental_log( - scope, - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record.msg, - "attributes": attrs, - "time_unix_nano": int(record.created * 1e9), - "trace_id": None, - }, - ) + client._capture_experimental_log( + scope, + { + "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, + }, + ) From 1d74c18de3baa0d2b55239880bf47b5a0c33f542 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 13:08:18 +0200 Subject: [PATCH 06/24] do we need this? --- tests/integrations/logging/test_logging.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integrations/logging/test_logging.py b/tests/integrations/logging/test_logging.py index fa8a53d759..8121bfc0a2 100644 --- a/tests/integrations/logging/test_logging.py +++ b/tests/integrations/logging/test_logging.py @@ -288,7 +288,6 @@ def test_logging_dictionary_args(sentry_init, capture_events): assert event["logentry"]["params"] == {"foo": "bar", "bar": "baz"} -@minimum_python_37 def test_sentry_logs_warning(sentry_init, capture_envelopes): """ The python logger module should create 'warn' sentry logs if the flag is on. @@ -314,7 +313,6 @@ def test_sentry_logs_warning(sentry_init, capture_envelopes): assert logs[0]["severity_text"] == "warn" -@minimum_python_37 def test_sentry_logs_debug(sentry_init, capture_envelopes): """ The python logger module should not create 'debug' sentry logs if the flag is on by default @@ -329,7 +327,6 @@ def test_sentry_logs_debug(sentry_init, capture_envelopes): assert len(envelopes) == 0 -@minimum_python_37 def test_no_log_infinite_loop(sentry_init, capture_envelopes): """ If 'debug' mode is true, and you set a low log level in the logging integration, there should be no infinite loops. @@ -348,7 +345,6 @@ def test_no_log_infinite_loop(sentry_init, capture_envelopes): assert len(envelopes) == 1 -@minimum_python_37 def test_logging_errors(sentry_init, capture_envelopes): """ The python logger module should be able to log errors without erroring From f48d29338f972ba4930ae27bdc2d02fe462adb09 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 13:10:08 +0200 Subject: [PATCH 07/24] unused import --- tests/test_logs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_logs.py b/tests/test_logs.py index 3f3d069fde..394a91772a 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -9,7 +9,6 @@ import sentry_sdk.logger from sentry_sdk import get_client from sentry_sdk.envelope import Envelope -from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.types import Log from sentry_sdk.consts import SPANDATA, VERSION From a15716e36165c5730af3786b9be9ba2d78ddcd76 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 13:21:47 +0200 Subject: [PATCH 08/24] wut py3.6 --- tests/integrations/logging/test_logging.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integrations/logging/test_logging.py b/tests/integrations/logging/test_logging.py index 8121bfc0a2..656ac38963 100644 --- a/tests/integrations/logging/test_logging.py +++ b/tests/integrations/logging/test_logging.py @@ -370,8 +370,9 @@ def test_logging_errors(sentry_init, capture_envelopes): assert logs[1]["severity_text"] == "error" assert logs[1]["attributes"]["sentry.message.template"] == "error is %s" - assert ( - logs[1]["attributes"]["sentry.message.parameter.0"] == "Exception('test exc 2')" + assert logs[1]["attributes"]["sentry.message.parameter.0"] in ( + "Exception('test exc 2')", + "Exception('test exc 2',)", # py3.6 ) assert "code.line.number" in logs[1]["attributes"] From 0b15f29f264738c250c2c672a05944b8e452b00a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 13:26:53 +0200 Subject: [PATCH 09/24] comment --- sentry_sdk/integrations/loguru.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 1cc9d43e0e..313a158b6a 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -145,6 +145,9 @@ class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): def loguru_sentry_logs_handler(message): + # This is intentionally a callable sink instead of a standard logging handler + # since like this we get direct access to message.record + client = sentry_sdk.get_client() if not client.is_active(): From a377c5451d93c6d9d64e9a19c68a0f21f99297b3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 15:38:10 +0200 Subject: [PATCH 10/24] . --- sentry_sdk/integrations/loguru.py | 2 +- tests/integrations/loguru/test_loguru.py | 219 ++++++++++++++++++++++- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 313a158b6a..140836454a 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -172,7 +172,7 @@ def loguru_sentry_logs_handler(message): 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 + attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] else: attrs["code.file.path"] = record["file"].path diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index 6be09b86dc..837c40e4e7 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -1,8 +1,13 @@ +from unittest.mock import MagicMock, patch + import pytest from loguru import logger +from loguru._recattrs import RecordFile, RecordLevel import sentry_sdk +from sentry_sdk.consts import VERSION from sentry_sdk.integrations.loguru import LoguruIntegration, LoggingLevels +from tests.test_logs import envelopes_to_logs logger.remove(0) # don't print to console @@ -54,7 +59,7 @@ def test_just_log( formatted_message = ( " | " + "{:9}".format(level.name.upper()) - + "| tests.integrations.loguru.test_loguru:test_just_log:52 - test" + + "| tests.integrations.loguru.test_loguru:test_just_log:57 - test" ) if not created_event: @@ -127,3 +132,215 @@ def test_event_format(sentry_init, capture_events, uninstall_integration, reques (event,) = events assert event["logentry"]["message"] == formatted_message + + +def test_sentry_logs_warning( + sentry_init, capture_envelopes, uninstall_integration, request +): + """We should capture warning logs.""" + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + logger.warning("this is {} a {}", "just", "template") + + sentry_sdk.get_client().flush() + logs = envelopes_to_logs(envelopes) + + attrs = logs[0]["attributes"] + # no access to pre-formatted message atm, so template is already filled in + assert attrs["sentry.message.template"] == "this is just a template" + assert "code.file.path" in attrs + assert "code.line.number" in attrs + assert attrs["logger.name"] == "tests.integrations.loguru.test_loguru" + assert attrs["sentry.environment"] == "production" + assert attrs["sentry.origin"] == "auto.logger.loguru" + assert logs[0]["severity_number"] == 13 + assert logs[0]["severity_text"] == "warn" + + +def test_sentry_logs_debug( + sentry_init, capture_envelopes, uninstall_integration, request +): + """We don't capture debug logs by default.""" + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + logger.debug("this is %s a template %s", "1", "2") + sentry_sdk.get_client().flush() + + assert len(envelopes) == 0 + + +def test_no_log_infinite_loop( + sentry_init, capture_envelopes, uninstall_integration, request +): + """ + In debug mode, there should be no infinite loops even when a low log level is set. + """ + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init( + _experiments={"enable_logs": True}, + integrations=[LoguruIntegration(sentry_logs_level=LoggingLevels.DEBUG)], + debug=True, + ) + envelopes = capture_envelopes() + + logger.debug("this is %s a template %s", "1", "2") + sentry_sdk.get_client().flush() + + assert len(envelopes) == 1 + + +def test_logging_errors(sentry_init, capture_envelopes, uninstall_integration, request): + """We're able to log errors without erroring.""" + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + logger.error(Exception("test exc 1")) + logger.error("error is %s", Exception("test exc 2")) + sentry_sdk.get_client().flush() + + error_event_1 = envelopes[0].items[0].payload.json + assert error_event_1["level"] == "error" + error_event_2 = envelopes[1].items[0].payload.json + assert error_event_2["level"] == "error" + + logs = envelopes_to_logs(envelopes) + assert logs[0]["severity_text"] == "error" + assert logs[0]["attributes"]["sentry.message.template"] == "test exc 1" + assert "code.line.number" in logs[0]["attributes"] + + assert logs[1]["severity_text"] == "error" + assert logs[1]["attributes"]["sentry.message.template"] == "error is %s" + assert "code.line.number" in logs[1]["attributes"] + + assert len(logs) == 2 + + +def test_log_strips_project_root( + sentry_init, capture_envelopes, uninstall_integration, request +): + """ + The python logger should strip project roots from the log record path + """ + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init( + _experiments={"enable_logs": True}, + project_root="/custom/test", + ) + envelopes = capture_envelopes() + + class FakeMessage: + def __init__(self, *args, **kwargs): + pass + + @property + def record(self): + return { + "elapsed": MagicMock(), + "exception": None, + "file": RecordFile(name="app.py", path="/custom/test/blah/path.py"), + "function": "", + "level": RecordLevel(name="ERROR", no=20, icon=""), + "line": 35, + "message": "some message", + "module": "app", + "name": "__main__", + "process": MagicMock(), + "thread": MagicMock(), + "time": MagicMock(), + "extra": MagicMock(), + } + + @record.setter + def record(self, val): + pass + + with patch("loguru._handler.Message", FakeMessage): + logger.error("some message") + + sentry_sdk.get_client().flush() + + logs = envelopes_to_logs(envelopes) + assert len(logs) == 1 + attrs = logs[0]["attributes"] + assert attrs["code.file.path"] == "blah/path.py" + + +def test_logger_with_all_attributes( + sentry_init, capture_envelopes, uninstall_integration, request +): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + logger.warning("log #{}", 1) + sentry_sdk.get_client().flush() + + logs = envelopes_to_logs(envelopes) + + attributes = logs[0]["attributes"] + + assert "process.pid" in attributes + assert isinstance(attributes["process.pid"], int) + del attributes["process.pid"] + + assert "sentry.release" in attributes + assert isinstance(attributes["sentry.release"], str) + del attributes["sentry.release"] + + assert "server.address" in attributes + assert isinstance(attributes["server.address"], str) + del attributes["server.address"] + + assert "thread.id" in attributes + assert isinstance(attributes["thread.id"], int) + del attributes["thread.id"] + + assert "code.file.path" in attributes + assert isinstance(attributes["code.file.path"], str) + del attributes["code.file.path"] + + assert "code.function.name" in attributes + assert isinstance(attributes["code.function.name"], str) + del attributes["code.function.name"] + + assert "code.line.number" in attributes + assert isinstance(attributes["code.line.number"], int) + del attributes["code.line.number"] + + assert "process.executable.name" in attributes + assert isinstance(attributes["process.executable.name"], str) + del attributes["process.executable.name"] + + assert "thread.name" in attributes + assert isinstance(attributes["thread.name"], str) + del attributes["thread.name"] + + assert attributes.pop("sentry.sdk.name").startswith("sentry.python") + + # Assert on the remaining non-dynamic attributes. + assert attributes == { + "logger.name": "tests.integrations.loguru.test_loguru", + "sentry.origin": "auto.logger.loguru", + "sentry.message.template": "log #1", + "sentry.environment": "production", + "sentry.sdk.version": VERSION, + "sentry.severity_number": 13, + "sentry.severity_text": "warn", + } From e66c0c2f491b0f485313a8dcf9b120e6da3fd9ae Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 16:04:03 +0200 Subject: [PATCH 11/24] mypy --- sentry_sdk/integrations/loguru.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 140836454a..f43916dbab 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -13,12 +13,15 @@ if TYPE_CHECKING: from logging import LogRecord - from typing import Optional, Any, Tuple + from typing import Any, Tuple 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") @@ -68,10 +71,11 @@ def _loguru_level_to_otel(record_level): class LoguruIntegration(Integration): identifier = "loguru" - level = DEFAULT_LEVEL # type: Optional[int] - event_level = DEFAULT_EVENT_LEVEL # type: Optional[int] + level = DEFAULT_LEVEL # type: int + event_level = DEFAULT_EVENT_LEVEL # type: int breadcrumb_format = DEFAULT_FORMAT event_format = DEFAULT_FORMAT + sentry_logs_level = DEFAULT_LEVEL # type: int def __init__( self, @@ -81,7 +85,7 @@ def __init__( event_format=DEFAULT_FORMAT, sentry_logs_level=DEFAULT_LEVEL, ): - # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[int]) -> None + # type: (int, int, str | loguru.FormatFunction, str | loguru.FormatFunction, int) -> None LoguruIntegration.level = level LoguruIntegration.event_level = event_level LoguruIntegration.breadcrumb_format = breadcrumb_format @@ -145,6 +149,7 @@ class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): def loguru_sentry_logs_handler(message): + # type: (Message) -> None # This is intentionally a callable sink instead of a standard logging handler # since like this we get direct access to message.record @@ -165,9 +170,7 @@ def loguru_sentry_logs_handler(message): otel_severity_number, otel_severity_text = _python_level_to_otel(record["level"].no) - attrs = { - "sentry.origin": "auto.logger.loguru", - } + attrs = {"sentry.origin": "auto.logger.loguru"} # type: dict[str, Any] project_root = client.options["project_root"] if record.get("file"): From ac6dd2df704435dc8e29bdafd1f6c25564d049c5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 16:14:04 +0200 Subject: [PATCH 12/24] . --- sentry_sdk/integrations/loguru.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index f43916dbab..dcce79d246 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -166,8 +166,6 @@ def loguru_sentry_logs_handler(message): if record["level"].no < LoguruIntegration.sentry_logs_level: return - scope = sentry_sdk.get_current_scope() - otel_severity_number, otel_severity_text = _python_level_to_otel(record["level"].no) attrs = {"sentry.origin": "auto.logger.loguru"} # type: dict[str, Any] @@ -200,7 +198,6 @@ def loguru_sentry_logs_handler(message): attrs["sentry.message.template"] = record["message"] client._capture_experimental_log( - scope, { "severity_text": otel_severity_text, "severity_number": otel_severity_number, @@ -208,5 +205,5 @@ def loguru_sentry_logs_handler(message): "attributes": attrs, "time_unix_nano": int(record["time"].timestamp() * 1e9), "trace_id": None, - }, + } ) From 88cb4d7df76b8a98c49cbaa3c2e7bb4a822aebc2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Jun 2025 16:33:51 +0200 Subject: [PATCH 13/24] . --- sentry_sdk/integrations/loguru.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index dcce79d246..eaf500bb2b 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -6,7 +6,6 @@ BreadcrumbHandler, EventHandler, _BaseHandler, - _python_level_to_otel, ) from typing import TYPE_CHECKING @@ -166,7 +165,7 @@ def loguru_sentry_logs_handler(message): if record["level"].no < LoguruIntegration.sentry_logs_level: return - otel_severity_number, otel_severity_text = _python_level_to_otel(record["level"].no) + otel_severity_number, otel_severity_text = _loguru_level_to_otel(record["level"].no) attrs = {"sentry.origin": "auto.logger.loguru"} # type: dict[str, Any] From 05ccc595ff281129bff1f140bd83ab6814a1ac29 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Jun 2025 09:40:32 +0200 Subject: [PATCH 14/24] more tests --- sentry_sdk/integrations/loguru.py | 9 ++-- tests/integrations/loguru/test_loguru.py | 61 +++++++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index eaf500bb2b..027ab9a3e2 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from logging import LogRecord - from typing import Any, Tuple + from typing import Any, Optional, Tuple try: import loguru @@ -74,7 +74,7 @@ class LoguruIntegration(Integration): event_level = DEFAULT_EVENT_LEVEL # type: int breadcrumb_format = DEFAULT_FORMAT event_format = DEFAULT_FORMAT - sentry_logs_level = DEFAULT_LEVEL # type: int + sentry_logs_level = DEFAULT_LEVEL # type: Optional[int] def __init__( self, @@ -162,7 +162,10 @@ def loguru_sentry_logs_handler(message): record = message.record - if record["level"].no < LoguruIntegration.sentry_logs_level: + if ( + LoguruIntegration.sentry_logs_level is not None + and record["level"].no < LoguruIntegration.sentry_logs_level + ): return otel_severity_number, otel_severity_text = _loguru_level_to_otel(record["level"].no) diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index 837c40e4e7..56c285205a 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -137,7 +137,6 @@ def test_event_format(sentry_init, capture_events, uninstall_integration, reques def test_sentry_logs_warning( sentry_init, capture_envelopes, uninstall_integration, request ): - """We should capture warning logs.""" uninstall_integration("loguru") request.addfinalizer(logger.remove) @@ -164,7 +163,6 @@ def test_sentry_logs_warning( def test_sentry_logs_debug( sentry_init, capture_envelopes, uninstall_integration, request ): - """We don't capture debug logs by default.""" uninstall_integration("loguru") request.addfinalizer(logger.remove) @@ -177,6 +175,65 @@ def test_sentry_logs_debug( assert len(envelopes) == 0 +def test_sentry_log_levels( + sentry_init, capture_envelopes, uninstall_integration, request +): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init( + integrations=[LoguruIntegration(sentry_logs_level=LoggingLevels.SUCCESS)], + _experiments={"enable_logs": True}, + ) + envelopes = capture_envelopes() + + logger.trace("this is a log") + logger.debug("this is a log") + logger.info("this is a log") + logger.success("this is a log") + logger.warning("this is a log") + logger.error("this is a log") + logger.critical("this is a log") + + sentry_sdk.get_client().flush() + logs = envelopes_to_logs(envelopes) + assert len(logs) == 4 + + assert logs[0]["severity_number"] == 11 + assert logs[0]["severity_text"] == "info" + assert logs[1]["severity_number"] == 13 + assert logs[1]["severity_text"] == "warn" + assert logs[2]["severity_number"] == 17 + assert logs[2]["severity_text"] == "error" + assert logs[3]["severity_number"] == 21 + assert logs[3]["severity_text"] == "fatal" + + +def test_turn_off_sentry_logs( + sentry_init, capture_envelopes, uninstall_integration, request +): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init( + integrations=[LoguruIntegration(sentry_logs_level=None)], + _experiments={"enable_logs": True}, + ) + envelopes = capture_envelopes() + + logger.trace("this is a log") + logger.debug("this is a log") + logger.info("this is a log") + logger.success("this is a log") + logger.warning("this is a log") + logger.error("this is a log") + logger.critical("this is a log") + + sentry_sdk.get_client().flush() + logs = envelopes_to_logs(envelopes) + assert len(logs) == 0 + + def test_no_log_infinite_loop( sentry_init, capture_envelopes, uninstall_integration, request ): From 2c8be6ccede46e1bca81c9d07ee5753c2e9560da Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Jun 2025 10:17:25 +0200 Subject: [PATCH 15/24] typing --- sentry_sdk/integrations/loguru.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 027ab9a3e2..df325ee585 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -70,8 +70,8 @@ def _loguru_level_to_otel(record_level): class LoguruIntegration(Integration): identifier = "loguru" - level = DEFAULT_LEVEL # type: int - event_level = DEFAULT_EVENT_LEVEL # type: int + 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] @@ -84,7 +84,7 @@ def __init__( event_format=DEFAULT_FORMAT, sentry_logs_level=DEFAULT_LEVEL, ): - # type: (int, int, str | loguru.FormatFunction, str | loguru.FormatFunction, int) -> None + # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, int) -> None LoguruIntegration.level = level LoguruIntegration.event_level = event_level LoguruIntegration.breadcrumb_format = breadcrumb_format From fab0fba765204201eb9471c005523b6a91e96123 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Jun 2025 10:22:18 +0200 Subject: [PATCH 16/24] more typing --- sentry_sdk/integrations/loguru.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index df325ee585..7baecd22c2 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -84,7 +84,7 @@ def __init__( event_format=DEFAULT_FORMAT, sentry_logs_level=DEFAULT_LEVEL, ): - # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, int) -> 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 From a6b083b54407853e8a4504dca9c249bad283cadf Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Jun 2025 10:23:36 +0200 Subject: [PATCH 17/24] . --- sentry_sdk/integrations/loguru.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 7baecd22c2..37417dad25 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -150,8 +150,7 @@ class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): def loguru_sentry_logs_handler(message): # type: (Message) -> None # This is intentionally a callable sink instead of a standard logging handler - # since like this we get direct access to message.record - + # since otherwise we wouldn't get direct access to message.record client = sentry_sdk.get_client() if not client.is_active(): From 1a35e02799de30a0c8d657b1ad710cd448fd2e97 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Jun 2025 10:24:40 +0200 Subject: [PATCH 18/24] . --- sentry_sdk/integrations/loguru.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 37417dad25..c7d1ae1889 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -162,8 +162,8 @@ def loguru_sentry_logs_handler(message): record = message.record if ( - LoguruIntegration.sentry_logs_level is not None - and record["level"].no < LoguruIntegration.sentry_logs_level + LoguruIntegration.sentry_logs_level is None + or record["level"].no < LoguruIntegration.sentry_logs_level ): return From f26fc8c3dd716fcb8cdb439e6b39eef036547fa7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Jun 2025 10:30:22 +0200 Subject: [PATCH 19/24] another test --- tests/integrations/loguru/test_loguru.py | 29 ++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index 56c285205a..2bbf1ee87e 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -209,7 +209,7 @@ def test_sentry_log_levels( assert logs[3]["severity_text"] == "fatal" -def test_turn_off_sentry_logs( +def test_disable_loguru_logs( sentry_init, capture_envelopes, uninstall_integration, request ): uninstall_integration("loguru") @@ -234,6 +234,30 @@ def test_turn_off_sentry_logs( assert len(logs) == 0 +def test_disable_sentry_logs( + sentry_init, capture_envelopes, uninstall_integration, request +): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init( + _experiments={"enable_logs": False}, + ) + envelopes = capture_envelopes() + + logger.trace("this is a log") + logger.debug("this is a log") + logger.info("this is a log") + logger.success("this is a log") + logger.warning("this is a log") + logger.error("this is a log") + logger.critical("this is a log") + + sentry_sdk.get_client().flush() + logs = envelopes_to_logs(envelopes) + assert len(logs) == 0 + + def test_no_log_infinite_loop( sentry_init, capture_envelopes, uninstall_integration, request ): @@ -288,9 +312,6 @@ def test_logging_errors(sentry_init, capture_envelopes, uninstall_integration, r def test_log_strips_project_root( sentry_init, capture_envelopes, uninstall_integration, request ): - """ - The python logger should strip project roots from the log record path - """ uninstall_integration("loguru") request.addfinalizer(logger.remove) From 8397b1092db005b3c67eb62064fd95279ae81c96 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Jun 2025 11:05:27 +0200 Subject: [PATCH 20/24] remove template --- sentry_sdk/integrations/loguru.py | 3 --- tests/integrations/loguru/test_loguru.py | 5 ----- 2 files changed, 8 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index c7d1ae1889..404d8ac948 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -195,9 +195,6 @@ def loguru_sentry_logs_handler(message): if record.get("name"): attrs["logger.name"] = record["name"] - if record["message"]: - attrs["sentry.message.template"] = record["message"] - client._capture_experimental_log( { "severity_text": otel_severity_text, diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index 2bbf1ee87e..d6f4d92865 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -149,8 +149,6 @@ def test_sentry_logs_warning( logs = envelopes_to_logs(envelopes) attrs = logs[0]["attributes"] - # no access to pre-formatted message atm, so template is already filled in - assert attrs["sentry.message.template"] == "this is just a template" assert "code.file.path" in attrs assert "code.line.number" in attrs assert attrs["logger.name"] == "tests.integrations.loguru.test_loguru" @@ -299,11 +297,9 @@ def test_logging_errors(sentry_init, capture_envelopes, uninstall_integration, r logs = envelopes_to_logs(envelopes) assert logs[0]["severity_text"] == "error" - assert logs[0]["attributes"]["sentry.message.template"] == "test exc 1" assert "code.line.number" in logs[0]["attributes"] assert logs[1]["severity_text"] == "error" - assert logs[1]["attributes"]["sentry.message.template"] == "error is %s" assert "code.line.number" in logs[1]["attributes"] assert len(logs) == 2 @@ -416,7 +412,6 @@ def test_logger_with_all_attributes( assert attributes == { "logger.name": "tests.integrations.loguru.test_loguru", "sentry.origin": "auto.logger.loguru", - "sentry.message.template": "log #1", "sentry.environment": "production", "sentry.sdk.version": VERSION, "sentry.severity_number": 13, From e3670f9f89b298acac8cf920d3920efeae0121ac Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 10 Jun 2025 10:44:53 +0200 Subject: [PATCH 21/24] Apply suggestions from code review Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- sentry_sdk/integrations/loguru.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 404d8ac948..ded32e2f6f 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from logging import LogRecord - from typing import Any, Optional, Tuple + from typing import Any, Optional try: import loguru @@ -51,7 +51,7 @@ class LoggingLevels(enum.IntEnum): def _loguru_level_to_otel(record_level): - # type: (int) -> Tuple[int, str] + # type: (int) -> tuple[int, str] for py_level, otel_severity_number, otel_severity_text in [ (LoggingLevels.CRITICAL, 21, "fatal"), (LoggingLevels.ERROR, 17, "error"), @@ -74,7 +74,7 @@ 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] + sentry_logs_level = DEFAULT_LEVEL # type: Optional[LoggingLevels] def __init__( self, @@ -84,7 +84,7 @@ def __init__( event_format=DEFAULT_FORMAT, sentry_logs_level=DEFAULT_LEVEL, ): - # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[int]) -> None + # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[LoggingLevels]) -> None LoguruIntegration.level = level LoguruIntegration.event_level = event_level LoguruIntegration.breadcrumb_format = breadcrumb_format From eb8e5a46fbd1ee906fe2a0d01e036f29d08b0f58 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 10 Jun 2025 13:51:55 +0200 Subject: [PATCH 22/24] Move otel severity mapping logic --- sentry_sdk/integrations/logging.py | 32 ++++++++++++++---------------- sentry_sdk/integrations/loguru.py | 31 +++++++++++++---------------- sentry_sdk/logger.py | 27 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 44b31d7599..756a35a900 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -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, @@ -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 @@ -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. @@ -312,21 +323,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. @@ -352,7 +348,9 @@ def emit(self, record): def _capture_log_from_record(self, client, record): # type: (BaseClient, LogRecord) -> None - 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" diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index ded32e2f6f..67583046da 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -7,6 +7,7 @@ EventHandler, _BaseHandler, ) +from sentry_sdk.logger import _log_level_to_otel from typing import TYPE_CHECKING @@ -49,22 +50,16 @@ class LoggingLevels(enum.IntEnum): "CRITICAL": "CRITICAL", } - -def _loguru_level_to_otel(record_level): - # type: (int) -> tuple[int, str] - 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 - - return 0, "default" +# 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): @@ -167,7 +162,9 @@ def loguru_sentry_logs_handler(message): ): return - otel_severity_number, otel_severity_text = _loguru_level_to_otel(record["level"].no) + 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] diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index 2f5e859533..c6a2ce27b7 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -6,6 +6,16 @@ from sentry_sdk import get_client from sentry_sdk.utils import safe_repr +OTEL_RANGES = [ + # ((severity level range), severity text) + ((1, 4), "trace"), + ((5, 8), "debug"), + ((9, 12), "info"), + ((13, 16), "warn"), + ((17, 20), "error"), + ((21, 24), "fatal"), +] + def _capture_log(severity_text, severity_number, template, **kwargs): # type: (str, int, str, **Any) -> None @@ -52,3 +62,20 @@ def _capture_log(severity_text, severity_number, template, **kwargs): warning = functools.partial(_capture_log, "warn", 13) error = functools.partial(_capture_log, "error", 17) fatal = functools.partial(_capture_log, "fatal", 21) + + +def _otel_severity_text(otel_severity_number): + for (lower, upper), severity in OTEL_RANGES: + if lower <= otel_severity_number <= upper: + return severity + + return "default" + + +def _log_level_to_otel(level, mapping): + # type: (int, dict[int, int]) -> tuple[int, str] + for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): + if level >= py_level: + return otel_severity_number, _otel_severity_text(otel_severity_number) + + return 0, "default" From cfac8d72b892040ab89626cc4e0fe2b4db3e196e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 10 Jun 2025 14:06:09 +0200 Subject: [PATCH 23/24] mypy --- sentry_sdk/integrations/loguru.py | 4 ++-- sentry_sdk/logger.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 67583046da..df3ecf161a 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -69,7 +69,7 @@ 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[LoggingLevels] + sentry_logs_level = DEFAULT_LEVEL # type: Optional[int] def __init__( self, @@ -79,7 +79,7 @@ def __init__( event_format=DEFAULT_FORMAT, sentry_logs_level=DEFAULT_LEVEL, ): - # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[LoggingLevels]) -> 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 diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index c6a2ce27b7..c18cf91ff2 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -8,6 +8,7 @@ OTEL_RANGES = [ # ((severity level range), severity text) + # https://opentelemetry.io/docs/specs/otel/logs/data-model ((1, 4), "trace"), ((5, 8), "debug"), ((9, 12), "info"), @@ -65,6 +66,7 @@ def _capture_log(severity_text, severity_number, template, **kwargs): def _otel_severity_text(otel_severity_number): + # type: (int) -> str for (lower, upper), severity in OTEL_RANGES: if lower <= otel_severity_number <= upper: return severity @@ -73,7 +75,7 @@ def _otel_severity_text(otel_severity_number): def _log_level_to_otel(level, mapping): - # type: (int, dict[int, int]) -> tuple[int, str] + # type: (int, dict[Any, int]) -> tuple[int, str] for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): if level >= py_level: return otel_severity_number, _otel_severity_text(otel_severity_number) From 61f6395f0e71e8ce570ad6999564e57402ad096d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 10 Jun 2025 16:16:40 +0200 Subject: [PATCH 24/24] add test --- tests/integrations/loguru/test_loguru.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index d6f4d92865..20d3230b49 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -354,6 +354,55 @@ def record(self, val): assert attrs["code.file.path"] == "blah/path.py" +def test_log_keeps_full_path_if_not_in_project_root( + sentry_init, capture_envelopes, uninstall_integration, request +): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + + sentry_init( + _experiments={"enable_logs": True}, + project_root="/custom/test", + ) + envelopes = capture_envelopes() + + class FakeMessage: + def __init__(self, *args, **kwargs): + pass + + @property + def record(self): + return { + "elapsed": MagicMock(), + "exception": None, + "file": RecordFile(name="app.py", path="/blah/path.py"), + "function": "", + "level": RecordLevel(name="ERROR", no=20, icon=""), + "line": 35, + "message": "some message", + "module": "app", + "name": "__main__", + "process": MagicMock(), + "thread": MagicMock(), + "time": MagicMock(), + "extra": MagicMock(), + } + + @record.setter + def record(self, val): + pass + + with patch("loguru._handler.Message", FakeMessage): + logger.error("some message") + + sentry_sdk.get_client().flush() + + logs = envelopes_to_logs(envelopes) + assert len(logs) == 1 + attrs = logs[0]["attributes"] + assert attrs["code.file.path"] == "/blah/path.py" + + def test_logger_with_all_attributes( sentry_init, capture_envelopes, uninstall_integration, request ):