diff --git a/swh/core/api/gunicorn_config.py b/swh/core/api/gunicorn_config.py index 7de6bb7ac41332acacc2aef61a545b8fd2cd4ec5..1e2ffbea2019411c1ebc5189a8c9cb0d9debb4a0 100644 --- a/swh/core/api/gunicorn_config.py +++ b/swh/core/api/gunicorn_config.py @@ -11,6 +11,7 @@ and redefining functions and variables they want. May be imported by gunicorn using `--config 'python:swh.core.api.gunicorn_config'`.""" +from typing import Optional from ..sentry import init_sentry @@ -24,6 +25,7 @@ def post_fork( sentry_integrations=None, extra_sentry_kwargs={}, disable_logging_events=True, + traces_sample_rate: Optional[float] = None, ): # Initializes sentry as soon as possible in gunicorn's worker processes. @@ -38,4 +40,5 @@ def post_fork( integrations=sentry_integrations, extra_kwargs=extra_sentry_kwargs, disable_logging_events=disable_logging_events, + traces_sample_rate=traces_sample_rate, ) diff --git a/swh/core/api/tests/test_gunicorn.py b/swh/core/api/tests/test_gunicorn.py index c19d693af81814373e234aecb1440452579b9064..3a4b21a5c544cdf67bb8b72f39a142c04920e787 100644 --- a/swh/core/api/tests/test_gunicorn.py +++ b/swh/core/api/tests/test_gunicorn.py @@ -29,6 +29,7 @@ def test_post_fork_default(mocker): debug=False, release="0.0.0", environment=None, + traces_sample_rate=None, ) @@ -53,6 +54,7 @@ def test_post_fork_with_dsn_env(mocker): debug=False, release=None, environment=None, + traces_sample_rate=None, ) @@ -87,6 +89,7 @@ def test_post_fork_with_package_env(mocker): debug=False, release="swh.core@" + version, environment="tests", + traces_sample_rate=None, ) @@ -114,6 +117,7 @@ def test_post_fork_debug(mocker): debug=True, release=None, environment=None, + traces_sample_rate=None, ) @@ -134,6 +138,7 @@ def test_post_fork_no_flask(mocker): debug=False, release=None, environment=None, + traces_sample_rate=None, ) @@ -152,6 +157,7 @@ def test_post_fork_override_logging_events_envvar(mocker): debug=False, release=None, environment=None, + traces_sample_rate=None, ) @@ -178,4 +184,58 @@ def test_post_fork_extras(mocker): bar="baz", release=None, environment=None, + traces_sample_rate=None, + ) + + +def test_post_fork_traces_sample_rate(mocker): + flask_integration = object() # unique object to check for equality + logging_integration = object() # unique object to check for equality + mocker.patch( + "sentry_sdk.integrations.flask.FlaskIntegration", new=lambda: flask_integration + ) + mocker.patch( + "sentry_sdk.integrations.logging.LoggingIntegration", + new=lambda event_level: logging_integration, + ) + sentry_sdk_init = mocker.patch("sentry_sdk.init") + mocker.patch.dict(os.environ, {"SWH_SENTRY_DSN": "test_dsn"}) + + gunicorn_config.post_fork(None, None, traces_sample_rate=1.0) + + sentry_sdk_init.assert_called_once_with( + dsn="test_dsn", + integrations=[flask_integration, logging_integration], + debug=False, + release=None, + environment=None, + traces_sample_rate=1.0, + ) + + +def test_post_fork_override_traces_sample_rate_envvar(mocker): + flask_integration = object() # unique object to check for equality + logging_integration = object() # unique object to check for equality + mocker.patch( + "sentry_sdk.integrations.flask.FlaskIntegration", new=lambda: flask_integration + ) + mocker.patch( + "sentry_sdk.integrations.logging.LoggingIntegration", + new=lambda event_level: logging_integration, + ) + sentry_sdk_init = mocker.patch("sentry_sdk.init") + mocker.patch.dict( + os.environ, + {"SWH_SENTRY_DSN": "test_dsn", "SWH_SENTRY_TRACES_SAMPLE_RATE": "0.999"}, + ) + + gunicorn_config.post_fork(None, None) + + sentry_sdk_init.assert_called_once_with( + dsn="test_dsn", + integrations=[flask_integration, logging_integration], + debug=False, + release=None, + environment=None, + traces_sample_rate=0.999, ) diff --git a/swh/core/sentry.py b/swh/core/sentry.py index 6fd5b1bf8e177353436b79c35a233e56699df995..a0dd15546ccc680d50250afc56eafa3f4b4d050f 100644 --- a/swh/core/sentry.py +++ b/swh/core/sentry.py @@ -46,6 +46,37 @@ def override_with_bool_envvar(envvar: str, default: bool) -> bool: return default +def override_with_float_envvar( + envvar: str, default: Optional[float] +) -> Optional[float]: + """Override `default` with the environment variable `envvar` casted as a float. + + `default` is returned if the environment variable `envvar` is missing or if + we're not able to cast it to a float. + + Args: + envvar: the name of the environment variable + default: default value + + Returns: + A float or `default` + """ + envvalue = os.environ.get(envvar) + if envvalue is None: + return default + try: + return float(envvalue) + except ValueError: + logger.warning( + "Could not interpret environment variable %s=%r as float, " + "using default value %s", + envvar, + envvalue, + default, + ) + return default + + def init_sentry( sentry_dsn: Optional[str] = None, *, @@ -54,10 +85,11 @@ def init_sentry( debug: bool = False, disable_logging_events: bool = False, integrations: Optional[List] = None, + traces_sample_rate: Optional[float] = None, extra_kwargs: Optional[Dict] = None, deferred_init: bool = False, -): - """Configure the sentry integration +) -> None: + """Configure the sentry integration. Args: sentry_dsn: Sentry DSN; where sentry report will be sent. Overridden by @@ -70,10 +102,12 @@ def init_sentry( log entries as Sentry events. Overridden by :envvar:`SWH_SENTRY_DISABLE_LOGGING_EVENTS` integrations: list of dedicated Sentry integrations to include + traces_sample_rate: a number between 0 and 1, controlling the percentage chance a + given transaction will be sent to Sentry. Overridden by + :envvar:`SWH_SENTRY_TRACES_SAMPLE_RATE` extra_kwargs: dict of additional parameters passed to :func:`sentry_sdk.init` deferred_init: indicates that sentry will be properly initialized in subsequent calls and that no warnings about missing DSN should be logged - """ if integrations is None: integrations = [] @@ -101,10 +135,17 @@ def init_sentry( integrations.append(LoggingIntegration(event_level=None)) + # to completely disable tracing `traces_sample_rate` should be set to None instead + # of 0.0 + traces_sample_rate = override_with_float_envvar( + "SWH_SENTRY_TRACES_SAMPLE_RATE", traces_sample_rate + ) + sentry_sdk.init( release=get_sentry_release(main_package, sentry_dsn), environment=environment, dsn=sentry_dsn, + traces_sample_rate=traces_sample_rate, integrations=integrations, debug=debug, **extra_kwargs, diff --git a/swh/core/tests/test_cli.py b/swh/core/tests/test_cli.py index cdd50e4aee8dc9cfd2121c7ce6fbc664ba4b7586..170cb1e1b841615c71c7c13e8ff005a4b141aa2a 100644 --- a/swh/core/tests/test_cli.py +++ b/swh/core/tests/test_cli.py @@ -124,6 +124,7 @@ def test_command(swhmain, mocker): integrations=[], release="0.0.0", environment=None, + traces_sample_rate=None, ) assert_result(result) assert result.output.strip() == "Hello SWH!" @@ -185,6 +186,7 @@ def test_sentry(swhmain, mocker): integrations=[], release=None, environment=None, + traces_sample_rate=None, ) @@ -207,6 +209,7 @@ def test_sentry_debug(swhmain, mocker): integrations=[], release=None, environment=None, + traces_sample_rate=None, ) @@ -231,6 +234,7 @@ def test_sentry_env(swhmain, mocker): integrations=[], release=None, environment=None, + traces_sample_rate=None, ) @@ -259,6 +263,7 @@ def test_sentry_env_main_package(swhmain, mocker): integrations=[], release="swh.core@" + version, environment="tests", + traces_sample_rate=None, ) diff --git a/swh/core/tests/test_sentry.py b/swh/core/tests/test_sentry.py index 44ad156d02e494c6781df1acf585ffba6d2100db..a5405907bd78d753be810349052dec51dc92d763 100644 --- a/swh/core/tests/test_sentry.py +++ b/swh/core/tests/test_sentry.py @@ -9,7 +9,11 @@ import pytest import sentry_sdk from sentry_sdk import capture_exception, capture_message, set_tag -from swh.core.sentry import init_sentry, override_with_bool_envvar +from swh.core.sentry import ( + init_sentry, + override_with_bool_envvar, + override_with_float_envvar, +) SENTRY_DSN = "https://user@example.org/1234" @@ -48,6 +52,31 @@ def test_override_with_bool_envvar_logging(monkeypatch, caplog): assert caplog.records[0].levelname == "WARNING" +@pytest.mark.parametrize( + "envvalue,retval", + (("1.0", 1.0), ("0.0", 0.0), ("0", 0.0), ("a", None)), +) +def test_override_with_float_envvar(monkeypatch, envvalue: str, retval: bool): + envvar = "OVERRIDE_WITH_FLOAT_ENVVAR" + monkeypatch.setenv(envvar, envvalue) + assert override_with_float_envvar(envvar, None) == retval + + +def test_override_with_float_envvar_logging(monkeypatch, caplog): + envvar = "OVERRIDE_WITH_FLOAT_ENVVAR" + monkeypatch.setenv(envvar, "not a float env value") + for default in (None, 1.0): + caplog.clear() + assert override_with_float_envvar(envvar, default) == default + assert len(caplog.records) == 1 + assert ( + "OVERRIDE_WITH_FLOAT_ENVVAR='not a float env value'" + in caplog.records[0].getMessage() + ) + assert f"using default value {default}" in caplog.records[0].getMessage() + assert caplog.records[0].levelname == "WARNING" + + def test_sentry(): reports = [] init_sentry(SENTRY_DSN, extra_kwargs={"transport": reports.append}) @@ -160,3 +189,14 @@ def test_sentry_deferred_init(caplog, deferred_init): ) else: assert not caplog.records + + +@pytest.mark.parametrize("traces_sample_rate", [1.0, 0.0, None]) +def test_sentry_traces_sample_rate(caplog, traces_sample_rate): + init_sentry(None, traces_sample_rate=traces_sample_rate) + client = sentry_sdk.get_client() + if traces_sample_rate is not None: + assert client.options["traces_sample_rate"] == traces_sample_rate + else: + assert client.options["enable_tracing"] is None + assert client.options["traces_sample_rate"] is None