diff --git a/requirements.txt b/requirements.txt index b9c961619..f33651823 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -raven +sentry-sdk openpyxl xlrd xlwt diff --git a/sentry/README.rst b/sentry/README.rst index 46adbbea8..6b04741d6 100644 --- a/sentry/README.rst +++ b/sentry/README.rst @@ -1,33 +1,33 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - ====== Sentry ====== -This module allows painless `Sentry `__ integration with -Odoo. - -Installation -============ - -The module can be installed just like any other Odoo module, by adding the -module's directory to Odoo *addons_path*. In order for the module to correctly -wrap the Odoo WSGI application, it also needs to be loaded as a server-wide -module. This can be done with the ``server_wide_modules`` parameter in your -Odoo config file or with the ``--load`` command-line parameter. - -This module additionally requires the raven_ Python package to be available on -the system. It can be installed using pip:: - - pip install raven +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/12.0-migsentrynewapi-fer/sentry + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-12-0-migsentrynewapi-fer/server-tools-12-0-migsentrynewapi-fer-sentry + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/149/12.0-migsentrynewapi-fer + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| -Configuration -============= -The following additional configuration options can be added to your Odoo -configuration file: +This module allows painless `Sentry `__ integration with +Odoo. ============================= ==================================================================== ========================================================== Option Description Default @@ -57,16 +57,6 @@ configuration file: odoo.exceptions.Warning, odoo.exceptions.except_orm`` -``sentry_processors`` A string of comma-separated processor classes which will be applied ``raven.processors.SanitizePasswordsProcessor, - on an event before sending it to Sentry. odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor`` - -``sentry_transport`` Transport class which will be used to send events to Sentry. ``threaded`` - Possible values: *threaded*: spawns an async worker for processing - messages, *synchronous*: a synchronous blocking transport; - *requests_threaded*: an asynchronous transport using the *requests* - library; *requests_synchronous* - blocking transport using the - *requests* library. - ``sentry_include_context`` If enabled, additional context data will be extracted from current ``True`` HTTP request and user session (if available). This has no effect for Cron jobs, as no request/session is available inside a Cron job. @@ -84,11 +74,14 @@ configuration file: ============================= ==================================================================== ========================================================== Other `client arguments -`_ can be +`_ can be configured by prepending the argument name with *sentry_* in your Odoo config -file. Currently supported additional client arguments are: ``install_sys_hook, -include_paths, exclude_paths, machine, auto_log_stacks, capture_locals, -string_max_length, list_max_length, site, include_versions, environment``. +file. Currently supported additional client arguments are: ``with_locals, +max_breadcrumbs, release, environment, server_name, shutdown_timeout, +in_app_include, in_app_exclude, default_integrations, dist, sample_rate, +send_default_pii, http_proxy, https_proxy, request_bodies, debug, +attach_stacktrace, ca_certs, propagate_traces, traces_sample_rate, +auto_enabling_integrations``. Example Odoo configuration -------------------------- @@ -100,14 +93,42 @@ Below is an example of Odoo configuration file with *Odoo Sentry* options:: sentry_enabled = true sentry_logging_level = warn sentry_exclude_loggers = werkzeug - sentry_ignore_exceptions = odoo.exceptions.AccessDenied,odoo.exceptions.AccessError,odoo.exceptions.MissingError,odoo.exceptions.RedirectWarning,odoo.exceptions.UserError,odoo.exceptions.ValidationError,odoo.exceptions.Warning,odoo.exceptions.except_orm - sentry_processors = raven.processors.SanitizePasswordsProcessor,odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor - sentry_transport = threaded + sentry_ignore_exceptions = odoo.exceptions.AccessDenied, + odoo.exceptions.AccessError,odoo.exceptions.MissingError, + odoo.exceptions.RedirectWarning,odoo.exceptions.UserError, + odoo.exceptions.ValidationError,odoo.exceptions.Warning, + odoo.exceptions.except_orm sentry_include_context = true sentry_environment = production - sentry_auto_log_stacks = false - sentry_odoo_dir = /home/odoo/odoo/ sentry_release = 1.3.2 + sentry_odoo_dir = /home/odoo/odoo/ + + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +The module can be installed just like any other Odoo module, by adding the +module's directory to Odoo *addons_path*. In order for the module to correctly +wrap the Odoo WSGI application, it also needs to be loaded as a server-wide +module. This can be done with the ``server_wide_modules`` parameter in your +Odoo config file or with the ``--load`` command-line parameter. + +This module additionally requires the sentry-sdk Python package to be available on +the system. It can be installed using pip:: + + pip install sentry-sdk + +Configuration +============= + +The following additional configuration options can be added to your Odoo +configuration file: + Usage ===== @@ -137,41 +158,70 @@ Known issues / Roadmap Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= -Images ------- +Authors +~~~~~~~ -* `Module Icon `_ +* Mohammed Barsi +* Versada +* Nicolas JEUDY +* Vauxoo Contributors ------------- +~~~~~~~~~~~~ * Mohammed Barsi * Andrius Preimantas * Naglis Jonaitis * Atte Isopuro -Maintainer ----------- +Other credits +~~~~~~~~~~~~~ + +* Vauxoo + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit https://odoo-community.org. - - -.. _raven: https://github.com/getsentry/raven-python +.. |maintainer-barsi| image:: https://github.com/barsi.png?size=40px + :target: https://github.com/barsi + :alt: barsi +.. |maintainer-naglis| image:: https://github.com/naglis.png?size=40px + :target: https://github.com/naglis + :alt: naglis +.. |maintainer-versada| image:: https://github.com/versada.png?size=40px + :target: https://github.com/versada + :alt: versada +.. |maintainer-moylop260| image:: https://github.com/moylop260.png?size=40px + :target: https://github.com/moylop260 + :alt: moylop260 +.. |maintainer-fernandahf| image:: https://github.com/fernandahf.png?size=40px + :target: https://github.com/fernandahf + :alt: fernandahf + +Current `maintainers `__: + +|maintainer-barsi| |maintainer-naglis| |maintainer-versada| |maintainer-moylop260| |maintainer-fernandahf| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sentry/__init__.py b/sentry/__init__.py index 968f6ae31..7001103db 100644 --- a/sentry/__init__.py +++ b/sentry/__init__.py @@ -1,82 +1 @@ -# Copyright 2016-2017 Versada -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import logging - -from odoo.service import wsgi_server -from odoo.tools import config as odoo_config - -from . import const -from .logutils import LoggerNameFilter, OdooSentryHandler - -import collections - -_logger = logging.getLogger(__name__) -HAS_RAVEN = True -try: - import raven - from raven.middleware import Sentry -except ImportError: - HAS_RAVEN = False - _logger.debug('Cannot import "raven". Please make sure it is installed.') - - -def get_odoo_commit(odoo_dir): - '''Attempts to get Odoo git commit from :param:`odoo_dir`.''' - if not odoo_dir: - return - try: - return raven.fetch_git_sha(odoo_dir) - except raven.exceptions.InvalidGitRepository: - _logger.debug( - 'Odoo directory: "%s" not a valid git repository', odoo_dir) - - -def initialize_raven(config, client_cls=None): - ''' - Setup an instance of :class:`raven.Client`. - - :param config: Sentry configuration - :param client: class used to instantiate the raven client. - ''' - enabled = config.get('sentry_enabled', False) - if not (HAS_RAVEN and enabled): - return - - if config.get('sentry_odoo_dir') and config.get('sentry_release'): - _logger.debug('Both sentry_odoo_dir and sentry_release defined, choosing sentry_release') - options = { - 'release': config.get('sentry_release', get_odoo_commit(config.get('sentry_odoo_dir'))), - } - for option in const.get_sentry_options(): - value = config.get('sentry_%s' % option.key, option.default) - if isinstance(option.converter, collections.Callable): - value = option.converter(value) - options[option.key] = value - - level = config.get('sentry_logging_level', const.DEFAULT_LOG_LEVEL) - exclude_loggers = const.split_multiple( - config.get('sentry_exclude_loggers', const.DEFAULT_EXCLUDE_LOGGERS) - ) - if level not in const.LOG_LEVEL_MAP: - level = const.DEFAULT_LOG_LEVEL - - client_cls = client_cls or raven.Client - client = client_cls(**options) - handler = OdooSentryHandler( - config.get('sentry_include_context', True), - client=client, - level=const.LOG_LEVEL_MAP[level], - ) - if exclude_loggers: - handler.addFilter(LoggerNameFilter( - exclude_loggers, name='sentry.logger.filter')) - raven.conf.setup_logging(handler) - wsgi_server.application = Sentry( - wsgi_server.application, client=client) - - client.captureMessage('Starting Odoo Server') - return client - - -sentry_client = initialize_raven(odoo_config) +from .hooks import post_load diff --git a/sentry/__manifest__.py b/sentry/__manifest__.py index d4471b69e..5a0e7200e 100644 --- a/sentry/__manifest__.py +++ b/sentry/__manifest__.py @@ -9,16 +9,19 @@ 'author': 'Mohammed Barsi,' 'Versada,' 'Nicolas JEUDY,' - 'Odoo Community Association (OCA)', + 'Odoo Community Association (OCA),' + 'Vauxoo', + 'maintainers': ['barsi', 'naglis', 'versada', 'moylop260', 'fernandahf'], 'license': 'AGPL-3', 'application': False, 'installable': True, 'external_dependencies': { 'python': [ - 'raven', + 'sentry_sdk', ] }, 'depends': [ 'base', ], + 'post_load': 'post_load', } diff --git a/sentry/const.py b/sentry/const.py index 0092d7544..c7cdfe6ef 100644 --- a/sentry/const.py +++ b/sentry/const.py @@ -1,21 +1,18 @@ # Copyright 2016-2017 Versada # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - import collections import logging +import warnings import odoo.loglevels -_logger = logging.getLogger(__name__) -try: - import raven - from raven.conf import defaults -except ImportError: - _logger.debug('Cannot import "raven". Please make sure it is installed.') +from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk import HttpTransport +from sentry_sdk.consts import DEFAULT_OPTIONS def split_multiple(string, delimiter=',', strip_chars=None): - '''Splits :param:`string` and strips :param:`strip_chars` from values.''' + """Splits :param:`string` and strips :param:`strip_chars` from values.""" if not string: return [] return [v.strip(strip_chars) for v in string.split(delimiter)] @@ -29,6 +26,7 @@ LOG_LEVEL_MAP = dict([ (getattr(odoo.loglevels, 'LOG_%s' % x), getattr(logging, x)) for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET') ]) + DEFAULT_LOG_LEVEL = 'warn' ODOO_USER_EXCEPTIONS = [ @@ -44,46 +42,83 @@ ODOO_USER_EXCEPTIONS = [ ] DEFAULT_IGNORED_EXCEPTIONS = ','.join(ODOO_USER_EXCEPTIONS) -PROCESSORS = ( - 'raven.processors.SanitizePasswordsProcessor', - 'odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor', -) -DEFAULT_PROCESSORS = ','.join(PROCESSORS) - EXCLUDE_LOGGERS = ( 'werkzeug', ) DEFAULT_EXCLUDE_LOGGERS = ','.join(EXCLUDE_LOGGERS) +DEFAULT_ENVIRONMENT = 'develop' + DEFAULT_TRANSPORT = 'threaded' def select_transport(name=DEFAULT_TRANSPORT): + warnings.warn( + "`sentry_transport` has been deprecated. " + "Its not neccesary send it, will use `HttpTranport` by default.", + DeprecationWarning, + ) return { - 'requests_synchronous': raven.transport.RequestsHTTPTransport, - 'requests_threaded': raven.transport.ThreadedRequestsHTTPTransport, - 'synchronous': raven.transport.HTTPTransport, - 'threaded': raven.transport.ThreadedHTTPTransport, - }.get(name, DEFAULT_TRANSPORT) + 'threaded': HttpTransport, + }.get(name, HttpTransport) + + +def get_sentry_logging(level=DEFAULT_LOG_LEVEL): + if level not in LOG_LEVEL_MAP: + level = DEFAULT_LOG_LEVEL + + return LoggingIntegration( + level=LOG_LEVEL_MAP[level], + event_level=logging.WARNING + ) def get_sentry_options(): return [ SentryOption('dsn', '', str.strip), - SentryOption('install_sys_hook', False, None), - SentryOption('transport', DEFAULT_TRANSPORT, select_transport), - SentryOption('include_paths', '', split_multiple), - SentryOption('exclude_paths', '', split_multiple), - SentryOption('machine', defaults.NAME, None), - SentryOption('auto_log_stacks', defaults.AUTO_LOG_STACKS, None), - SentryOption('capture_locals', defaults.CAPTURE_LOCALS, None), - SentryOption('string_max_length', defaults.MAX_LENGTH_STRING, None), - SentryOption('list_max_length', defaults.MAX_LENGTH_LIST, None), - SentryOption('site', None, None), - SentryOption('include_versions', True, None), + SentryOption('transport', + DEFAULT_OPTIONS['transport'], select_transport), + SentryOption('logging_level', + DEFAULT_LOG_LEVEL, get_sentry_logging), + SentryOption( + 'with_locals', DEFAULT_OPTIONS['with_locals'], None), + SentryOption( + 'max_breadcrumbs', DEFAULT_OPTIONS['max_breadcrumbs'], None), + SentryOption('release', DEFAULT_OPTIONS['release'], None), + SentryOption( + 'environment', DEFAULT_OPTIONS['environment'], None), SentryOption( - 'ignore_exceptions', DEFAULT_IGNORED_EXCEPTIONS, split_multiple), - SentryOption('processors', DEFAULT_PROCESSORS, split_multiple), - SentryOption('environment', None, None), - SentryOption('release', None, None), + 'server_name', DEFAULT_OPTIONS['server_name'], None), + SentryOption('shutdown_timeout', + DEFAULT_OPTIONS['shutdown_timeout'], None), + SentryOption('integrations', + DEFAULT_OPTIONS['integrations'], None), + SentryOption('in_app_include', + DEFAULT_OPTIONS['in_app_include'], split_multiple), + SentryOption('in_app_exclude', + DEFAULT_OPTIONS['in_app_exclude'], split_multiple), + SentryOption('default_integrations', + DEFAULT_OPTIONS['default_integrations'], None), + SentryOption('dist', DEFAULT_OPTIONS['dist'], None), + SentryOption('sample_rate', + DEFAULT_OPTIONS['sample_rate'], None), + SentryOption('send_default_pii', + DEFAULT_OPTIONS['send_default_pii'], None), + SentryOption('http_proxy', + DEFAULT_OPTIONS['http_proxy'], None), + SentryOption('https_proxy', + DEFAULT_OPTIONS['https_proxy'], None), + SentryOption('ignore_exceptions', + DEFAULT_IGNORED_EXCEPTIONS, split_multiple), + SentryOption('request_bodies', + DEFAULT_OPTIONS['request_bodies'], None), + SentryOption('attach_stacktrace', + DEFAULT_OPTIONS['attach_stacktrace'], None), + SentryOption('ca_certs', DEFAULT_OPTIONS['ca_certs'], None), + SentryOption('propagate_traces', + DEFAULT_OPTIONS['propagate_traces'], None), + SentryOption('traces_sample_rate', + DEFAULT_OPTIONS['traces_sample_rate'], None), + SentryOption('auto_enabling_integrations', + DEFAULT_OPTIONS['auto_enabling_integrations'], None), ] diff --git a/sentry/generalutils.py b/sentry/generalutils.py new file mode 100644 index 000000000..802c81fa2 --- /dev/null +++ b/sentry/generalutils.py @@ -0,0 +1,63 @@ +try: + from collections.abc import Mapping +except ImportError: # pragma: no cover + # Python < 3.3 + from collections import Mapping # pragma: no cover + + +def string_types(): + """ Taken from https://git.io/JIv5J """ + + return str, + + +def is_namedtuple(value): + """ https://stackoverflow.com/a/2166841/1843746 + But modified to handle subclasses of namedtuples. + Taken from https://git.io/JIsfY + """ + if not isinstance(value, tuple): + return False + f = getattr(type(value), '_fields', None) + if not isinstance(f, tuple): + return False + return all(type(n) == str for n in f) + + +def iteritems(d, **kw): + """ Override iteritems for support multiple versions python. + Taken from https://git.io/JIvMi + """ + return iter(d.items(**kw)) + + +def varmap(func, var, context=None, name=None): + """ Executes ``func(key_name, value)`` on all values + recurisively discovering dict and list scoped + values. Taken from https://git.io/JIvMN + """ + if context is None: + context = {} + objid = id(var) + if objid in context: + return func(name, '<...>') + context[objid] = 1 + + if isinstance(var, (list, tuple)) and not is_namedtuple(var): + ret = [varmap(func, f, context, name) for f in var] + else: + ret = func(name, var) + if isinstance(ret, Mapping): + ret = dict((k, varmap(func, v, context, k)) + for k, v in iteritems(var)) + del context[objid] + return ret + + +def get_environ(environ): + """ Returns our whitelisted environment variables. + Taken from https://git.io/JIsf2 + """ + for key in ('REMOTE_ADDR', 'SERVER_NAME', 'SERVER_PORT'): + if key in environ: + yield key, environ[key] diff --git a/sentry/hooks.py b/sentry/hooks.py new file mode 100644 index 000000000..e4e9198a6 --- /dev/null +++ b/sentry/hooks.py @@ -0,0 +1,113 @@ +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +import odoo.http +from odoo.service import wsgi_server +from odoo.tools import config as odoo_config + +from . import const +from .logutils import SanitizeOdooCookiesProcessor, fetch_git_sha +from .logutils import InvalidGitRepository, get_extra_context + +import collections + +_logger = logging.getLogger(__name__) +HAS_SENTRY_SDK = True +try: + import sentry_sdk + from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware + from sentry_sdk.integrations.threading import ThreadingIntegration + from sentry_sdk.integrations.logging import ignore_logger +except ImportError: # pragma: no cover + HAS_SENTRY_SDK = False # pragma: no cover + _logger.debug("Cannot import 'sentry-sdk'.\ + Please make sure it is installed.") # pragma: no cover + + +def before_send(event, hint): + """ Add context to event if include_context is True + and sanitize sensitive data """ + if event.setdefault("tags", {})["include_context"]: + cxtest = get_extra_context(odoo.http.request) + info_request = ["tags", "user", "extra", "request"] + + for item in info_request: + info_item = event.setdefault(item, {}) + info_item.update(cxtest.setdefault(item, {})) + + raven_processor = SanitizeOdooCookiesProcessor() + raven_processor.process(event) + + return event + + +def get_odoo_commit(odoo_dir): + """Attempts to get Odoo git commit from :param:`odoo_dir`.""" + if not odoo_dir: + return + try: + return fetch_git_sha(odoo_dir) + except InvalidGitRepository: + _logger.debug( + "Odoo directory: '%s' not a valid git repository", odoo_dir) + + +def initialize_sentry(config): + """ Setup an instance of :class:`sentry_sdk.Client`. + :param config: Sentry configuration + :param client: class used to instantiate the sentry_sdk client. + """ + enabled = config.get("sentry_enabled", False) + if not (HAS_SENTRY_SDK and enabled): + return + _logger.info("Initializing sentry...") + if config.get("sentry_odoo_dir") and config.get("sentry_release"): + _logger.debug("Both sentry_odoo_dir and \ + sentry_release defined, choosing sentry_release") + options = { + "release": config.get("sentry_release", + get_odoo_commit(config.get("sentry_odoo_dir"))), + } + for option in const.get_sentry_options(): + value = config.get('sentry_%s' % option.key, option.default) + if isinstance(option.converter, collections.Callable): + value = option.converter(value) + options[option.key] = value + + exclude_loggers = const.split_multiple( + config.get("sentry_exclude_loggers", const.DEFAULT_EXCLUDE_LOGGERS) + ) + # Change name `ignore_exceptions` (with raven) + # to `ignore_errors' (sentry_sdk) + options["ignore_errors"] = options["ignore_exceptions"] + del options["ignore_exceptions"] + + options["before_send"] = before_send + + options["integrations"] = [options["logging_level"], + ThreadingIntegration(propagate_hub=True)] + # Remove logging_level, since in sentry_sdk is include in 'integrations' + del options["logging_level"] + + client = sentry_sdk.init(**options) + + sentry_sdk.set_tag("include_context", + config.get("sentry_include_context", True)) + + if exclude_loggers: + for item in exclude_loggers: + ignore_logger(item) + + wsgi_server.application = SentryWsgiMiddleware(wsgi_server.application) + + with sentry_sdk.push_scope() as scope: + scope.set_extra("debug", False) + sentry_sdk.capture_message("Starting Odoo Server", "info") + + return client + + +def post_load(): + initialize_sentry(odoo_config) diff --git a/sentry/logutils.py b/sentry/logutils.py index cef132bd7..25b304b6c 100644 --- a/sentry/logutils.py +++ b/sentry/logutils.py @@ -1,43 +1,33 @@ # Copyright 2016-2017 Versada # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import logging +import os.path import urllib.parse -import odoo.http - -_logger = logging.getLogger(__name__) -try: - from raven.handlers.logging import SentryHandler - from raven.processors import SanitizePasswordsProcessor - from raven.utils.wsgi import get_environ, get_headers -except ImportError: - _logger.debug('Cannot import "raven". Please make sure it is installed.') - SentryHandler = object - SanitizePasswordsProcessor = object +from .processor import SanitizePasswordsProcessor +from .generalutils import get_environ +from sentry_sdk._compat import text_type +from werkzeug import datastructures def get_request_info(request): - ''' - Returns context data extracted from :param:`request`. - - Heavily based on flask integration for Sentry: https://git.io/vP4i9. - ''' + """ Returns context data extracted from :param:`request`. + Heavily based on flask integration for Sentry: https://git.io/vP4i9. + """ urlparts = urllib.parse.urlsplit(request.url) return { 'url': '%s://%s%s' % (urlparts.scheme, urlparts.netloc, urlparts.path), 'query_string': urlparts.query, 'method': request.method, - 'headers': dict(get_headers(request.environ)), + 'headers': dict(datastructures.EnvironHeaders(request.environ)), 'env': dict(get_environ(request.environ)), } -def get_extra_context(): - ''' - Extracts additional context from the current request (if such is set). - ''' - request = odoo.http.request +def get_extra_context(request): + """ Extracts additional context from the current request + (if such is set). + """ try: session = getattr(request, 'session', {}) except RuntimeError: @@ -48,8 +38,8 @@ def get_extra_context(): 'database': session.get('db', None), }, 'user': { - 'login': session.get('login', None), - 'uid': session.get('uid', None), + 'email': session.get('login', None), + 'id': session.get('uid', None), }, 'extra': { 'context': session.get('context', {}), @@ -62,44 +52,65 @@ def get_extra_context(): return ctx -class LoggerNameFilter(logging.Filter): - ''' - Custom :class:`logging.Filter` which allows to filter loggers by name. - ''' - - def __init__(self, loggers, name=''): - super(LoggerNameFilter, self).__init__(name=name) - self._exclude_loggers = set(loggers) - - def filter(self, event): - return event.name not in self._exclude_loggers - - -class OdooSentryHandler(SentryHandler): - ''' - Customized :class:`raven.handlers.logging.SentryHandler`. +class SanitizeOdooCookiesProcessor(SanitizePasswordsProcessor): + """ Custom :class:`raven.processors.Processor`. + Allows to sanitize sensitive Odoo cookies, namely the "session_id" cookie. + """ - Allows to add additional Odoo and HTTP request data to the event which is - sent to Sentry. - ''' + KEYS = frozenset([ + 'session_id', + ]) - def __init__(self, include_extra_context, *args, **kwargs): - super(OdooSentryHandler, self).__init__(*args, **kwargs) - self.include_extra_context = include_extra_context - def emit(self, record): - if self.include_extra_context: - self.client.context.merge(get_extra_context()) - return super(OdooSentryHandler, self).emit(record) +class InvalidGitRepository(Exception): + pass -class SanitizeOdooCookiesProcessor(SanitizePasswordsProcessor): - ''' - Custom :class:`raven.processors.Processor`. +def fetch_git_sha(path, head=None): + """ >>> fetch_git_sha(os.path.dirname(__file__)) + Taken from https://git.io/JITmC + """ + if not head: + head_path = os.path.join(path, '.git', 'HEAD') + if not os.path.exists(head_path): + raise InvalidGitRepository( + 'Cannot identify HEAD for git repository at %s' % (path,)) - Allows to sanitize sensitive Odoo cookies, namely the "session_id" cookie. - ''' + with open(head_path, 'r') as fp: + head = text_type(fp.read()).strip() - KEYS = FIELDS = frozenset([ - 'session_id', - ]) + if head.startswith('ref: '): + head = head[5:] + revision_file = os.path.join( + path, '.git', *head.split('/') + ) + else: + return head + else: + revision_file = os.path.join(path, '.git', 'refs', 'heads', head) + + if not os.path.exists(revision_file): + if not os.path.exists(os.path.join(path, '.git')): + raise InvalidGitRepository( + '%s does not seem to be the root of a git repository' % (path,)) + + # Check for our .git/packed-refs' file since a `git gc` may have run + # https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery + packed_file = os.path.join(path, '.git', 'packed-refs') + if os.path.exists(packed_file): + with open(packed_file) as fh: + for line in fh: + line = line.rstrip() + if line and line[:1] not in ('#', '^'): + try: + revision, ref = line.split(' ', 1) + except ValueError: + continue + if ref == head: + return text_type(revision) + + raise InvalidGitRepository( + 'Unable to find ref to head "%s" in repository' % (head,)) + + with open(revision_file) as fh: + return text_type(fh.read()).strip() diff --git a/sentry/processor.py b/sentry/processor.py new file mode 100644 index 000000000..df2019689 --- /dev/null +++ b/sentry/processor.py @@ -0,0 +1,137 @@ +""" Custom class of raven.core.processors taken of https://git.io/JITko + This is a custom class of processor to filter and sanitize + passwords and keys from request data, it does not exist in + sentry-sdk. +""" + +from __future__ import absolute_import + +import re + +from sentry_sdk._compat import text_type +from .generalutils import string_types, varmap + + +class SanitizeKeysProcessor(object): + """ Class from raven for sanitize keys, cookies, etc + Asterisk out things that correspond to a configurable set of keys. """ + + MASK = '*' * 8 + + def process(self, data, **kwargs): + if 'exception' in data: + if 'values' in data['exception']: + for value in data['exception'].get('values', []): + if 'stacktrace' in value: + self.filter_stacktrace(value['stacktrace']) + + if 'request' in data: + self.filter_http(data['request']) + + if 'extra' in data: + data['extra'] = self.filter_extra(data['extra']) + + if 'level' in data: + data['level'] = self.filter_level(data['level']) + + return data + + @property + def sanitize_keys(self): + pass + + def sanitize(self, item, value): + if value is None: + return + + if not item: # key can be a NoneType + return value + + # Just in case we have bytes here, we want to make them into text + # properly without failing so we can perform our check. + if isinstance(item, bytes): + item = item.decode('utf-8', 'replace') + else: + item = text_type(item) + + item = item.lower() + for key in self.sanitize_keys: + if key in item: + # store mask as a fixed length for security + return self.MASK + return value + + def filter_stacktrace(self, data): + for frame in data.get('frames', []): + if 'vars' not in frame: + continue + frame['vars'] = varmap(self.sanitize, frame['vars']) + + def filter_http(self, data): + for n in ('data', 'cookies', 'headers', 'env', 'query_string'): + if n not in data: + continue + + # data could be provided as bytes and if it's python3 + if isinstance(data[n], bytes): + data[n] = data[n].decode('utf-8', 'replace') + + if isinstance(data[n], string_types()) and '=' in data[n]: + # at this point we've assumed it's a standard HTTP query + # or cookie + if n == 'cookies': + delimiter = ';' + else: + delimiter = '&' + + data[n] = self._sanitize_keyvals(data[n], delimiter) + else: + data[n] = varmap(self.sanitize, data[n]) + if n == 'headers' and 'Cookie' in data[n]: + data[n]['Cookie'] = self._sanitize_keyvals( + data[n]['Cookie'], ';' + ) + + def filter_extra(self, data): + return varmap(self.sanitize, data) + + def filter_level(self, data): + return re.sub(r'\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))', '', data) + + def _sanitize_keyvals(self, keyvals, delimiter): + sanitized_keyvals = [] + for keyval in keyvals.split(delimiter): + keyval = keyval.split('=') + if len(keyval) == 2: + sanitized_keyvals.append((keyval[0], self.sanitize(*keyval))) + else: + sanitized_keyvals.append(keyval) + + return delimiter.join('='.join(keyval) for keyval in sanitized_keyvals) + + +class SanitizePasswordsProcessor(SanitizeKeysProcessor): + """ Asterisk out things that look like passwords, credit card numbers, + and API keys in frames, http, and basic extra data. """ + + KEYS = frozenset([ + 'password', + 'secret', + 'passwd', + 'authorization', + 'api_key', + 'apikey', + 'sentry_dsn', + 'access_token', + ]) + VALUES_RE = re.compile(r'^(?:\d[ -]*?){13,16}$') + + @property + def sanitize_keys(self): + return self.KEYS + + def sanitize(self, item, value): + value = super(SanitizePasswordsProcessor, self).sanitize(item, value) + if isinstance(value, string_types()) and self.VALUES_RE.match(value): + return self.MASK + return value diff --git a/sentry/readme/CONFIGURE.rst b/sentry/readme/CONFIGURE.rst new file mode 100644 index 000000000..9f1b0a4d1 --- /dev/null +++ b/sentry/readme/CONFIGURE.rst @@ -0,0 +1,3 @@ +The following additional configuration options can be added to your Odoo +configuration file: + diff --git a/sentry/readme/CONTRIBUTORS.rst b/sentry/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..059d6b3a6 --- /dev/null +++ b/sentry/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* Mohammed Barsi +* Andrius Preimantas +* Naglis Jonaitis +* Atte Isopuro diff --git a/sentry/readme/CREDITS.rst b/sentry/readme/CREDITS.rst new file mode 100644 index 000000000..7f8b9f7ab --- /dev/null +++ b/sentry/readme/CREDITS.rst @@ -0,0 +1 @@ +* Vauxoo diff --git a/sentry/readme/DESCRIPTION.rst b/sentry/readme/DESCRIPTION.rst new file mode 100644 index 000000000..89ed269f7 --- /dev/null +++ b/sentry/readme/DESCRIPTION.rst @@ -0,0 +1,76 @@ +This module allows painless `Sentry `__ integration with +Odoo. + +============================= ==================================================================== ========================================================== + Option Description Default +============================= ==================================================================== ========================================================== +``sentry_dsn`` Sentry *Data Source Name*. You can find this value in your Sentry ``''`` + project configuration. Typically it looks something like this: + *https://:@sentry.example.com/* + This is the only required option in order to use the module. + +``sentry_enabled`` Whether or not Sentry logging is enabled. ``False`` + +``sentry_logging_level`` The minimal logging level for which to send reports to Sentry. ``warn`` + Possible values: *notset*, *debug*, *info*, *warn*, *error*, + *critical*. It is recommended to have this set to at least *warn*, + to avoid spamming yourself with Sentry events. + +``sentry_exclude_loggers`` A string of comma-separated logger names which should be excluded ``werkzeug`` + from Sentry. + +``sentry_ignored_exceptions`` A string of comma-separated exceptions which should be ignored. ``odoo.exceptions.AccessDenied, + You can use a star symbol (*) at the end, to ignore all exceptions odoo.exceptions.AccessError, + from a module, eg.: *odoo.exceptions.**. odoo.exceptions.DeferredException, + odoo.exceptions.MissingError, + odoo.exceptions.RedirectWarning, + odoo.exceptions.UserError, + odoo.exceptions.ValidationError, + odoo.exceptions.Warning, + odoo.exceptions.except_orm`` + +``sentry_include_context`` If enabled, additional context data will be extracted from current ``True`` + HTTP request and user session (if available). This has no effect + for Cron jobs, as no request/session is available inside a Cron job. + +``sentry_release`` Explicitly define a version to be sent as the release version to + Sentry. Useful in conjuntion with Sentry's "Resolve in the next + release"-functionality. Also useful if your production deployment + does not include any Git context from which a commit might be read. + Overrides *sentry_odoo_dir*. + +``sentry_odoo_dir`` Absolute path to your Odoo installation directory. This is optional + and will only be used to extract the Odoo Git commit, which will be + sent to Sentry, to allow to distinguish between Odoo updates. + Overridden by *sentry_release* +============================= ==================================================================== ========================================================== + +Other `client arguments +`_ can be +configured by prepending the argument name with *sentry_* in your Odoo config +file. Currently supported additional client arguments are: ``with_locals, +max_breadcrumbs, release, environment, server_name, shutdown_timeout, +in_app_include, in_app_exclude, default_integrations, dist, sample_rate, +send_default_pii, http_proxy, https_proxy, request_bodies, debug, +attach_stacktrace, ca_certs, propagate_traces, traces_sample_rate, +auto_enabling_integrations``. + +Example Odoo configuration +-------------------------- + +Below is an example of Odoo configuration file with *Odoo Sentry* options:: + + [options] + sentry_dsn = https://:@sentry.example.com/ + sentry_enabled = true + sentry_logging_level = warn + sentry_exclude_loggers = werkzeug + sentry_ignore_exceptions = odoo.exceptions.AccessDenied, + odoo.exceptions.AccessError,odoo.exceptions.MissingError, + odoo.exceptions.RedirectWarning,odoo.exceptions.UserError, + odoo.exceptions.ValidationError,odoo.exceptions.Warning, + odoo.exceptions.except_orm + sentry_include_context = true + sentry_environment = production + sentry_release = 1.3.2 + sentry_odoo_dir = /home/odoo/odoo/ diff --git a/sentry/readme/INSTALL.rst b/sentry/readme/INSTALL.rst new file mode 100644 index 000000000..6ccb9e341 --- /dev/null +++ b/sentry/readme/INSTALL.rst @@ -0,0 +1,10 @@ +The module can be installed just like any other Odoo module, by adding the +module's directory to Odoo *addons_path*. In order for the module to correctly +wrap the Odoo WSGI application, it also needs to be loaded as a server-wide +module. This can be done with the ``server_wide_modules`` parameter in your +Odoo config file or with the ``--load`` command-line parameter. + +This module additionally requires the sentry-sdk Python package to be available on +the system. It can be installed using pip:: + + pip install sentry-sdk diff --git a/sentry/readme/ROADMAP.rst b/sentry/readme/ROADMAP.rst new file mode 100644 index 000000000..92e100a5b --- /dev/null +++ b/sentry/readme/ROADMAP.rst @@ -0,0 +1,11 @@ +* **No database separation** -- This module functions by intercepting all Odoo + logging records in a running Odoo process. This means that once installed in + one database, it will intercept and report errors for all Odoo databases, + which are used on that Odoo server. + +* **Frontend integration** -- In the future, it would be nice to add + Odoo client-side error reporting to this module as well, by integrating + `raven-js `_. Additionally, `Sentry user + feedback form `_ could be + integrated into the Odoo client error dialog window to allow users shortly + describe what they were doing when things went wrong. diff --git a/sentry/readme/USAGE.rst b/sentry/readme/USAGE.rst new file mode 100644 index 000000000..66d98f441 --- /dev/null +++ b/sentry/readme/USAGE.rst @@ -0,0 +1,6 @@ +Once configured and installed, the module will report any logging event at and +above the configured Sentry logging level, no additional actions are necessary. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/149/10.0 diff --git a/sentry/static/description/index.html b/sentry/static/description/index.html new file mode 100644 index 000000000..88ff15e0b --- /dev/null +++ b/sentry/static/description/index.html @@ -0,0 +1,579 @@ + + + + + + +Sentry + + + +
+

