diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 5899af00d9..3cc97978af 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -24,6 +24,34 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - The UnraisableHookIntegration is now enabled by default. - We now don't suppress chained exceptions in the ASGI and asyncio integrations by default. The related `suppress_asgi_chained_exceptions` experimental option was removed. +### Loguru + +- The Loguru logging integration is not auto-enabled by default anymore if you have Loguru installed. To continue using it, add it to the `integrations` list in your `sentry_sdk.init()`: + + ```python + import sentry_sdk + from sentry_sdk.integrations.loguru import LoguruIntegration + + sentry_sdk.init( + integrations=[ + LoguruIntegration(), + ] + ) + ``` + +- The `level` integration option is now called `breadcrumb_level`. +- The `sentry_logs_level` integration option is now called `level`. +- The `capture_sentry_logs` option was removed. Use `level=None` to disable log capture. +- When you enable the integration by adding `LoguruIntegration` to your `sentry_sdk.init()`, it'll start capturing Sentry logs and breadcrumbs. Creating events from logs can be enabled by providing additional integration options. + + | Old name | New name | Old default | New default | Description | + | --- | --- | --- | --- | --- | + | `level` | `breadcrumb_level` | `INFO` | `INFO` | Captures logs of that level and higher as breadcrumbs. | + | `event_level` | `event_level` | `ERROR` | `None` | Captures logs of that level and higher as events. | + | `sentry_logs_level` | `level` | `INFO` | `INFO` | Captures logs of that level and higher as Sentry logs. | + | `capture_sentry_logs` | removed | `False` | n/a | Allows to opt out of instrumenting logs as Sentry logs. Use `level` (previously `sentry_logs_level`) to adjust what should be captured instead. | + + ## Removed - The SDK no longer supports Python 3.6. The oldest supported version is now 3.7. diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 0f674cb4bf..316d343765 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -96,7 +96,6 @@ def iter_default_integrations( "sentry_sdk.integrations.langchain.LangchainIntegration", "sentry_sdk.integrations.langgraph.LanggraphIntegration", "sentry_sdk.integrations.litestar.LitestarIntegration", - "sentry_sdk.integrations.loguru.LoguruIntegration", "sentry_sdk.integrations.mcp.MCPIntegration", "sentry_sdk.integrations.openai.OpenAIIntegration", "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index e6707fd429..5b5645b31a 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from logging import LogRecord - from typing import Any, Optional, Union + from typing import Any, Optional try: import loguru @@ -27,12 +27,6 @@ raise DidNotEnable("LOGURU is not installed or incompatible") -_SENTINEL = object() - - -_SENTINEL = object() - - class LoggingLevels(enum.IntEnum): TRACE = 5 DEBUG = 10 @@ -73,27 +67,24 @@ class LoguruIntegration(Integration): identifier = "loguru" level: "Optional[int]" = DEFAULT_LEVEL - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL - breadcrumb_format = DEFAULT_FORMAT event_format = DEFAULT_FORMAT - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL - capture_sentry_logs: "Optional[Union[bool, object]]" = _SENTINEL + event_level: "Optional[int]" = None + breadcrumb_level: "Optional[int]" = DEFAULT_LEVEL + breadcrumb_format = DEFAULT_FORMAT def __init__( self, level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + event_level: "Optional[int]" = None, event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - capture_sentry_logs: "Optional[Union[bool, object]]" = _SENTINEL, + breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + breadcrumb_level: "Optional[int]" = DEFAULT_LEVEL, ) -> 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 - LoguruIntegration.capture_sentry_logs = capture_sentry_logs + LoguruIntegration.breadcrumb_level = breadcrumb_level + LoguruIntegration.breadcrumb_format = breadcrumb_format @staticmethod def setup_once() -> None: @@ -102,9 +93,8 @@ def setup_once() -> None: if LoguruIntegration.level is not None: logger.add( - LoguruBreadcrumbHandler(level=LoguruIntegration.level), + loguru_handler, level=LoguruIntegration.level, - format=LoguruIntegration.breadcrumb_format, ) if LoguruIntegration.event_level is not None: @@ -114,10 +104,11 @@ def setup_once() -> None: format=LoguruIntegration.event_format, ) - if LoguruIntegration.sentry_logs_level is not None: + if LoguruIntegration.breadcrumb_level is not None: logger.add( - loguru_sentry_logs_handler, - level=LoguruIntegration.sentry_logs_level, + LoguruBreadcrumbHandler(level=LoguruIntegration.breadcrumb_level), + level=LoguruIntegration.breadcrumb_level, + format=LoguruIntegration.breadcrumb_format, ) @@ -151,22 +142,16 @@ class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): pass -def loguru_sentry_logs_handler(message: "Message") -> None: +def loguru_handler(message: "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 LoguruIntegration.capture_sentry_logs is not True: - return - record = message.record - if ( - LoguruIntegration.sentry_logs_level is None - or record["level"].no < LoguruIntegration.sentry_logs_level - ): + if LoguruIntegration.level is None or record["level"].no < LoguruIntegration.level: return otel_severity_number, otel_severity_text = _log_level_to_otel( diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index e82f845c2c..20e90cffcf 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -12,31 +12,72 @@ logger.remove(0) # don't print to console +def test_defaults(sentry_init, capture_events): + """No logs, events, breadcrumbs captured by default.""" + sentry_init() + + events = capture_events() + + logger.error("test") + + assert not events + + +def test_defaults_enabled(sentry_init, capture_items): + """Logs and breadcrumbs captured by default if integration is enabled.""" + sentry_init(integrations=[LoguruIntegration()]) + + items = capture_items() + + logger.error("test") + sentry_sdk.capture_message("oh no") + + sentry_sdk.flush() + + events = [item for item in items if item.type == "event"] + logs = [item for item in items if item.type == "log"] + + assert len(events) == 1 + assert events[0].payload["message"] == "oh no" + + breadcrumbs = events[0].payload["breadcrumbs"]["values"] + assert len(breadcrumbs) == 1 + assert breadcrumbs[0]["level"] == "error" + assert breadcrumbs[0]["category"] == "tests.integrations.loguru.test_loguru" + + assert len(logs) == 1 + assert logs[0].payload["body"] == "test" + assert logs[0].payload["level"] == "error" + assert logs[0].payload["attributes"]["sentry.origin"] == "auto.log.loguru" + + @pytest.mark.parametrize( - "level,created_event,expected_sentry_level", + "level,created_event,created_breadcrumb,created_log,expected_event_level,expected_log_level", [ - # None - no breadcrumb - # False - no event - # True - event created - (LoggingLevels.TRACE, None, "debug"), - (LoggingLevels.DEBUG, None, "debug"), - (LoggingLevels.INFO, False, "info"), - (LoggingLevels.SUCCESS, False, "info"), - (LoggingLevels.WARNING, False, "warning"), - (LoggingLevels.ERROR, True, "error"), - (LoggingLevels.CRITICAL, True, "critical"), + (LoggingLevels.TRACE, False, False, False, "debug", "trace"), + (LoggingLevels.DEBUG, False, False, False, "debug", "debug"), + (LoggingLevels.INFO, False, True, True, "info", "info"), + (LoggingLevels.SUCCESS, False, True, True, "info", "info"), + (LoggingLevels.WARNING, False, True, True, "warning", "warn"), + (LoggingLevels.ERROR, True, True, True, "error", "error"), + (LoggingLevels.CRITICAL, True, True, True, "critical", "fatal"), ], ) @pytest.mark.parametrize("disable_breadcrumbs", [True, False]) @pytest.mark.parametrize("disable_events", [True, False]) -def test_just_log( +@pytest.mark.parametrize("disable_logs", [True, False]) +def test_levels( sentry_init, - capture_events, + capture_items, level, created_event, - expected_sentry_level, + created_breadcrumb, + created_log, + expected_event_level, + expected_log_level, disable_breadcrumbs, disable_events, + disable_logs, uninstall_integration, request, ): @@ -46,46 +87,52 @@ def test_just_log( sentry_init( integrations=[ LoguruIntegration( - level=None if disable_breadcrumbs else LoggingLevels.INFO.value, + breadcrumb_level=None + if disable_breadcrumbs + else LoggingLevels.INFO.value, event_level=None if disable_events else LoggingLevels.ERROR.value, + level=None if disable_logs else LoggingLevels.INFO.value, ) ], - default_integrations=False, ) - events = capture_events() + items = capture_items() getattr(logger, level.name.lower())("test") + sentry_sdk.flush() + expected_pattern = ( r" \| " + r"{:9}".format(level.name.upper()) - + r"\| tests\.integrations\.loguru\.test_loguru:test_just_log:\d+ - test" + + r"\| tests\.integrations\.loguru\.test_loguru:test_levels:\d+ - test" ) - if not created_event: + breadcrumbs = sentry_sdk.get_isolation_scope()._breadcrumbs + if not disable_breadcrumbs and created_breadcrumb: + (breadcrumb,) = breadcrumbs + assert breadcrumb["level"] == expected_event_level + assert breadcrumb["category"] == "tests.integrations.loguru.test_loguru" + assert re.fullmatch(expected_pattern, breadcrumb["message"][23:]) + else: + assert not breadcrumbs + + events = [item for item in items if item.type == "event"] + if disable_events or not created_event: assert not events - breadcrumbs = sentry_sdk.get_isolation_scope()._breadcrumbs - if ( - not disable_breadcrumbs and created_event is not None - ): # not None == not TRACE or DEBUG level - (breadcrumb,) = breadcrumbs - assert breadcrumb["level"] == expected_sentry_level - assert breadcrumb["category"] == "tests.integrations.loguru.test_loguru" - assert re.fullmatch(expected_pattern, breadcrumb["message"][23:]) - else: - assert not breadcrumbs - - return + logs = [item for item in items if item.type == "log"] + if disable_logs or not created_log: + assert not logs - if disable_events: - assert not events - return + for event in events: + assert event.payload["level"] == expected_event_level + assert event.payload["logger"] == "tests.integrations.loguru.test_loguru" + assert re.fullmatch(expected_pattern, event.payload["logentry"]["message"][23:]) - (event,) = events - assert event["level"] == expected_sentry_level - assert event["logger"] == "tests.integrations.loguru.test_loguru" - assert re.fullmatch(expected_pattern, event["logentry"]["message"][23:]) + for log in logs: + assert log.payload["level"] == expected_log_level + assert log.payload["body"] == "test" + assert log.payload["attributes"]["sentry.origin"] == "auto.log.loguru" def test_breadcrumb_format(sentry_init, capture_events, uninstall_integration, request): @@ -95,12 +142,12 @@ def test_breadcrumb_format(sentry_init, capture_events, uninstall_integration, r sentry_init( integrations=[ LoguruIntegration( - level=LoggingLevels.INFO.value, - event_level=None, + breadcrumb_level=LoggingLevels.INFO.value, breadcrumb_format="{message}", + level=None, + event_level=None, ) ], - default_integrations=False, ) logger.info("test") @@ -119,11 +166,11 @@ def test_event_format(sentry_init, capture_events, uninstall_integration, reques integrations=[ LoguruIntegration( level=None, + breadcrumb_level=None, event_level=LoggingLevels.ERROR.value, event_format="{message}", ) ], - default_integrations=False, ) events = capture_events() @@ -140,7 +187,7 @@ def test_sentry_logs_warning( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") logger.warning("this is {} a {}", "just", "template") @@ -164,7 +211,7 @@ def test_sentry_logs_debug( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) envelopes = capture_envelopes() logger.debug("this is %s a template %s", "1", "2") @@ -178,11 +225,7 @@ def test_sentry_log_levels(sentry_init, capture_items, uninstall_integration, re request.addfinalizer(logger.remove) sentry_init( - integrations=[ - LoguruIntegration( - capture_sentry_logs=True, sentry_logs_level=LoggingLevels.SUCCESS - ) - ], + integrations=[LoguruIntegration(level=LoggingLevels.SUCCESS)], ) items = capture_items("log") @@ -215,9 +258,7 @@ def test_disable_loguru_logs( request.addfinalizer(logger.remove) sentry_init( - integrations=[ - LoguruIntegration(capture_sentry_logs=True, sentry_logs_level=None) - ], + integrations=[LoguruIntegration(level=None)], ) items = capture_items("log") @@ -256,30 +297,6 @@ def test_disable_sentry_logs_by_default( assert len(logs) == 0 -def test_disable_sentry_logs_explicitly( - sentry_init, capture_items, uninstall_integration, request -): - uninstall_integration("loguru") - request.addfinalizer(logger.remove) - - sentry_init( - integrations=[LoguruIntegration(capture_sentry_logs=False)], - ) - items = capture_items("log") - - 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 = [item.payload for item in items] - assert len(logs) == 0 - - def test_no_log_infinite_loop( sentry_init, capture_envelopes, uninstall_integration, request ): @@ -290,11 +307,7 @@ def test_no_log_infinite_loop( request.addfinalizer(logger.remove) sentry_init( - integrations=[ - LoguruIntegration( - capture_sentry_logs=True, sentry_logs_level=LoggingLevels.DEBUG - ) - ], + integrations=[LoguruIntegration(level=LoggingLevels.DEBUG)], debug=True, ) envelopes = capture_envelopes() @@ -312,7 +325,13 @@ def test_logging_errors( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init( + integrations=[ + LoguruIntegration( + level=LoggingLevels.INFO.value, event_level=LoggingLevels.ERROR.value + ) + ] + ) envelopes = capture_envelopes() items = capture_items("log") @@ -342,7 +361,7 @@ def test_log_strips_project_root( request.addfinalizer(logger.remove) sentry_init( - integrations=[LoguruIntegration(capture_sentry_logs=True)], + integrations=[LoguruIntegration()], project_root="/custom/test", ) items = capture_items("log") @@ -391,7 +410,7 @@ def test_log_keeps_full_path_if_not_in_project_root( request.addfinalizer(logger.remove) sentry_init( - integrations=[LoguruIntegration(capture_sentry_logs=True)], + integrations=[LoguruIntegration()], project_root="/custom/test", ) items = capture_items("log") @@ -439,7 +458,7 @@ def test_logger_with_all_attributes( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") logger.warning("log #{}", 1) @@ -511,7 +530,7 @@ def test_logger_capture_parameters_from_args( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") logger.warning("Task ID: {}", 123) @@ -530,7 +549,7 @@ def test_logger_capture_parameters_from_kwargs( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") logger.warning("Task ID: {task_id}", task_id=123) @@ -549,7 +568,7 @@ def test_logger_capture_parameters_from_contextualize( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") with logger.contextualize(task_id=123): @@ -569,7 +588,7 @@ def test_logger_capture_parameters_from_bind( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") logger.bind(task_id=123).warning("Log") @@ -587,7 +606,7 @@ def test_logger_capture_parameters_from_patch( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") logger.patch(lambda record: record["extra"].update(task_id=123)).warning("Log") @@ -605,7 +624,7 @@ def test_no_parameters_no_template( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(integrations=[LoguruIntegration(capture_sentry_logs=True)]) + sentry_init(integrations=[LoguruIntegration()]) items = capture_items("log") logger.warning("Logging a hardcoded warning")