diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 761c102f35..3ce14b1563 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -24,6 +24,7 @@ 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. - In the AWS Lambda and GCP integrations, the message of the warning the SDK optionally emits if a function is about to time out has changed. +- We changed the way we emit warnings. Deprecations will from now on be always emitted using `warnings.warn()`, while all other warnings will be emitted using `logger.warning()`. - `sentry_sdk.init()` can no longer be used as a context manager. ### Logging diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index f62175c09f..6a016c22d5 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -61,30 +61,26 @@ def enabled(option: str) -> bool: lazy_mode = enabled("lazy-apps") or enabled("lazy") if lazy_mode and not threads_enabled: - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI without thread support. " - "This might lead to unexpected issues. " - 'Please run uWSGI with "--enable-threads" for full support.' - ) + from sentry_sdk.utils import logger + + logger.warning( + "IMPORTANT: " + "We detected the use of uWSGI without thread support. " + "This might lead to unexpected issues. " + 'Please run uWSGI with "--enable-threads" for full support.' ) return False elif not lazy_mode and (not threads_enabled or not fork_hooks_on): - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI in preforking mode without " - "thread support. This might lead to crashing workers. " - 'Please run uWSGI with both "--enable-threads" and ' - '"--py-call-uwsgi-fork-hooks" for full support.' - ) + from sentry_sdk.utils import logger + + logger.warning( + "IMPORTANT: " + "We detected the use of uWSGI in preforking mode without " + "thread support. This might lead to crashing workers. " + 'Please run uWSGI with both "--enable-threads" and ' + '"--py-call-uwsgi-fork-hooks" for full support.' ) return False diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ec29ee895b..8c18d53831 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -5,7 +5,6 @@ import socket import sys import uuid -import warnings from collections.abc import Iterable, Mapping from contextvars import ContextVar from datetime import datetime, timezone @@ -348,9 +347,8 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": else rv["send_default_pii"] ) elif has_data_collection_enabled(rv) and rv["event_scrubber"]: - warnings.warn( + logger.warning( "Event scrubbers are not enabled when data collection configuration is provided. Ignoring event_scrubber...", - stacklevel=2, ) rv["event_scrubber"] = None @@ -366,21 +364,18 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": ) if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): - warnings.warn( + logger.warning( "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", - stacklevel=2, ) if rv["ignore_spans"] and not has_span_streaming_enabled(rv): - warnings.warn( + logger.warning( "The `ignore_spans` parameter only works when `trace_lifecycle` is set to `stream`.", - stacklevel=2, ) if rv["before_send_span"] and not has_span_streaming_enabled(rv): - warnings.warn( + logger.warning( "The `before_send_span` parameter only works when `trace_lifecycle` is set to `stream`.", - stacklevel=2, ) return rv @@ -1334,9 +1329,8 @@ def close( """ if self.transport is not None: if self._has_async_transport(): - warnings.warn( + logger.warning( "close() used with AsyncHttpTransport. Use close_async() instead.", - stacklevel=2, ) self._flush_components() else: @@ -1381,9 +1375,8 @@ def flush( """ if self.transport is not None: if self._has_async_transport(): - warnings.warn( + logger.warning( "flush() used with AsyncHttpTransport. Use flush_async() instead.", - stacklevel=2, ) return if timeout is None: diff --git a/sentry_sdk/data_collection.py b/sentry_sdk/data_collection.py index 4b130da1fb..4a6889ef7d 100644 --- a/sentry_sdk/data_collection.py +++ b/sentry_sdk/data_collection.py @@ -22,11 +22,11 @@ ``DeprecationWarning`` is emitted for ``send_default_pii``. """ -import warnings from typing import TYPE_CHECKING, List, Mapping, Optional, Union, cast from urllib.parse import parse_qs, urlencode from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.utils import deprecation_warning if TYPE_CHECKING: from typing import Any, Dict @@ -315,11 +315,9 @@ def _resolve_data_collection(options: "Dict[str, Any]") -> "DataCollection": ) ) if send_default_pii is not None: - warnings.warn( + deprecation_warning( "`send_default_pii` is deprecated and ignored when " "`data_collection` is set.", - DeprecationWarning, - stacklevel=2, ) return _resolve_explicit( user_dc, diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index ff7eda64c0..4caf3988cf 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -1,5 +1,4 @@ import sys -import warnings import weakref from inspect import isawaitable from typing import TYPE_CHECKING @@ -22,6 +21,7 @@ ensure_integration_enabled, event_from_exception, has_data_collection_enabled, + logger, parse_version, reraise, ) @@ -155,9 +155,8 @@ async def _context_enter(request: "Request") -> None: isinstance(integration, SanicIntegration) and integration._unsampled_statuses ): - warnings.warn( + logger.warning( "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", - stacklevel=2, ) sentry_sdk.traces.continue_trace(dict(request.headers)) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index d1b4e613a5..4e298f3b95 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -1,5 +1,4 @@ import functools -import warnings from inspect import isawaitable import sentry_sdk @@ -13,6 +12,7 @@ ensure_integration_enabled, event_from_exception, has_data_collection_enabled, + logger, package_version, ) @@ -95,9 +95,8 @@ def _sentry_patched_schema_init( should_use_async_extension = _guess_if_using_async(extensions) if should_use_async_extension is None: - warnings.warn( + logger.warning( "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", - stacklevel=2, ) should_use_async_extension = False diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py index 606caa852e..3c299deef0 100644 --- a/sentry_sdk/integrations/threading.py +++ b/sentry_sdk/integrations/threading.py @@ -1,5 +1,4 @@ import sys -import warnings from concurrent.futures import Future, ThreadPoolExecutor from functools import wraps from threading import Thread, current_thread @@ -11,6 +10,7 @@ from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, + logger, reraise, ) @@ -62,12 +62,11 @@ def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": if integration.propagate_scope: if is_async_emulated_with_threads: - warnings.warn( + logger.warning( "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " "Please either upgrade to Django channels 4.0+, use Django's async features " "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", - stacklevel=2, ) isolation_scope = sentry_sdk.get_isolation_scope() current_scope = sentry_sdk.get_current_scope() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index ab00ac5948..ff7e1375a0 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1,7 +1,6 @@ import os import platform import sys -import warnings from collections import deque from contextlib import contextmanager from contextvars import ContextVar @@ -51,6 +50,7 @@ capture_internal_exception, capture_internal_exceptions, datetime_from_isoformat, + deprecation_warning, disable_capture_event, event_from_exception, exc_info_from_error, @@ -755,10 +755,8 @@ def transaction(self) -> "Any": return None if isinstance(self._span, StreamedSpan): - warnings.warn( + deprecation_warning( "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, ) return None @@ -793,10 +791,8 @@ def transaction(self, value: "Any") -> None: self._transaction = value if self._span: if isinstance(self._span, StreamedSpan): - warnings.warn( + deprecation_warning( "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, ) return None @@ -1078,10 +1074,8 @@ def start_transaction( """ client = self.get_client() if has_span_streaming_enabled(client.options): - warnings.warn( + deprecation_warning( "Scope.start_transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, ) return NoOpSpan() @@ -1155,18 +1149,14 @@ def start_span(self, **kwargs: "Any") -> "Span": """ client = sentry_sdk.get_client() if has_span_streaming_enabled(client.options): - warnings.warn( + deprecation_warning( "Scope.start_span is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, ) return NoOpSpan() if kwargs.get("description") is not None: - warnings.warn( + deprecation_warning( "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, ) with new_scope(): diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index b02167672d..0799f7cba0 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -9,7 +9,6 @@ import sys import uuid -import warnings from datetime import datetime, timedelta, timezone from enum import Enum from typing import TYPE_CHECKING @@ -24,6 +23,7 @@ from sentry_sdk.tracing_utils import Baggage from sentry_sdk.utils import ( capture_internal_exceptions, + deprecation_warning, format_attribute, get_current_thread_meta, logger, @@ -170,11 +170,10 @@ def start_span( client = sentry_sdk.get_client() if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( + logger.warning( "Using span streaming API in non-span-streaming mode. Use " "sentry_sdk.start_transaction() and sentry_sdk.start_span() " "instead.", - stacklevel=2, ) return NoOpStreamedSpan() @@ -349,10 +348,8 @@ def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: self._end(end_timestamp) def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( + deprecation_warning( "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, ) self.end(end_timestamp) @@ -718,10 +715,8 @@ def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: self._end() def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( + deprecation_warning( "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, ) self._end() diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index f8b46829fa..895b2a0671 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,5 +1,4 @@ import uuid -import warnings from datetime import datetime, timedelta, timezone from enum import Enum from typing import TYPE_CHECKING, cast @@ -9,6 +8,7 @@ from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( capture_internal_exceptions, + deprecation_warning, get_current_thread_meta, is_valid_sample_rate, logger, @@ -417,10 +417,8 @@ def start_child(self, **kwargs: "Any") -> "Span": inherited from the current span/transaction. """ if kwargs.get("description") is not None: - warnings.warn( + deprecation_warning( "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, ) kwargs.setdefault("sampled", self.sampled) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index a931124538..7e54a5f55f 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -5,7 +5,6 @@ import re import sys import uuid -import warnings from collections.abc import Mapping, MutableMapping from datetime import datetime, timedelta, timezone from random import Random @@ -20,6 +19,7 @@ _is_in_project_root, _module_in_list, capture_internal_exceptions, + deprecation_warning, filename_for_module, has_data_collection_enabled, is_sentry_url, @@ -1037,10 +1037,8 @@ async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": return await f(*args, **kwargs) if isinstance(current_span, StreamedSpan): - warnings.warn( + deprecation_warning( "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, ) return await f(*args, **kwargs) @@ -1082,10 +1080,8 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": return f(*args, **kwargs) if isinstance(current_span, StreamedSpan): - warnings.warn( + deprecation_warning( "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, ) return f(*args, **kwargs) @@ -1143,10 +1139,9 @@ def span_decorator(f: "Any") -> "Any": async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": client = sentry_sdk.get_client() if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( + logger.warning( "Using span streaming API in non-span-streaming mode. Use " "@sentry_sdk.trace instead.", - stacklevel=2, ) span_name = name or qualname_from_function(f) or "" @@ -1166,10 +1161,9 @@ async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": client = sentry_sdk.get_client() if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( + logger.warning( "Using span streaming API in non-span-streaming mode. Use " "@sentry_sdk.trace instead.", - stacklevel=2, ) span_name = name or qualname_from_function(f) or "" diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 5974266826..bc839d763b 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -10,6 +10,7 @@ import sys import threading import time +import warnings from collections import namedtuple from contextlib import contextmanager from contextvars import ContextVar @@ -2023,3 +2024,12 @@ def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue": # Coerce to string if we don't know what to do with the value. This should # never happen as we pre-format early in format_attribute, but let's be safe. return {"value": safe_repr(val), "type": "string"} + + +def deprecation_warning(msg: str) -> None: + """ + Emit a warnings.warn about a deprecation. + + For other types of warnings, use logger.warning(). + """ + warnings.warn(msg, stacklevel=3, category=DeprecationWarning) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 0424c965ec..bbdf693ddb 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -59,6 +59,8 @@ async def test_basic( trace_lifecycle="stream" if span_streaming else "static", ) + from unittest import mock + import channels # type: ignore[import-not-found] if span_streaming: @@ -70,13 +72,14 @@ async def test_basic( and django.VERSION >= (3, 0) and django.VERSION < (4, 0) ): - # We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older + # We log a warning for channels 2.x and 3.x on Python 3.8 and older # because the async support was not really good back then and there is a known issue. - # See the TreadingIntegration for details. - with pytest.warns(UserWarning): + # See the ThreadingIntegration for details. + with mock.patch("sentry_sdk.integrations.threading.logger") as mock_logger: comm = HttpCommunicator(application, "GET", "/view-exc?test=query") response = await comm.get_response() await comm.wait() + mock_logger.warning.assert_called() else: comm = HttpCommunicator(application, "GET", "/view-exc?test=query") response = await comm.get_response() @@ -111,13 +114,14 @@ async def test_basic( and django.VERSION >= (3, 0) and django.VERSION < (4, 0) ): - # We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older + # We log a warning for channels 2.x and 3.x on Python 3.8 and older # because the async support was not really good back then and there is a known issue. - # See the TreadingIntegration for details. - with pytest.warns(UserWarning): + # See the ThreadingIntegration for details. + with mock.patch("sentry_sdk.integrations.threading.logger") as mock_logger: comm = HttpCommunicator(application, "GET", "/view-exc?test=query") response = await comm.get_response() await comm.wait() + mock_logger.warning.assert_called() else: comm = HttpCommunicator(application, "GET", "/view-exc?test=query") response = await comm.get_response() diff --git a/tests/test_client.py b/tests/test_client.py index 387cab3e8f..0d010ec99e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,7 +4,6 @@ import subprocess import sys import time -import warnings from collections import Counter, defaultdict from collections.abc import Mapping from textwrap import dedent @@ -1273,18 +1272,19 @@ def test_error_sampler(_, sentry_init, capture_events, test_config): [{"py-call-uwsgi-fork-hooks": True}, ["--enable-threads"]], ], ) -def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags): +def test_uwsgi_warnings(sentry_init, opt, missing_flags): uwsgi = mock.MagicMock() uwsgi.opt = opt - with mock.patch.dict("sys.modules", uwsgi=uwsgi): - sentry_init() - if missing_flags: - assert len(recwarn) == 1 - record = recwarn.pop() - for flag in missing_flags: - assert flag in str(record.message) - else: - assert not recwarn + with mock.patch("sentry_sdk.utils.logger") as mock_logger: + with mock.patch.dict("sys.modules", uwsgi=uwsgi): + sentry_init() + if missing_flags: + assert mock_logger.warning.call_count == 1 + message = mock_logger.warning.call_args[0][0] + for flag in missing_flags: + assert flag in message + else: + mock_logger.warning.assert_not_called() class TestSpanClientReports: @@ -1446,8 +1446,11 @@ def test_dropped_transaction(sentry_init, capture_record_lost_event_calls, test_ def test_ignore_spans_warns_without_streaming(sentry_init): - with pytest.warns(UserWarning, match=r"`ignore_spans` parameter only works"): + with mock.patch("sentry_sdk.client.logger") as mock_logger: sentry_init(ignore_spans=["/health"], trace_lifecycle="static") + mock_logger.warning.assert_any_call( + "The `ignore_spans` parameter only works when `trace_lifecycle` is set to `stream`.", + ) @pytest.mark.parametrize( @@ -1459,11 +1462,12 @@ def test_ignore_spans_warns_without_streaming(sentry_init): ], ) def test_ignore_spans_does_not_warn(sentry_init, options): - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") + with mock.patch("sentry_sdk.client.logger") as mock_logger: sentry_init(**options) - ignore_spans_warnings = [w for w in caught if "ignore_spans" in str(w.message)] + ignore_spans_warnings = [ + c for c in mock_logger.warning.call_args_list if "ignore_spans" in str(c) + ] assert ignore_spans_warnings == [] @@ -1589,8 +1593,6 @@ async def test_async_proxy(monkeypatch, testcase): @pytest.mark.skipif(not PY38, reason="Async client methods require Python 3.8+") async def test_close_with_async_transport_warns(): """Test close() with AsyncHttpTransport emits a warning.""" - import warnings as _warnings - client = Client( "https://foo@sentry.io/123", _experiments={"transport_async": True}, @@ -1598,10 +1600,11 @@ async def test_close_with_async_transport_warns(): ) assert isinstance(client.transport, AsyncHttpTransport) - with _warnings.catch_warnings(record=True) as w: - _warnings.simplefilter("always") + with mock.patch("sentry_sdk.client.logger") as mock_logger: client.close() - assert any("close_async()" in str(warning.message) for warning in w) + assert any( + "close_async()" in str(c) for c in mock_logger.warning.call_args_list + ) @skip_under_gevent @@ -1654,8 +1657,6 @@ async def test_close_async_no_transport(): @pytest.mark.skipif(not PY38, reason="Async client methods require Python 3.8+") async def test_flush_with_async_transport_warns(): """Test flush() with AsyncHttpTransport emits a warning and returns.""" - import warnings as _warnings - client = Client( "https://foo@sentry.io/123", _experiments={"transport_async": True}, @@ -1663,10 +1664,11 @@ async def test_flush_with_async_transport_warns(): ) assert isinstance(client.transport, AsyncHttpTransport) - with _warnings.catch_warnings(record=True) as w: - _warnings.simplefilter("always") + with mock.patch("sentry_sdk.client.logger") as mock_logger: client.flush(timeout=1.0) - assert any("flush_async()" in str(warning.message) for warning in w) + assert any( + "flush_async()" in str(c) for c in mock_logger.warning.call_args_list + ) await client.close_async() diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 8aef2f7d17..9c45ece905 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -1,7 +1,6 @@ import re import sys import time -import warnings from unittest import mock import pytest @@ -446,20 +445,22 @@ def experimental(span, hint): def test_before_send_span_warns_without_span_streaming(sentry_init): - import warnings + from unittest import mock def before_send_span(span, hint): return span - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + with mock.patch("sentry_sdk.client.logger") as mock_logger: sentry_init( traces_sample_rate=1.0, before_send_span=before_send_span, ) - (warning,) = [x for x in w if "before_send_span" in str(x.message)] - assert "trace_lifecycle" in str(warning.message) + warnings = [ + c for c in mock_logger.warning.call_args_list if "before_send_span" in str(c) + ] + assert len(warnings) == 1 + assert "trace_lifecycle" in str(warnings[0]) def test_span_attributes(sentry_init, capture_items): @@ -2023,13 +2024,11 @@ def test_top_level_trace_lifecycle_takes_precedence_over_experiments( items = capture_items("span") - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - with sentry_sdk.traces.start_span(name="segment") as segment: - if streaming_enabled: - assert isinstance(segment, StreamedSpan) - else: - assert isinstance(segment, NoOpStreamedSpan) + with sentry_sdk.traces.start_span(name="segment") as segment: + if streaming_enabled: + assert isinstance(segment, StreamedSpan) + else: + assert isinstance(segment, NoOpStreamedSpan) sentry_sdk.get_client().flush() spans = [item.payload for item in items]