diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index dca39beab8..5d584a5537 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -23,6 +23,8 @@ handle_in_app, is_gevent, logger, + get_before_send_log, + has_logs_enabled, ) from sentry_sdk.serializer import serialize from sentry_sdk.tracing import trace @@ -382,7 +384,8 @@ def _capture_envelope(envelope): ) self.log_batcher = None - if experiments.get("enable_logs", False): + + if has_logs_enabled(self.options): from sentry_sdk._log_batcher import LogBatcher self.log_batcher = LogBatcher(capture_func=_capture_envelope) @@ -898,9 +901,8 @@ def capture_event( return return_value def _capture_experimental_log(self, log): - # type: (Log) -> None - logs_enabled = self.options["_experiments"].get("enable_logs", False) - if not logs_enabled: + # type: (Optional[Log]) -> None + if not has_logs_enabled(self.options) or log is None: return current_scope = sentry_sdk.get_current_scope() @@ -955,9 +957,10 @@ def _capture_experimental_log(self, log): f'[Sentry Logs] [{log.get("severity_text")}] {log.get("body")}' ) - before_send_log = self.options["_experiments"].get("before_send_log") + before_send_log = get_before_send_log(self.options) if before_send_log is not None: log = before_send_log(log, {}) + if log is None: return diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3ae33b6a94..b56c0ba2dd 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -798,6 +798,8 @@ def __init__( custom_repr=None, # type: Optional[Callable[..., Optional[str]]] add_full_stack=DEFAULT_ADD_FULL_STACK, # type: bool max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int] + enable_logs=False, # type: bool + before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]] ): # type: (...) -> None """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. @@ -1168,7 +1170,6 @@ def __init__( :param profile_session_sample_rate: - :param enable_tracing: :param propagate_traces: @@ -1179,6 +1180,14 @@ def __init__( :param instrumenter: + :param enable_logs: Set `enable_logs` to True to enable the SDK to emit + Sentry logs. Defaults to False. + + :param before_send_log: An optional function to modify or filter out logs + before they're sent to Sentry. Any modifications to the log in this + function will be retained. If the function returns None, the log will + not be sent to Sentry. + :param _experiments: """ pass diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index a50512f622..15ff2ed233 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -12,6 +12,7 @@ event_from_exception, current_stacktrace, capture_internal_exceptions, + has_logs_enabled, ) from sentry_sdk.integrations import Integration @@ -344,7 +345,7 @@ def emit(self, record): if not client.is_active(): return - if not client.options["_experiments"].get("enable_logs", False): + if not has_logs_enabled(client.options): return self._capture_log_from_record(client, record) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index df3ecf161a..b910b9a407 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -8,6 +8,7 @@ _BaseHandler, ) from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import has_logs_enabled from typing import TYPE_CHECKING @@ -151,7 +152,7 @@ def loguru_sentry_logs_handler(message): if not client.is_active(): return - if not client.options["_experiments"].get("enable_logs", False): + if not has_logs_enabled(client.options): return record = message.record diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 9c6f2cfc3b..b0f3fa4a4c 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -59,7 +59,7 @@ from gevent.hub import Hub - from sentry_sdk._types import Event, ExcInfo + from sentry_sdk._types import Event, ExcInfo, Log, Hint P = ParamSpec("P") R = TypeVar("R") @@ -1984,3 +1984,24 @@ def serialize_item(item): return json.dumps(serialized, default=str) except Exception: return str(data) + + +def has_logs_enabled(options): + # type: (Optional[dict[str, Any]]) -> bool + if options is None: + return False + + return bool( + options.get("enable_logs", False) + or options["_experiments"].get("enable_logs", False) + ) + + +def get_before_send_log(options): + # type: (Optional[dict[str, Any]]) -> Optional[Callable[[Log, Hint], Optional[Log]]] + if options is None: + return None + + return options.get("before_send_log") or options["_experiments"].get( + "before_send_log" + ) diff --git a/tests/integrations/logging/test_logging.py b/tests/integrations/logging/test_logging.py index 6ef4ae371b..7ecdf42500 100644 --- a/tests/integrations/logging/test_logging.py +++ b/tests/integrations/logging/test_logging.py @@ -304,7 +304,7 @@ 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}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger") @@ -329,7 +329,7 @@ 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}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger") @@ -344,7 +344,7 @@ 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}, + enable_logs=True, integrations=[LoggingIntegration(sentry_logs_level=logging.DEBUG)], debug=True, ) @@ -361,7 +361,7 @@ 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}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger") @@ -396,7 +396,7 @@ 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}, + enable_logs=True, project_root="/custom/test", ) envelopes = capture_envelopes() @@ -425,7 +425,7 @@ 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}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger") @@ -498,7 +498,7 @@ def test_sentry_logs_named_parameters(sentry_init, capture_envelopes): """ The python logger module should capture named parameters from dictionary arguments in Sentry logs. """ - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger") @@ -543,7 +543,7 @@ def test_sentry_logs_named_parameters_complex_values(sentry_init, capture_envelo """ The python logger module should handle complex values in named parameters using safe_repr. """ - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger") diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index c120d1d7e2..38093d24cb 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -141,7 +141,7 @@ def test_sentry_logs_warning( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() logger.warning("this is {} a {}", "just", "template") @@ -165,7 +165,7 @@ def test_sentry_logs_debug( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() logger.debug("this is %s a template %s", "1", "2") @@ -182,7 +182,7 @@ def test_sentry_log_levels( sentry_init( integrations=[LoguruIntegration(sentry_logs_level=LoggingLevels.SUCCESS)], - _experiments={"enable_logs": True}, + enable_logs=True, ) envelopes = capture_envelopes() @@ -216,7 +216,7 @@ def test_disable_loguru_logs( sentry_init( integrations=[LoguruIntegration(sentry_logs_level=None)], - _experiments={"enable_logs": True}, + enable_logs=True, ) envelopes = capture_envelopes() @@ -267,7 +267,7 @@ def test_no_log_infinite_loop( request.addfinalizer(logger.remove) sentry_init( - _experiments={"enable_logs": True}, + enable_logs=True, integrations=[LoguruIntegration(sentry_logs_level=LoggingLevels.DEBUG)], debug=True, ) @@ -284,7 +284,7 @@ def test_logging_errors(sentry_init, capture_envelopes, uninstall_integration, r uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() logger.error(Exception("test exc 1")) @@ -313,7 +313,7 @@ def test_log_strips_project_root( request.addfinalizer(logger.remove) sentry_init( - _experiments={"enable_logs": True}, + enable_logs=True, project_root="/custom/test", ) envelopes = capture_envelopes() @@ -362,7 +362,7 @@ def test_log_keeps_full_path_if_not_in_project_root( request.addfinalizer(logger.remove) sentry_init( - _experiments={"enable_logs": True}, + enable_logs=True, project_root="/custom/test", ) envelopes = capture_envelopes() @@ -410,7 +410,7 @@ def test_logger_with_all_attributes( uninstall_integration("loguru") request.addfinalizer(logger.remove) - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() logger.warning("log #{}", 1) diff --git a/tests/test_logs.py b/tests/test_logs.py index a2f412dcb0..b2578d83d5 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -80,7 +80,7 @@ def test_logs_disabled_by_default(sentry_init, capture_envelopes): @minimum_python_37 def test_logs_basics(sentry_init, capture_envelopes): - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() sentry_sdk.logger.trace("This is a 'trace' log...") @@ -111,11 +111,29 @@ def test_logs_basics(sentry_init, capture_envelopes): assert logs[5].get("severity_number") == 21 +@minimum_python_37 +def test_logs_experimental_option_still_works(sentry_init, capture_envelopes): + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + sentry_sdk.logger.error("This is an error log...") + + get_client().flush() + + logs = envelopes_to_logs(envelopes) + assert len(logs) == 1 + + assert logs[0].get("severity_text") == "error" + assert logs[0].get("severity_number") == 17 + + @minimum_python_37 def test_logs_before_send_log(sentry_init, capture_envelopes): - before_log_called = [False] + before_log_called = False def _before_log(record, hint): + nonlocal before_log_called + assert set(record.keys()) == { "severity_text", "severity_number", @@ -128,15 +146,13 @@ def _before_log(record, hint): if record["severity_text"] in ["fatal", "error"]: return None - before_log_called[0] = True + before_log_called = True return record sentry_init( - _experiments={ - "enable_logs": True, - "before_send_log": _before_log, - } + enable_logs=True, + before_send_log=_before_log, ) envelopes = capture_envelopes() @@ -155,7 +171,37 @@ def _before_log(record, hint): assert logs[1]["severity_text"] == "debug" assert logs[2]["severity_text"] == "info" assert logs[3]["severity_text"] == "warn" - assert before_log_called[0] + assert before_log_called is True + + +@minimum_python_37 +def test_logs_before_send_log_experimental_option_still_works( + sentry_init, capture_envelopes +): + before_log_called = False + + def _before_log(record, hint): + nonlocal before_log_called + before_log_called = True + + return record + + sentry_init( + enable_logs=True, + _experiments={ + "before_send_log": _before_log, + }, + ) + envelopes = capture_envelopes() + + sentry_sdk.logger.error("This is an error log...") + + get_client().flush() + logs = envelopes_to_logs(envelopes) + assert len(logs) == 1 + + assert logs[0]["severity_text"] == "error" + assert before_log_called is True @minimum_python_37 @@ -163,7 +209,7 @@ def test_logs_attributes(sentry_init, capture_envelopes): """ Passing arbitrary attributes to log messages. """ - sentry_init(_experiments={"enable_logs": True}, server_name="test-server") + sentry_init(enable_logs=True, server_name="test-server") envelopes = capture_envelopes() attrs = { @@ -196,7 +242,7 @@ def test_logs_message_params(sentry_init, capture_envelopes): """ This is the official way of how to pass vars to log messages. """ - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() sentry_sdk.logger.warning("The recorded value was '{int_var}'", int_var=1) @@ -239,7 +285,7 @@ def test_logs_tied_to_transactions(sentry_init, capture_envelopes): """ Log messages are also tied to transactions. """ - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() with sentry_sdk.start_transaction(name="test-transaction") as trx: @@ -255,7 +301,7 @@ def test_logs_tied_to_spans(sentry_init, capture_envelopes): """ Log messages are also tied to spans. """ - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() with sentry_sdk.start_transaction(name="test-transaction"): @@ -271,7 +317,7 @@ def test_auto_flush_logs_after_100(sentry_init, capture_envelopes): """ If you log >100 logs, it should automatically trigger a flush. """ - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger") @@ -288,7 +334,7 @@ def test_auto_flush_logs_after_100(sentry_init, capture_envelopes): def test_log_user_attributes(sentry_init, capture_envelopes): """User attributes are sent if enable_logs is True.""" - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) sentry_sdk.set_user({"id": "1", "email": "test@example.com", "username": "test"}) envelopes = capture_envelopes() @@ -314,7 +360,7 @@ def test_auto_flush_logs_after_5s(sentry_init, capture_envelopes): """ If you log a single log, it should automatically flush after 5 seconds, at most 10 seconds. """ - sentry_init(_experiments={"enable_logs": True}) + sentry_init(enable_logs=True) envelopes = capture_envelopes() python_logger = logging.Logger("test-logger")