From 97511b51e434d2917f62c68bd5bc983d756e517f Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 15 Feb 2024 18:25:53 +0900 Subject: [PATCH 01/10] docs: clarify logging level overriding in readme --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 32e1769..8245a4b 100644 --- a/README.rst +++ b/README.rst @@ -212,7 +212,7 @@ add the following to your ``test/__init__.py``: """Logging configuration for tests.""" packages = ['package_name'] - level_package = logging.INFO + level_global = logging.INFO TestsLogging.configure() @@ -230,7 +230,8 @@ like so: class TestsLogging(Logging): """Logging configuration for tests.""" - enable_file = False + level_global = logging.DEBUG # relevant if level_global is set to e.g. INFO in parent class + enable_file = False # relevant if enable_file is set to True in parent class As for using the logging in your code, you can use it as usual, for example: From 4fd10bc45b5e2c7feb09b515956965ff54dd564b Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 15 Feb 2024 19:03:07 +0900 Subject: [PATCH 02/10] feat: add basic Sentry boilerplate --- MANIFEST.in | 1 + boilerplates/sentry.py | 46 +++++++++++++++++++++++++++++++++++++++++ requirements_sentry.txt | 2 ++ setup.py | 1 + test/test_sentry.py | 15 ++++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 boilerplates/sentry.py create mode 100644 requirements_sentry.txt create mode 100644 test/test_sentry.py diff --git a/MANIFEST.in b/MANIFEST.in index 9645c66..036f4d2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ include requirements_setup.txt include requirements_packaging_tests.txt include requirements_config.txt include requirements_logging.txt +include requirements_sentry.txt include requirements_cli.txt include requirements_git_repo_tests.txt diff --git a/boilerplates/sentry.py b/boilerplates/sentry.py new file mode 100644 index 0000000..5ce7c59 --- /dev/null +++ b/boilerplates/sentry.py @@ -0,0 +1,46 @@ +"""Boilerplate for integrating Sentry into the project.""" + +import logging +import os +import typing as t + +import sentry_sdk +import sentry_sdk.integrations.logging + +_LOG = logging.getLogger(__name__) + + +class Sentry: + """Sentry configuration. + + For each parameter, the value is taken from the environment variable if it is set, otherwise + from the class attribute if it is set. + + For each parameter, the name of the environment variable name is 'SENTRY_' + followed by the parameter name in upper case. + """ + + dsn: str + release: str + environment: str + + @classmethod + def _get_param(cls, param_name: str) -> t.Optional[str]: + """Get a parameter value by checking envvar first.""" + return os.environ.get(f'SENTRY_{param_name.upper()}', getattr(cls, param_name, None)) + + @classmethod + def init(cls, *args, **kwargs): + """Initialise Sentry SDK.""" + dsn = cls._get_param('dsn') + if dsn is None or not dsn: + _LOG.info('Sentry DSN is not set, skipping Sentry SDK initialisation') + return + sentry_sdk.init( + *args, dsn=dsn, + release=cls._get_param('release'), environment=cls._get_param('environment'), + integrations=[ + sentry_sdk.integrations.logging.LoggingIntegration( + level=logging.INFO, event_level=logging.ERROR)], + traces_sample_rate=1.0, profiles_sample_rate=1.0, enable_tracing=True, + **kwargs) diff --git a/requirements_sentry.txt b/requirements_sentry.txt new file mode 100644 index 0000000..1ac3ae0 --- /dev/null +++ b/requirements_sentry.txt @@ -0,0 +1,2 @@ +ratelimit ~= 2.2 +sentry-sdk ~= 1.40 diff --git a/setup.py b/setup.py index 5c9c09d..e51de72 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ class Package(boilerplates.setup.Package): 'requirements_packaging_tests.txt'), 'config': boilerplates.setup.parse_requirements('requirements_config.txt'), 'logging': boilerplates.setup.parse_requirements('requirements_logging.txt'), + 'sentry': boilerplates.setup.parse_requirements('requirements_sentry.txt'), 'cli': boilerplates.setup.parse_requirements('requirements_cli.txt'), 'git_repo_tests': boilerplates.setup.parse_requirements('requirements_git_repo_tests.txt')} diff --git a/test/test_sentry.py b/test/test_sentry.py new file mode 100644 index 0000000..f988e72 --- /dev/null +++ b/test/test_sentry.py @@ -0,0 +1,15 @@ +"""Unit tests for Sentry boilerplate.""" + +import logging +import unittest + +import boilerplates.sentry + + +class SentryTests(unittest.TestCase): + + def test_init(self): + with self.assertLogs('boilerplates.sentry', logging.INFO) as context: + boilerplates.sentry.Sentry.init() + self.assertEqual(len(context.output), 1, msg=context.output) + self.assertIn('skipping Sentry SDK initialisation', context.output[0]) From 801233eaa68773aaac993f36cbb8a6f8049fa98e Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 15 Feb 2024 20:07:35 +0900 Subject: [PATCH 03/10] docs: add readme section for sentry --- README.rst | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.rst b/README.rst index 8245a4b..803eb69 100644 --- a/README.rst +++ b/README.rst @@ -252,6 +252,47 @@ And, you will need to add the following to your ``requirements.txt`` file (or eq boilerplates[logging] ~= +Sentry boilerplate +------------------ + +This boilerplate aims at simplifying the process of setting up Sentry integration +for your Python application. + +Assumptions for this boilerplate are similar to logging boilerplate, in that +you want to use the standard built-in Python +logging module (``logging``), and that your application probably has a CLI entry point +or some executable script, as opposed to only being a library. + +Then, the example ``__main__.py`` file may look like: + +.. code:: python + + """Entry point of the command-line interface.""" + + import boilerplates.sentry + + from ._version import VERSION + + + class Sentry(boilerplates.sentry.Sentry): + """Sentry configuration.""" + + release = VERSION + + + ... + + + if __name__ == '__main__': + Sentry.init() + ... + +And, you will need to add the following to your ``requirements.txt`` file (or equivalent): + +.. code:: text + + boilerplates[sentry] ~= + CLI boilerplate --------------- From 9fdfa8d271ef97c010274c8fdf8903ac730d7881 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 15 Feb 2024 20:07:47 +0900 Subject: [PATCH 04/10] chore: update copyright year --- NOTICE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE b/NOTICE index 64119da..069b0fe 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,4 @@ -Copyright (c) 2023 Mateusz Bysiek https://mbdevpl.github.io/ +Copyright (c) 2023-2024 Mateusz Bysiek https://mbdevpl.github.io/ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 1187add129f9420c70625b7c80236de4ec3b2ee1 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Mon, 4 Mar 2024 17:07:07 +0900 Subject: [PATCH 05/10] test: install sentry dependencies for tests --- requirements_test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_test.txt b/requirements_test.txt index c651256..7ea8454 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,5 +3,6 @@ -r requirements_packaging_tests.txt -r requirements_config.txt -r requirements_logging.txt +-r requirements_sentry.txt -r requirements_cli.txt -r requirements_git_repo_tests.txt From e1a8098301330db863a9c8f04a0e25db218f9270 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Mon, 4 Mar 2024 20:25:15 +0900 Subject: [PATCH 06/10] docs: add note about details to readme --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 803eb69..25e9783 100644 --- a/README.rst +++ b/README.rst @@ -287,6 +287,9 @@ Then, the example ``__main__.py`` file may look like: Sentry.init() ... +You can and should adjust the class fields to your needs, please take a look +at the ``boilerplates.sentry.Sentry`` class implementation for details. + And, you will need to add the following to your ``requirements.txt`` file (or equivalent): .. code:: text From 23d43fe28985aa01f08bb96ed5557acc32c4c888 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Mon, 4 Mar 2024 20:46:46 +0900 Subject: [PATCH 07/10] test: mock sentry_sdk.init to improve code coverage --- test/test_sentry.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/test_sentry.py b/test/test_sentry.py index f988e72..54c4c50 100644 --- a/test/test_sentry.py +++ b/test/test_sentry.py @@ -2,6 +2,7 @@ import logging import unittest +import unittest.mock import boilerplates.sentry @@ -13,3 +14,21 @@ def test_init(self): boilerplates.sentry.Sentry.init() self.assertEqual(len(context.output), 1, msg=context.output) self.assertIn('skipping Sentry SDK initialisation', context.output[0]) + + def test_init_without_dsn(self): + class Sentry(boilerplates.sentry.Sentry): + dsn = '' + with self.assertLogs('boilerplates.sentry', logging.INFO) as context, \ + unittest.mock.patch('sentry_sdk.init') as sentry_sdk_init_mock: + Sentry.init() + sentry_sdk_init_mock.assert_not_called() + self.assertEqual(len(context.output), 1, msg=context.output) + self.assertIn('skipping Sentry SDK initialisation', context.output[0]) + + def test_init_with_dsn(self): + class Sentry(boilerplates.sentry.Sentry): + dsn = 'https://spam@ham.ingest.sentry.io/eggs' + with self.assertNoLogs('boilerplates.sentry', logging.INFO), \ + unittest.mock.patch('sentry_sdk.init') as sentry_sdk_init_mock: + Sentry.init() + sentry_sdk_init_mock.assert_called_once() From a5d454cf985e1f3fc21501152229712a01f7c60a Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Mon, 4 Mar 2024 21:03:34 +0900 Subject: [PATCH 08/10] test: assertNoLogs needs Python >= 3.10 --- test/test_sentry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_sentry.py b/test/test_sentry.py index 54c4c50..5ae54a1 100644 --- a/test/test_sentry.py +++ b/test/test_sentry.py @@ -1,6 +1,7 @@ """Unit tests for Sentry boilerplate.""" import logging +import sys import unittest import unittest.mock @@ -25,6 +26,7 @@ class Sentry(boilerplates.sentry.Sentry): self.assertEqual(len(context.output), 1, msg=context.output) self.assertIn('skipping Sentry SDK initialisation', context.output[0]) + @unittest.skipUnless(sys.version_info >= (3, 10), 'this test requires Python 3.10') def test_init_with_dsn(self): class Sentry(boilerplates.sentry.Sentry): dsn = 'https://spam@ham.ingest.sentry.io/eggs' From 8d4451e5625e205dbfe27dc3525850fd78081625 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Tue, 5 Mar 2024 22:16:46 +0900 Subject: [PATCH 09/10] feat: add more sentry integrations --- boilerplates/sentry.py | 28 ++++++++++++++++++++++++---- requirements_sentry.txt | 3 +-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/boilerplates/sentry.py b/boilerplates/sentry.py index 5ce7c59..f288958 100644 --- a/boilerplates/sentry.py +++ b/boilerplates/sentry.py @@ -5,7 +5,14 @@ import typing as t import sentry_sdk +import sentry_sdk.integrations +import sentry_sdk.integrations.argv +import sentry_sdk.integrations.excepthook import sentry_sdk.integrations.logging +import sentry_sdk.integrations.modules +import sentry_sdk.integrations.pure_eval +import sentry_sdk.integrations.stdlib +import sentry_sdk.integrations.threading _LOG = logging.getLogger(__name__) @@ -23,6 +30,19 @@ class Sentry: dsn: str release: str environment: str + integrations: t.List[sentry_sdk.integrations.Integration] = [ + sentry_sdk.integrations.argv.ArgvIntegration(), + sentry_sdk.integrations.excepthook.ExcepthookIntegration(always_run=False), + sentry_sdk.integrations.logging.LoggingIntegration( + level=logging.INFO, event_level=logging.ERROR), + sentry_sdk.integrations.modules.ModulesIntegration(), + sentry_sdk.integrations.pure_eval.PureEvalIntegration(), + sentry_sdk.integrations.stdlib.StdlibIntegration(), + sentry_sdk.integrations.threading.ThreadingIntegration() + ] + + traces_sample_rate: float = 1.0 + profiles_sample_rate: float = 1.0 @classmethod def _get_param(cls, param_name: str) -> t.Optional[str]: @@ -39,8 +59,8 @@ def init(cls, *args, **kwargs): sentry_sdk.init( *args, dsn=dsn, release=cls._get_param('release'), environment=cls._get_param('environment'), - integrations=[ - sentry_sdk.integrations.logging.LoggingIntegration( - level=logging.INFO, event_level=logging.ERROR)], - traces_sample_rate=1.0, profiles_sample_rate=1.0, enable_tracing=True, + integrations=cls.integrations, + traces_sample_rate=cls.traces_sample_rate, + profiles_sample_rate=cls.profiles_sample_rate, + enable_tracing=cls.profiles_sample_rate > 0, **kwargs) diff --git a/requirements_sentry.txt b/requirements_sentry.txt index 1ac3ae0..dbc74e1 100644 --- a/requirements_sentry.txt +++ b/requirements_sentry.txt @@ -1,2 +1 @@ -ratelimit ~= 2.2 -sentry-sdk ~= 1.40 +sentry-sdk[pure_eval] ~= 1.40 From 3c6c227d21744ad1bbe4be2d07e9adf60293cf99 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Apr 2024 15:49:20 +0900 Subject: [PATCH 10/10] refactor: clean up initial implementation --- boilerplates/sentry.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/boilerplates/sentry.py b/boilerplates/sentry.py index f288958..fd7f426 100644 --- a/boilerplates/sentry.py +++ b/boilerplates/sentry.py @@ -45,20 +45,29 @@ class Sentry: profiles_sample_rate: float = 1.0 @classmethod - def _get_param(cls, param_name: str) -> t.Optional[str]: - """Get a parameter value by checking envvar first.""" + def _get_str_param(cls, param_name: str) -> t.Optional[str]: + """Get a string parameter value by checking envvar first. + + Works only for 'dsn', 'release' or 'environment' parameters. + """ + assert param_name in ('dsn', 'release', 'environment'), param_name return os.environ.get(f'SENTRY_{param_name.upper()}', getattr(cls, param_name, None)) + @classmethod + def is_dsn_set(cls) -> bool: + """Check if Sentry DSN parameter is set, thus if Sentry SDK should be initialised or not.""" + dsn = cls._get_str_param('dsn') + return dsn is not None and len(dsn) > 0 + @classmethod def init(cls, *args, **kwargs): """Initialise Sentry SDK.""" - dsn = cls._get_param('dsn') - if dsn is None or not dsn: + if not cls.is_dsn_set(): _LOG.info('Sentry DSN is not set, skipping Sentry SDK initialisation') return sentry_sdk.init( - *args, dsn=dsn, - release=cls._get_param('release'), environment=cls._get_param('environment'), + *args, dsn=cls._get_str_param('dsn'), + release=cls._get_str_param('release'), environment=cls._get_str_param('environment'), integrations=cls.integrations, traces_sample_rate=cls.traces_sample_rate, profiles_sample_rate=cls.profiles_sample_rate,