From 061fe4ab7fc2a659d571f33ef8439d5c25f5e9d7 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Thu, 12 Oct 2017 14:57:40 +0300 Subject: [PATCH 1/3] [ADD] sentry: Adds module --- requirements.txt | 1 + sentry/README.rst | 168 +++++++++++++++++++++++++++++ sentry/__init__.py | 78 ++++++++++++++ sentry/__manifest__.py | 24 +++++ sentry/const.py | 89 +++++++++++++++ sentry/logutils.py | 106 ++++++++++++++++++ sentry/static/description/icon.png | Bin 0 -> 2220 bytes sentry/tests/__init__.py | 8 ++ sentry/tests/test_client.py | 125 +++++++++++++++++++++ sentry/tests/test_logutils.py | 78 ++++++++++++++ 10 files changed, 677 insertions(+) create mode 100644 sentry/README.rst create mode 100644 sentry/__init__.py create mode 100644 sentry/__manifest__.py create mode 100644 sentry/const.py create mode 100644 sentry/logutils.py create mode 100644 sentry/static/description/icon.png create mode 100644 sentry/tests/__init__.py create mode 100644 sentry/tests/test_client.py create mode 100644 sentry/tests/test_logutils.py diff --git a/requirements.txt b/requirements.txt index 08b8b0254..fdbf76491 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ IPy python-json-logger odoorpc + raven diff --git a/sentry/README.rst b/sentry/README.rst new file mode 100644 index 000000000..f7f61e14d --- /dev/null +++ b/sentry/README.rst @@ -0,0 +1,168 @@ +.. 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 + +Configuration +============= + +The following additional configuration options can be added to your Odoo +configuration file: + +============================= ==================================================================== ========================================================== + 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_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. + +``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. +============================= ==================================================================== ========================================================== + +Other `client arguments +`_ 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``. + +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_processors = raven.processors.SanitizePasswordsProcessor,odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor + sentry_transport = threaded + sentry_include_context = true + sentry_environment = production + sentry_auto_log_stacks = false + sentry_odoo_dir = /home/odoo/odoo/ + +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. + +.. 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 + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* `Module Icon `_ + +Contributors +------------ + +* Mohammed Barsi +* Andrius Preimantas +* Naglis Jonaitis + +Maintainer +---------- + +.. 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 diff --git a/sentry/__init__.py b/sentry/__init__.py new file mode 100644 index 000000000..fe841dc4a --- /dev/null +++ b/sentry/__init__.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# 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 + +_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( + u'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 + options = { + '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 callable(option.converter): + 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) diff --git a/sentry/__manifest__.py b/sentry/__manifest__.py new file mode 100644 index 000000000..1ef7acfdb --- /dev/null +++ b/sentry/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + 'name': 'Sentry', + 'summary': 'Report Odoo errors to Sentry', + 'version': '10.0.1.0.0', + 'category': 'Extra Tools', + 'website': 'https://odoo-community.org/', + 'author': 'Mohammed Barsi,' + 'Versada,' + 'Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'application': False, + 'installable': True, + 'external_dependencies': { + 'python': [ + 'raven', + ] + }, + 'depends': [ + 'base', + ], +} diff --git a/sentry/const.py b/sentry/const.py new file mode 100644 index 000000000..5ceb3e2f2 --- /dev/null +++ b/sentry/const.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import collections +import logging + +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.') + + +def split_multiple(string, delimiter=',', strip_chars=None): + '''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)] + + +SentryOption = collections.namedtuple( + 'SentryOption', ['key', 'default', 'converter']) + +# Mapping of Odoo logging level -> Python stdlib logging library log level. +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 = [ + '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', +] +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_TRANSPORT = 'threaded' + + +def select_transport(name=DEFAULT_TRANSPORT): + return { + 'requests_synchronous': raven.transport.RequestsHTTPTransport, + 'requests_threaded': raven.transport.ThreadedRequestsHTTPTransport, + 'synchronous': raven.transport.HTTPTransport, + 'threaded': raven.transport.ThreadedHTTPTransport, + }.get(name, DEFAULT_TRANSPORT) + + +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( + 'ignore_exceptions', DEFAULT_IGNORED_EXCEPTIONS, split_multiple), + SentryOption('processors', DEFAULT_PROCESSORS, split_multiple), + SentryOption('environment', None, None), + ] diff --git a/sentry/logutils.py b/sentry/logutils.py new file mode 100644 index 000000000..178a518db --- /dev/null +++ b/sentry/logutils.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import urlparse + +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 + + +def get_request_info(request): + ''' + Returns context data extracted from :param:`request`. + + Heavily based on flask integration for Sentry: https://git.io/vP4i9. + ''' + urlparts = urlparse.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)), + '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 + try: + session = getattr(request, 'session', {}) + except RuntimeError: + ctx = {} + else: + ctx = { + 'tags': { + 'database': session.get('db', None), + }, + 'user': { + 'login': session.get('login', None), + 'uid': session.get('uid', None), + }, + 'extra': { + 'context': session.get('context', {}), + }, + } + if request.httprequest: + ctx.update({ + 'request': get_request_info(request.httprequest), + }) + 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`. + + Allows to add additional Odoo and HTTP request data to the event which is + sent to Sentry. + ''' + + 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 SanitizeOdooCookiesProcessor(SanitizePasswordsProcessor): + ''' + Custom :class:`raven.processors.Processor`. + + Allows to sanitize sensitive Odoo cookies, namely the "session_id" cookie. + ''' + + FIELDS = frozenset([ + 'session_id', + ]) diff --git a/sentry/static/description/icon.png b/sentry/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..134c89f93b226aba995199d3e250758fdcd205c0 GIT binary patch literal 2220 zcmeH{`#aMM9LK-bu!gzLofdYo$685|l4E7rj$1B^BH|3EwoxvLvbl5^qBS&UhUv(| zCQ8UE#iFcGh>ScW_s&)*a@le8<9W`XaQZyY`}2NYpWi<3AKsbAJY3YEhEMGTg)U>@#M7ZoKK@@t|AFZ3+{x5rQ!I|hY zVckaes_@ri$-F1=xaLY1I8|#qCvBqoH!YSi>^xWBg=`A3W~)v{GX^Rk=(8A!Z;J6x zADOLiKIRO?&P%JRUpwa-KKjc>k}Z~LaqucciT!4LiSBugxdk;@U&wVWU%CM+iN`BW zGnSY?&)uBV!jTCn5K9K{qlP!8r@E|zIg^^$yg^UJj0kc9NPUU?hR|SBz#13+5plf-Ne66*8cSTpS02keGHOL#A$iIc^(#K!`Ph=rfbvDYHEIKy@W&O zh&ZD*1P>o>G_`0I{3-vXj$#yzUZk)GB=v8q2$bhoAQM*()%HyfmAEw1?6zYc&ov-JYU#aa`RjwZ+b8j$&#SLs-HN z3V=Q+AI0vGkdoc2*Mn>ihEpk8Bio5m$-aAsp!g?qEfqb=sZk8HW%T*61Ph&2uE&Vpn?lc#;$_0>Jf#ffwS$uS5iyq?+9K zS8-JTe8Q}J*6AfGd#v~IlYn~HE8AFc_Y^)PYQ5S4cVWA6xThC0SRwtl_cCP*wpqeU%~KbbcXoJsDuvGnmRoX_&u1>gKdw^uu^O`0970ktWXqXG z3yK4wSK;P13o zh!;29Ok~7)R$dXRk>$1OEx5(chQ?0e*~$boHb`e$WA@--O#}>6l{8;>eWfCB;i(FK zT(Pa|kpZ@3Tp5+-gmG5Vr0zVhvlWD=p}Od^Omv2GFS7cU zbb)$((0J6=quyx7;R$Nyf5kS*7g mG#phHHkRJ`Z%?&n)r>~us~29!g9w{73J`D}&NYWmr~U(OaGt;b literal 0 HcmV?d00001 diff --git a/sentry/tests/__init__.py b/sentry/tests/__init__.py new file mode 100644 index 000000000..50cb79814 --- /dev/null +++ b/sentry/tests/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ( + test_client, + test_logutils, +) diff --git a/sentry/tests/test_client.py b/sentry/tests/test_client.py new file mode 100644 index 000000000..69bc1819f --- /dev/null +++ b/sentry/tests/test_client.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import sys +import unittest + +import raven + +from odoo import exceptions + +from .. import initialize_raven +from ..logutils import OdooSentryHandler + + +def log_handler_by_class(logger, handler_cls): + for handler in logger.handlers: + if isinstance(handler, handler_cls): + yield handler + + +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 + + :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. + + Extended based on the one found in raven-python to avoid additional testing + dependencies: https://git.io/vyGO3 + ''' + + def __init__(self, **kwargs): + self.events = [] + super(InMemoryClient, self).__init__(**kwargs) + + def is_enabled(self): + return True + + def send(self, **kwargs): + self.events.append(kwargs) + + 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): + return True + return False + + +class TestClientSetup(unittest.TestCase): + + 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) + + def assertEventCaptured(self, client, event_level, event_msg): + self.assertTrue( + client.has_event(event_level, event_msg), + msg=u'Event: "%s" was not captured' % event_msg + ) + + def assertEventNotCaptured(self, client, event_level, event_msg): + self.assertFalse( + client.has_event(event_level, event_msg), + msg=u'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') + + 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) + + 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] + 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.assertEventNotCaptured(client, level, msg) diff --git a/sentry/tests/test_logutils.py b/sentry/tests/test_logutils.py new file mode 100644 index 000000000..b81b69162 --- /dev/null +++ b/sentry/tests/test_logutils.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import unittest + +import mock + +from ..logutils import SanitizeOdooCookiesProcessor + + +class TestOdooCookieSanitizer(unittest.TestCase): + + def test_cookie_as_string(self): + data = { + 'request': { + 'cookies': 'website_lang=en_us;' + 'session_id=hello;' + 'Session_ID=hello;' + 'foo=bar', + }, + } + + proc = SanitizeOdooCookiesProcessor(mock.Mock()) + result = proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + self.assertEqual( + http['cookies'], + 'website_lang=en_us;' + 'session_id={m};' + 'Session_ID={m};' + 'foo=bar'.format( + m=proc.MASK, + ), + ) + + def test_cookie_as_string_with_partials(self): + data = { + 'request': { + 'cookies': 'website_lang=en_us;session_id;foo=bar', + }, + } + + proc = SanitizeOdooCookiesProcessor(mock.Mock()) + 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), + ) + + def test_cookie_header(self): + data = { + 'request': { + 'headers': { + 'Cookie': 'foo=bar;' + 'session_id=hello;' + 'Session_ID=hello;' + 'a_session_id_here=hello', + }, + }, + } + + proc = SanitizeOdooCookiesProcessor(mock.Mock()) + result = proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + self.assertEqual( + http['headers']['Cookie'], + 'foo=bar;' + 'session_id={m};' + 'Session_ID={m};' + 'a_session_id_here={m}'.format(m=proc.MASK)) From af88d888028921eeaf5712dd49071e1c948e9d84 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Thu, 12 Oct 2017 15:05:19 +0300 Subject: [PATCH 2/3] [MIG] sentry: Backports to Odoo 8.0 --- sentry/README.rst | 25 +++++++++++----------- sentry/__init__.py | 4 ++-- sentry/{__manifest__.py => __openerp__.py} | 2 +- sentry/const.py | 23 ++++++++++---------- sentry/logutils.py | 4 ++-- sentry/tests/test_client.py | 22 +++++++++---------- sentry/tests/test_logutils.py | 4 ++-- 7 files changed, 41 insertions(+), 43 deletions(-) rename sentry/{__manifest__.py => __openerp__.py} (95%) diff --git a/sentry/README.rst b/sentry/README.rst index f7f61e14d..25324a2bc 100644 --- a/sentry/README.rst +++ b/sentry/README.rst @@ -47,18 +47,17 @@ configuration file: ``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_ignored_exceptions`` A string of comma-separated exceptions which should be ignored. ``openerp.exceptions.AccessDenied, + You can use a star symbol (*) at the end, to ignore all exceptions openerp.exceptions.AccessError, + from a module, eg.: *openerp.exceptions.**. openerp.exceptions.DeferredException, + openerp.exceptions.MissingError, + openerp.exceptions.RedirectWarning, + openerp.exceptions.ValidationError, + openerp.exceptions.Warning, + openerp.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`` + on an event before sending it to Sentry. openerp.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 @@ -93,8 +92,8 @@ 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_ignore_exceptions = openerp.exceptions.AccessDenied,openerp.exceptions.AccessError,openerp.exceptions.MissingError,openerp.exceptions.RedirectWarning,openerp.exceptions.ValidationError,openerp.exceptions.Warning,openerp.exceptions.except_orm + sentry_processors = raven.processors.SanitizePasswordsProcessor,openerp.addons.sentry.logutils.SanitizeOdooCookiesProcessor sentry_transport = threaded sentry_include_context = true sentry_environment = production @@ -109,7 +108,7 @@ 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 + :target: https://runbot.odoo-community.org/runbot/149/8.0 Known issues / Roadmap ====================== diff --git a/sentry/__init__.py b/sentry/__init__.py index fe841dc4a..aa7f59408 100644 --- a/sentry/__init__.py +++ b/sentry/__init__.py @@ -4,8 +4,8 @@ import logging -from odoo.service import wsgi_server -from odoo.tools import config as odoo_config +from openerp.service import wsgi_server +from openerp.tools import config as odoo_config from . import const from .logutils import LoggerNameFilter, OdooSentryHandler diff --git a/sentry/__manifest__.py b/sentry/__openerp__.py similarity index 95% rename from sentry/__manifest__.py rename to sentry/__openerp__.py index 1ef7acfdb..78015b267 100644 --- a/sentry/__manifest__.py +++ b/sentry/__openerp__.py @@ -4,7 +4,7 @@ { 'name': 'Sentry', 'summary': 'Report Odoo errors to Sentry', - 'version': '10.0.1.0.0', + 'version': '8.0.1.0.0', 'category': 'Extra Tools', 'website': 'https://odoo-community.org/', 'author': 'Mohammed Barsi,' diff --git a/sentry/const.py b/sentry/const.py index 5ceb3e2f2..270a07dbe 100644 --- a/sentry/const.py +++ b/sentry/const.py @@ -5,7 +5,7 @@ import collections import logging -import odoo.loglevels +import openerp.loglevels _logger = logging.getLogger(__name__) try: @@ -27,27 +27,26 @@ SentryOption = collections.namedtuple( # Mapping of Odoo logging level -> Python stdlib logging library log level. LOG_LEVEL_MAP = dict([ - (getattr(odoo.loglevels, 'LOG_%s' % x), getattr(logging, x)) + (getattr(openerp.loglevels, 'LOG_%s' % x), getattr(logging, x)) for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET') ]) DEFAULT_LOG_LEVEL = 'warn' ODOO_USER_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', + 'openerp.exceptions.AccessDenied', + 'openerp.exceptions.AccessError', + 'openerp.exceptions.DeferredException', + 'openerp.exceptions.MissingError', + 'openerp.exceptions.RedirectWarning', + 'openerp.exceptions.ValidationError', + 'openerp.exceptions.Warning', + 'openerp.exceptions.except_orm', ] DEFAULT_IGNORED_EXCEPTIONS = ','.join(ODOO_USER_EXCEPTIONS) PROCESSORS = ( 'raven.processors.SanitizePasswordsProcessor', - 'odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor', + 'openerp.addons.sentry.logutils.SanitizeOdooCookiesProcessor', ) DEFAULT_PROCESSORS = ','.join(PROCESSORS) diff --git a/sentry/logutils.py b/sentry/logutils.py index 178a518db..3a3155e0f 100644 --- a/sentry/logutils.py +++ b/sentry/logutils.py @@ -5,7 +5,7 @@ import logging import urlparse -import odoo.http +import openerp.http _logger = logging.getLogger(__name__) try: @@ -38,7 +38,7 @@ def get_extra_context(): ''' Extracts additional context from the current request (if such is set). ''' - request = odoo.http.request + request = openerp.http.request try: session = getattr(request, 'session', {}) except RuntimeError: diff --git a/sentry/tests/test_client.py b/sentry/tests/test_client.py index 69bc1819f..63bd3f8f1 100644 --- a/sentry/tests/test_client.py +++ b/sentry/tests/test_client.py @@ -4,11 +4,11 @@ import logging import sys -import unittest +import unittest2 import raven -from odoo import exceptions +from openerp import exceptions from .. import initialize_raven from ..logutils import OdooSentryHandler @@ -59,16 +59,16 @@ class InMemoryClient(raven.Client): return False -class TestClientSetup(unittest.TestCase): +class TestClientSetup(unittest2.TestCase): 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. + def tearDown(self): + super(TestClientSetup, self).tearDown() + # Remove our logging handler to avoid interfering with tests of other + # modules. remove_logging_handler('', OdooSentryHandler) def assertEventCaptured(self, client, event_level, event_msg): @@ -105,9 +105,9 @@ class TestClientSetup(unittest.TestCase): config = { 'sentry_enabled': True, 'sentry_dsn': 'http://public:secret@example.com/1', - 'sentry_ignore_exceptions': 'odoo.exceptions.UserError', + 'sentry_ignore_exceptions': 'openerp.exceptions.ValidationError', } - level, msg = logging.WARNING, 'Test UserError' + level, msg = logging.WARNING, 'Test ValidationError' client = initialize_raven(config, client_cls=InMemoryClient) handlers = list( @@ -116,8 +116,8 @@ class TestClientSetup(unittest.TestCase): self.assertTrue(handlers) handler = handlers[0] try: - raise exceptions.UserError(msg) - except exceptions.UserError: + raise exceptions.ValidationError(msg) + except exceptions.ValidationError: exc_info = sys.exc_info() record = logging.LogRecord( __name__, level, __file__, 42, msg, (), exc_info) diff --git a/sentry/tests/test_logutils.py b/sentry/tests/test_logutils.py index b81b69162..84f6885ba 100644 --- a/sentry/tests/test_logutils.py +++ b/sentry/tests/test_logutils.py @@ -2,14 +2,14 @@ # Copyright 2016-2017 Versada # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import unittest +import unittest2 import mock from ..logutils import SanitizeOdooCookiesProcessor -class TestOdooCookieSanitizer(unittest.TestCase): +class TestOdooCookieSanitizer(unittest2.TestCase): def test_cookie_as_string(self): data = { From f6bf474c86c89376ade59ae26b6e4f72983bad06 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Fri, 5 Jan 2018 11:42:31 +0200 Subject: [PATCH 3/3] [FIX] sentry: Fix SanitizeOdooCookiesProcessor after raven 6.4.0 --- sentry/logutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry/logutils.py b/sentry/logutils.py index 3a3155e0f..e268670e0 100644 --- a/sentry/logutils.py +++ b/sentry/logutils.py @@ -101,6 +101,9 @@ class SanitizeOdooCookiesProcessor(SanitizePasswordsProcessor): Allows to sanitize sensitive Odoo cookies, namely the "session_id" cookie. ''' - FIELDS = frozenset([ + # `FIELDS` was renamed to `KEYS` in raven 6.4.0. + # Keep `FIELDS` for backwards compatibility. + # See also issue #1096 on OCA/server-tools. + KEYS = FIELDS = frozenset([ 'session_id', ])