Sentry

+ + +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

This module allows painless Sentry integration with +Odoo.

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDescriptionDefault
sentry_dsnSentry Data Source Name. You can find this value in your Sentry +project configuration. Typically it looks something like this: +https://<public_key>:<secret_key>@sentry.example.com/<project id> +This is the only required option in order to use the module.''
sentry_enabledWhether or not Sentry logging is enabled.False
sentry_logging_levelThe minimal logging level for which to send reports to Sentry. +Possible values: notset, debug, info, warn, error, +critical. It is recommended to have this set to at least warn, +to avoid spamming yourself with Sentry events.warn
sentry_exclude_loggersA string of comma-separated logger names which should be excluded +from Sentry.werkzeug
sentry_ignored_exceptionsA string of comma-separated exceptions which should be ignored. +You can use a star symbol (*) at the end, to ignore all exceptions +from a module, eg.: odoo.exceptions.*.odoo.exceptions.AccessDenied, +odoo.exceptions.AccessError, +odoo.exceptions.DeferredException, +odoo.exceptions.MissingError, +odoo.exceptions.RedirectWarning, +odoo.exceptions.UserError, +odoo.exceptions.ValidationError, +odoo.exceptions.Warning, +odoo.exceptions.except_orm
sentry_include_contextIf enabled, additional context data will be extracted from current +HTTP request and user session (if available). This has no effect +for Cron jobs, as no request/session is available inside a Cron job.True
sentry_releaseExplicitly define a version to be sent as the release version to +Sentry. Useful in conjuntion with Sentry’s “Resolve in the next +release”-functionality. Also useful if your production deployment +does not include any Git context from which a commit might be read. +Overrides sentry_odoo_dir. 
sentry_odoo_dirAbsolute path to your Odoo installation directory. This is optional +and will only be used to extract the Odoo Git commit, which will be +sent to Sentry, to allow to distinguish between Odoo updates. +Overridden by sentry_release 
+

