diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 97d76656b..226114202 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,40 @@ This document describes changes between each past release. 14.8.1 (unreleased) ------------------- +**Breaking Changes** + +- ``raven`` is not installed by default anymore (fixes #3054). Sentry reporting is now enabled via settings (or environment variables). + +In order to migrate from Kinto <14 to Kinto 15, remove the mention of ``sentry`` and ``raven`` from your logging configuration: + +.. code-block:: diff + + # kinto.ini + + [logger_root] + level = INFO + - handlers = console, sentry + + handlers = console + + [handlers] + - keys = console, sentry + + keys = console + + - [handler_sentry] + - class = raven.handlers.logging.SentryHandler + - args = ('https://:@app.getsentry.com/',) + - level = WARNING + - formatter = generic + +And add the following settings: + +.. code-block:: ini + + kinto.sentry_dsn = https://userid@o1.ingest.sentry.io/1 + kinto.sentry_env = prod + +For more information, see `Settings documentation `_. + **Documentation** - Fix ``/batch`` endpoint documentation about required authentication. diff --git a/docs/configuration/production.rst b/docs/configuration/production.rst index 8f8f72e32..87f0ac053 100644 --- a/docs/configuration/production.rst +++ b/docs/configuration/production.rst @@ -271,7 +271,6 @@ processed through `Kibana `_ or With the following configuration, all logs are structured in JSON and redirected to standard output (See `12factor app `_). -A `Sentry `_ logger is also enabled. .. note:: @@ -283,14 +282,14 @@ A `Sentry `_ logger is also enabled. keys = root [handlers] - keys = console, sentry + keys = console [formatters] keys = generic, json [logger_root] level = INFO - handlers = console, sentry + handlers = console [handler_console] class = StreamHandler @@ -298,12 +297,6 @@ A `Sentry `_ logger is also enabled. level = NOTSET formatter = json - [handler_sentry] - class = raven.handlers.logging.SentryHandler - args = ('https://:@app.getsentry.com/',) - level = WARNING - formatter = generic - [formatter_json] class = kinto.core.JsonLogFormatter diff --git a/docs/configuration/settings.rst b/docs/configuration/settings.rst index 61ac90921..b80ff07ae 100644 --- a/docs/configuration/settings.rst +++ b/docs/configuration/settings.rst @@ -396,17 +396,29 @@ Example output: {"Pid": 19240, "Type": "root", "Timestamp": 1489067817834153984, "Severity": 4, "Hostname": "pluo", "Logger": "%", "EnvVersion": "2.0", "Fields": {"perm": "read", "userid": "ldap:john@corp.com", "message": "Permission not granted.", "uri": "/buckets/123"}} +.. _handling-exceptions-with-sentry: + Handling exceptions with Sentry ::::::::::::::::::::::::::::::: -Requires the ``raven`` package. -Sentry logging can be enabled `as explained in official documentation -`_. +Sentry reporting can be enabled via the following settings: + +.. code-block:: ini + + kinto.sentry_dsn = https://userid@o1.ingest.sentry.io/1 + kinto.sentry_env = stage + +Or the equivalent environment variables: + +:: + + SENTRY_DSN=https://userid@o1.ingest.sentry.io/1 + SENTRY_ENV=stage .. note:: - The application sends an *INFO* message on startup (mainly for setup check). + The application sends an event on startup (mainly for setup check). Monitoring with StatsD diff --git a/kinto/config/kinto.tpl b/kinto/config/kinto.tpl index 893c43620..c333024de 100644 --- a/kinto/config/kinto.tpl +++ b/kinto/config/kinto.tpl @@ -239,6 +239,9 @@ kinto.bucket_create_principals = account:admin # kinto.statsd_prefix = kinto # kinto.statsd_url = +# kinto.sentry_dsn = +# kinto.sentry_env = + # kinto.newrelic_config = # kinto.newrelic_env = dev diff --git a/kinto/core/__init__.py b/kinto/core/__init__.py index 6824dc8a4..173e55004 100644 --- a/kinto/core/__init__.py +++ b/kinto/core/__init__.py @@ -63,6 +63,7 @@ "kinto.core.initialization.setup_deprecation", "kinto.core.initialization.setup_authentication", "kinto.core.initialization.setup_backoff", + "kinto.core.initialization.setup_sentry", "kinto.core.initialization.setup_statsd", "kinto.core.initialization.setup_listeners", "kinto.core.events.setup_transaction_hook", @@ -86,6 +87,8 @@ "retry_after_seconds": 30, "version_prefix_redirect_ttl_seconds": -1, "settings_prefix": "", + "sentry_dsn": None, + "sentry_env": None, "statsd_backend": "kinto.core.statsd", "statsd_prefix": "kinto.core", "statsd_url": None, diff --git a/kinto/core/initialization.py b/kinto/core/initialization.py index 4d8a92517..05c0329b4 100644 --- a/kinto/core/initialization.py +++ b/kinto/core/initialization.py @@ -5,7 +5,7 @@ from datetime import datetime from dateutil import parser as dateparser -from pyramid.events import NewRequest, NewResponse +from pyramid.events import ApplicationCreated, NewRequest, NewResponse from pyramid.exceptions import ConfigurationError from pyramid.httpexceptions import HTTPBadRequest, HTTPGone, HTTPTemporaryRedirect from pyramid.interfaces import IAuthenticationPolicy @@ -26,6 +26,11 @@ from werkzeug.middleware.profiler import ProfilerMiddleware except ImportError: # pragma: no cover ProfilerMiddleware = False +try: + import sentry_sdk + from sentry_sdk.integrations.pyramid import PyramidIntegration +except ImportError: # pragma: no cover + sentry_sdk = None logger = logging.getLogger(__name__) @@ -264,6 +269,34 @@ def setup_cache(config): config.registry.heartbeats["cache"] = heartbeat +def setup_sentry(config): + settings = config.get_settings() + + # Note: SENTRY_DSN and SENTRY_ENV env variables will override + # .ini values thanks to `load_default_settings()`. + + dsn = settings["sentry_dsn"] + if dsn: + env_options = {} + env = settings["sentry_env"] + if env: + env_options["environment"] = env + + sentry_sdk.init( + dsn, + integrations=[ + PyramidIntegration(), + ], + **env_options, + ) + + def on_app_created(event): + msg = "Running {project_name} {project_version}.".format_map(settings) + sentry_sdk.capture_message(msg, "info") + + config.add_subscriber(on_app_created, ApplicationCreated) + + def setup_statsd(config): settings = config.get_settings() config.registry.statsd = None diff --git a/requirements.txt b/requirements.txt index 00a7c005e..dd354535d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pyramid-multiauth==1.0.1 pyramid-tm==2.5 python-dateutil==2.8.2 python-memcached==1.59 -raven==6.10.0 +sentry-sdk==1.9.10 requests==2.28.1 SQLAlchemy==1.4.41 statsd==3.3.0 diff --git a/setup.cfg b/setup.cfg index f8bba467f..b1b377678 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,7 +77,7 @@ postgresql = zope.sqlalchemy monitoring = newrelic - raven + sentry-sdk statsd werkzeug diff --git a/tests/core/test_initialization.py b/tests/core/test_initialization.py index ac934e927..4ddd7992e 100644 --- a/tests/core/test_initialization.py +++ b/tests/core/test_initialization.py @@ -242,6 +242,56 @@ def test_load_default_settings_converts_to_native_correctly(self): self.assertEqual(settings["my_cool_setting"], "1.2") +class SentryTest(unittest.TestCase): + @unittest.skipIf(initialization.sentry_sdk is None, "sentry is not installed.") + def test_sentry_not_enabled_by_default(self): + config = Configurator() + with mock.patch("sentry_sdk.init") as mocked: + kinto.core.initialize(config, "0.0.1") + + self.assertFalse(mocked.called) + + @unittest.skipIf(initialization.sentry_sdk is None, "sentry is not installed.") + def test_sentry_enabled_if_sentry_dsn_is_set(self): + config = Configurator( + settings={ + "sentry_dsn": "https://notempty", + "sentry_env": "local", + } + ) + with mock.patch("sentry_sdk.init") as mocked: + kinto.core.initialize(config, "0.0.1") + + init_call = mocked.call_args_list[0] + self.assertEqual(init_call[0][0], "https://notempty") + self.assertEqual(init_call[1]["environment"], "local") + + @unittest.skipIf(initialization.sentry_sdk is None, "sentry is not installed.") + def test_message_is_sent_on_startup(self): + config = Configurator(settings={**kinto.core.DEFAULT_SETTINGS}) + config.add_settings( + { + "sentry_dsn": "https://notempty", + } + ) + + with mock.patch("sentry_sdk.init"): + kinto.core.initialize(config, "0.0.1", "name") + + with mock.patch("sentry_sdk.capture_message") as mocked: + config.make_wsgi_app() + + mocked.assert_called_with("Running 0.0.1.", "info") + + @unittest.skipIf(initialization.sentry_sdk is None, "sentry is not installed.") + def unexpected_exceptions_are_reported(self): + with mock.patch("kinto.core.views.hello.get_eos", side_effect=ValueError): + with mock.patch("sentry_sdk.hub.Hub.capture_event") as mocked: + resp = self.app.get("/") + assert resp.status == 500 + assert len(mocked.call_args_list) > 0 + + class StatsDConfigurationTest(unittest.TestCase): def setUp(self): settings = { diff --git a/tests/test_configuration/test.ini b/tests/test_configuration/test.ini index d3f7b71d7..5a86fa58d 100644 --- a/tests/test_configuration/test.ini +++ b/tests/test_configuration/test.ini @@ -239,6 +239,9 @@ kinto.bucket_create_principals = account:admin # kinto.statsd_prefix = kinto # kinto.statsd_url = +# kinto.sentry_dsn = +# kinto.sentry_env = + # kinto.newrelic_config = # kinto.newrelic_env = dev diff --git a/tox.ini b/tox.ini index e7723bc63..030cb0884 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps = -r{toxinidir}/dev-requirements.txt psycopg2 newrelic - raven + sentry-sdk statsd install_command = pip install {opts} {packages} -c{toxinidir}/requirements.txt