Skip to content

Commit

Permalink
Merge pull request #4 from mbdevpl/feature/sentry
Browse files Browse the repository at this point in the history
add Sentry boilerplate
  • Loading branch information
mbdevpl authored Apr 17, 2024
2 parents f8ab06a + 3c6c227 commit bd97bb9
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 3 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
49 changes: 47 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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:

Expand All @@ -251,6 +252,50 @@ And, you will need to add the following to your ``requirements.txt`` file (or eq
boilerplates[logging] ~= <version>
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()
...
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
boilerplates[sentry] ~= <version>
CLI boilerplate
---------------

Expand Down
75 changes: 75 additions & 0 deletions boilerplates/sentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Boilerplate for integrating Sentry into the project."""

import logging
import os
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__)


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
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_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."""
if not cls.is_dsn_set():
_LOG.info('Sentry DSN is not set, skipping Sentry SDK initialisation')
return
sentry_sdk.init(
*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,
enable_tracing=cls.profiles_sample_rate > 0,
**kwargs)
1 change: 1 addition & 0 deletions requirements_sentry.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sentry-sdk[pure_eval] ~= 1.40
1 change: 1 addition & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')}

Expand Down
36 changes: 36 additions & 0 deletions test/test_sentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Unit tests for Sentry boilerplate."""

import logging
import sys
import unittest
import unittest.mock

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])

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])

@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'
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()

0 comments on commit bd97bb9

Please sign in to comment.