Other client arguments can be +configured by prepending the argument name with sentry_ in your Odoo config +file. Currently supported additional client arguments are: with_locals, +max_breadcrumbs, release, environment, server_name, shutdown_timeout, +in_app_include, in_app_exclude, default_integrations, dist, sample_rate, +send_default_pii, http_proxy, https_proxy, request_bodies, debug, +attach_stacktrace, ca_certs, propagate_traces, traces_sample_rate, +auto_enabling_integrations.

+
+

Example Odoo configuration

+

Below is an example of Odoo configuration file with Odoo Sentry options:

+
+[options]
+sentry_dsn = https://<public_key>:<secret_key>@sentry.example.com/<project id>
+sentry_enabled = true
+sentry_logging_level = warn
+sentry_exclude_loggers = werkzeug
+sentry_ignore_exceptions = odoo.exceptions.AccessDenied,
+    odoo.exceptions.AccessError,odoo.exceptions.MissingError,
+    odoo.exceptions.RedirectWarning,odoo.exceptions.UserError,
+    odoo.exceptions.ValidationError,odoo.exceptions.Warning,
+    odoo.exceptions.except_orm
+sentry_include_context = true
+sentry_environment = production
+sentry_release = 1.3.2
+sentry_odoo_dir = /home/odoo/odoo/
+
+

Table of contents

+ +
+

Installation

+

The module can be installed just like any other Odoo module, by adding the +module’s directory to Odoo addons_path. In order for the module to correctly +wrap the Odoo WSGI application, it also needs to be loaded as a server-wide +module. This can be done with the server_wide_modules parameter in your +Odoo config file or with the --load command-line parameter.

+

This module additionally requires the sentry-sdk Python package to be available on +the system. It can be installed using pip:

+
+pip install sentry-sdk
+
+
+
+

Configuration

+

The following additional configuration options can be added to your Odoo +configuration file:

+
+
+

Usage

+

Once configured and installed, the module will report any logging event at and +above the configured Sentry logging level, no additional actions are necessary.

+Try me on Runbot +
+
+

Known issues / Roadmap

+
    +
  • No database separation – This module functions by intercepting all Odoo +logging records in a running Odoo process. This means that once installed in +one database, it will intercept and report errors for all Odoo databases, +which are used on that Odoo server.
  • +
  • Frontend integration – In the future, it would be nice to add +Odoo client-side error reporting to this module as well, by integrating +raven-js. Additionally, Sentry user +feedback form could be +integrated into the Odoo client error dialog window to allow users shortly +describe what they were doing when things went wrong.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Mohammed Barsi
  • +
  • Versada
  • +
  • Nicolas JEUDY
  • +
  • Vauxoo
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+
    +
  • Vauxoo
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

barsi naglis versada moylop260 fernandahf

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/sentry/tests/test_client.py b/sentry/tests/test_client.py index aafe9d0e7..71c71b752 100644 --- a/sentry/tests/test_client.py +++ b/sentry/tests/test_client.py @@ -3,122 +3,120 @@ import logging import sys -import unittest - -import raven - +from odoo.tests import TransactionCase +from odoo.tools import config from odoo import exceptions -from .. import initialize_raven -from ..logutils import OdooSentryHandler - +from ..hooks import initialize_sentry +from sentry_sdk.transport import HttpTransport +from sentry_sdk.integrations.logging import _IGNORED_LOGGERS -def log_handler_by_class(logger, handler_cls): - for handler in logger.handlers: - if isinstance(handler, handler_cls): - yield handler +def remove_handler_ignore(handler_name): + """Removes handlers of handlers ignored list. + """ + _IGNORED_LOGGERS.discard(handler_name) -def remove_logging_handler(logger_name, handler_cls): - '''Removes handlers of specified classes from a :class:`logging.Logger` - with a given name. - :param string logger_name: name of the logger +class TestException(exceptions.UserError): + pass - :param handler_cls: class of the handler to remove. You can pass a tuple of - classes to catch several classes - ''' - logger = logging.getLogger(logger_name) - for handler in log_handler_by_class(logger, handler_cls): - logger.removeHandler(handler) - -class InMemoryClient(raven.Client): - '''A :class:`raven.Client` subclass which simply stores events in a list. +class InMemoryTransport(HttpTransport): + """A :class:`sentry_sdk.Hub.transport` subclass which simply stores events in a list. Extended based on the one found in raven-python to avoid additional testing dependencies: https://git.io/vyGO3 - ''' + """ - def __init__(self, **kwargs): + def __init__(self, *args, **kwargs): self.events = [] - super(InMemoryClient, self).__init__(**kwargs) - - def is_enabled(self): - return True - def send(self, **kwargs): - self.events.append(kwargs) + def capture_event(self, event, *args, **kwargs): + self.events.append(event) def has_event(self, event_level, event_msg): for event in self.events: if (event.get('level') == event_level and - event.get('message') == event_msg): + event.get('logentry', {}).get('message') == event_msg): return True return False + def flush(self, *args, **kwargs): + pass -class TestClientSetup(unittest.TestCase): + def kill(self, *args, **kwargs): + pass + + +class TestClientSetup(TransactionCase): def setUp(self): super(TestClientSetup, self).setUp() - self.logger = logging.getLogger(__name__) - - # Sentry is enabled by default, so the default handler will be added - # when the module is loaded. After that, subsequent calls to - # setup_logging will not re-add our handler. We explicitly remove - # OdooSentryHandler handler so we can test with our in-memory client. - remove_logging_handler('', OdooSentryHandler) + self.dsn = 'http://public:secret@example.com/1' + config.options['sentry_enabled'] = True + config.options['sentry_dsn'] = self.dsn + self.client = initialize_sentry(config)._client + self.client.transport = InMemoryTransport({'dsn': self.dsn}) + self.handler = self.client.integrations['logging']._handler + + def log(self, level, msg, exc_info=None): + record = logging.LogRecord( + __name__, level, __file__, 42, msg, (), exc_info) + self.handler.emit(record) def assertEventCaptured(self, client, event_level, event_msg): self.assertTrue( - client.has_event(event_level, event_msg), + client.transport.has_event(event_level, event_msg), msg='Event: "%s" was not captured' % event_msg ) def assertEventNotCaptured(self, client, event_level, event_msg): self.assertFalse( - client.has_event(event_level, event_msg), + client.transport.has_event(event_level, event_msg), msg='Event: "%s" was captured' % event_msg ) def test_initialize_raven_sets_dsn(self): - config = { - 'sentry_enabled': True, - 'sentry_dsn': 'http://public:secret@example.com/1', - } - client = initialize_raven(config, client_cls=InMemoryClient) - self.assertEqual(client.remote.base_url, 'http://example.com') + self.assertEqual(self.client.dsn, self.dsn) def test_capture_event(self): - config = { - 'sentry_enabled': True, - 'sentry_dsn': 'http://public:secret@example.com/1', - } level, msg = logging.WARNING, 'Test event, can be ignored' - client = initialize_raven(config, client_cls=InMemoryClient) - self.logger.log(level, msg) - self.assertEventCaptured(client, level, msg) + self.log(level, msg) + level = "warning" + self.assertEventCaptured(self.client, level, msg) + + def test_capture_event_exc(self): + level, msg = logging.WARNING, 'Test event, can be ignored exception' + try: + raise TestException(msg) + except TestException: + exc_info = sys.exc_info() + self.log(level, msg, exc_info) + level = "warning" + self.assertEventCaptured(self.client, level, msg) def test_ignore_exceptions(self): - config = { - 'sentry_enabled': True, - 'sentry_dsn': 'http://public:secret@example.com/1', - 'sentry_ignore_exceptions': 'odoo.exceptions.UserError', - } - level, msg = logging.WARNING, 'Test UserError' - client = initialize_raven(config, client_cls=InMemoryClient) - - handlers = list( - log_handler_by_class(logging.getLogger(), OdooSentryHandler) - ) - self.assertTrue(handlers) - handler = handlers[0] + config.options['sentry_ignore_exceptions'] = "odoo.exceptions.UserError" + client = initialize_sentry(config)._client + client.transport = InMemoryTransport({'dsn': self.dsn}) + level, msg = logging.WARNING, 'Test exception' try: raise exceptions.UserError(msg) except exceptions.UserError: exc_info = sys.exc_info() - record = logging.LogRecord( - __name__, level, __file__, 42, msg, (), exc_info) - handler.emit(record) + self.log(level, msg, exc_info) + level = "warning" + self.assertEventNotCaptured(client, level, msg) + + def test_exclude_logger(self): + config.options['sentry_enabled'] = True + config.options['sentry_exclude_loggers'] = __name__ + client = initialize_sentry(config)._client + client.transport = InMemoryTransport({'dsn': self.dsn}) + level, msg = logging.WARNING, 'Test exclude logger %s' % __name__ + self.log(level, msg) + level = "warning" + # Revert ignored logger so it doesn't affect other tests + remove_handler_ignore(__name__) self.assertEventNotCaptured(client, level, msg) diff --git a/sentry/tests/test_logutils.py b/sentry/tests/test_logutils.py index bcabdcb6a..91fab2108 100644 --- a/sentry/tests/test_logutils.py +++ b/sentry/tests/test_logutils.py @@ -1,14 +1,12 @@ # Copyright 2016-2017 Versada # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import unittest - -import mock +from odoo.tests import TransactionCase from ..logutils import SanitizeOdooCookiesProcessor -class TestOdooCookieSanitizer(unittest.TestCase): +class TestOdooCookieSanitizer(TransactionCase): def test_cookie_as_string(self): data = { @@ -20,7 +18,7 @@ class TestOdooCookieSanitizer(unittest.TestCase): }, } - proc = SanitizeOdooCookiesProcessor(mock.Mock()) + proc = SanitizeOdooCookiesProcessor() result = proc.process(data) self.assertTrue('request' in result) @@ -42,14 +40,14 @@ class TestOdooCookieSanitizer(unittest.TestCase): }, } - proc = SanitizeOdooCookiesProcessor(mock.Mock()) + proc = SanitizeOdooCookiesProcessor() result = proc.process(data) self.assertTrue('request' in result) http = result['request'] self.assertEqual( http['cookies'], - 'website_lang=en_us;session_id;foo=bar'.format(m=proc.MASK), + 'website_lang=en_us;session_id;foo=bar', ) def test_cookie_header(self): @@ -64,7 +62,7 @@ class TestOdooCookieSanitizer(unittest.TestCase): }, } - proc = SanitizeOdooCookiesProcessor(mock.Mock()) + proc = SanitizeOdooCookiesProcessor() result = proc.process(data) self.assertTrue('request' in result)