[IMP] sentry: black, isort

pull/2516/head
Ivan 2020-02-28 02:07:12 +05:00 committed by prabakaran
parent 165715b747
commit 532bdbdbfe
7 changed files with 157 additions and 187 deletions

View File

@ -22,41 +22,44 @@ except ImportError:
def get_odoo_commit(odoo_dir): def get_odoo_commit(odoo_dir):
'''Attempts to get Odoo git commit from :param:`odoo_dir`.''' """Attempts to get Odoo git commit from :param:`odoo_dir`."""
if not odoo_dir: if not odoo_dir:
return return
try: try:
return raven.fetch_git_sha(odoo_dir) return raven.fetch_git_sha(odoo_dir)
except raven.exceptions.InvalidGitRepository: except raven.exceptions.InvalidGitRepository:
_logger.debug( _logger.debug('Odoo directory: "%s" not a valid git repository', odoo_dir)
'Odoo directory: "%s" not a valid git repository', odoo_dir)
def initialize_raven(config, client_cls=None): def initialize_raven(config, client_cls=None):
''' """
Setup an instance of :class:`raven.Client`. Setup an instance of :class:`raven.Client`.
:param config: Sentry configuration :param config: Sentry configuration
:param client: class used to instantiate the raven client. :param client: class used to instantiate the raven client.
''' """
enabled = config.get('sentry_enabled', False) enabled = config.get("sentry_enabled", False)
if not (HAS_RAVEN and enabled): if not (HAS_RAVEN and enabled):
return return
if config.get('sentry_odoo_dir') and config.get('sentry_release'): if config.get("sentry_odoo_dir") and config.get("sentry_release"):
_logger.debug('Both sentry_odoo_dir and sentry_release defined, choosing sentry_release') _logger.debug(
"Both sentry_odoo_dir and sentry_release defined, choosing sentry_release"
)
options = { options = {
'release': config.get('sentry_release', get_odoo_commit(config.get('sentry_odoo_dir'))), "release": config.get(
"sentry_release", get_odoo_commit(config.get("sentry_odoo_dir"))
)
} }
for option in const.get_sentry_options(): for option in const.get_sentry_options():
value = config.get('sentry_%s' % option.key, option.default) value = config.get("sentry_%s" % option.key, option.default)
if isinstance(option.converter, collections.Callable): if isinstance(option.converter, collections.Callable):
value = option.converter(value) value = option.converter(value)
options[option.key] = value options[option.key] = value
level = config.get('sentry_logging_level', const.DEFAULT_LOG_LEVEL) level = config.get("sentry_logging_level", const.DEFAULT_LOG_LEVEL)
exclude_loggers = const.split_multiple( exclude_loggers = const.split_multiple(
config.get('sentry_exclude_loggers', const.DEFAULT_EXCLUDE_LOGGERS) config.get("sentry_exclude_loggers", const.DEFAULT_EXCLUDE_LOGGERS)
) )
if level not in const.LOG_LEVEL_MAP: if level not in const.LOG_LEVEL_MAP:
level = const.DEFAULT_LOG_LEVEL level = const.DEFAULT_LOG_LEVEL
@ -64,18 +67,18 @@ def initialize_raven(config, client_cls=None):
client_cls = client_cls or raven.Client client_cls = client_cls or raven.Client
client = client_cls(**options) client = client_cls(**options)
handler = OdooSentryHandler( handler = OdooSentryHandler(
config.get('sentry_include_context', True), config.get("sentry_include_context", True),
client=client, client=client,
level=const.LOG_LEVEL_MAP[level], level=const.LOG_LEVEL_MAP[level],
) )
if exclude_loggers: if exclude_loggers:
handler.addFilter(LoggerNameFilter( handler.addFilter(
exclude_loggers, name='sentry.logger.filter')) LoggerNameFilter(exclude_loggers, name="sentry.logger.filter")
)
raven.conf.setup_logging(handler) raven.conf.setup_logging(handler)
wsgi_server.application = Sentry( wsgi_server.application = Sentry(wsgi_server.application, client=client)
wsgi_server.application, client=client)
client.captureMessage('Starting Odoo Server') client.captureMessage("Starting Odoo Server")
return client return client

View File

@ -1,24 +1,18 @@
# Copyright 2016-2017 Versada <https://versada.eu/> # Copyright 2016-2017 Versada <https://versada.eu/>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'Sentry', "name": "Sentry",
'summary': 'Report Odoo errors to Sentry', "summary": "Report Odoo errors to Sentry",
'version': '12.0.1.0.0', "version": "12.0.1.0.0",
'category': 'Extra Tools', "category": "Extra Tools",
'website': 'https://odoo-community.org/', "website": "https://odoo-community.org/",
'author': 'Mohammed Barsi,' "author": "Mohammed Barsi,"
'Versada,' "Versada,"
'Nicolas JEUDY,' "Nicolas JEUDY,"
'Odoo Community Association (OCA)', "Odoo Community Association (OCA)",
'license': 'AGPL-3', "license": "AGPL-3",
'application': False, "application": False,
'installable': True, "installable": True,
'external_dependencies': { "external_dependencies": {"python": ["raven"]},
'python': [ "depends": ["base"],
'raven',
]
},
'depends': [
'base',
],
} }

View File

@ -14,76 +14,72 @@ except ImportError:
_logger.debug('Cannot import "raven". Please make sure it is installed.') _logger.debug('Cannot import "raven". Please make sure it is installed.')
def split_multiple(string, delimiter=',', strip_chars=None): 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: if not string:
return [] return []
return [v.strip(strip_chars) for v in string.split(delimiter)] return [v.strip(strip_chars) for v in string.split(delimiter)]
SentryOption = collections.namedtuple( SentryOption = collections.namedtuple("SentryOption", ["key", "default", "converter"])
'SentryOption', ['key', 'default', 'converter'])
# Mapping of Odoo logging level -> Python stdlib logging library log level. # Mapping of Odoo logging level -> Python stdlib logging library log level.
LOG_LEVEL_MAP = dict([ LOG_LEVEL_MAP = {
(getattr(odoo.loglevels, 'LOG_%s' % x), getattr(logging, x)) getattr(odoo.loglevels, "LOG_%s" % x): getattr(logging, x)
for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET') for x in ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET")
]) }
DEFAULT_LOG_LEVEL = 'warn' DEFAULT_LOG_LEVEL = "warn"
ODOO_USER_EXCEPTIONS = [ ODOO_USER_EXCEPTIONS = [
'odoo.exceptions.AccessDenied', "odoo.exceptions.AccessDenied",
'odoo.exceptions.AccessError', "odoo.exceptions.AccessError",
'odoo.exceptions.DeferredException', "odoo.exceptions.DeferredException",
'odoo.exceptions.MissingError', "odoo.exceptions.MissingError",
'odoo.exceptions.RedirectWarning', "odoo.exceptions.RedirectWarning",
'odoo.exceptions.UserError', "odoo.exceptions.UserError",
'odoo.exceptions.ValidationError', "odoo.exceptions.ValidationError",
'odoo.exceptions.Warning', "odoo.exceptions.Warning",
'odoo.exceptions.except_orm', "odoo.exceptions.except_orm",
] ]
DEFAULT_IGNORED_EXCEPTIONS = ','.join(ODOO_USER_EXCEPTIONS) DEFAULT_IGNORED_EXCEPTIONS = ",".join(ODOO_USER_EXCEPTIONS)
PROCESSORS = ( PROCESSORS = (
'raven.processors.SanitizePasswordsProcessor', "raven.processors.SanitizePasswordsProcessor",
'odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor', "odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor",
) )
DEFAULT_PROCESSORS = ','.join(PROCESSORS) DEFAULT_PROCESSORS = ",".join(PROCESSORS)
EXCLUDE_LOGGERS = ( EXCLUDE_LOGGERS = ("werkzeug",)
'werkzeug', DEFAULT_EXCLUDE_LOGGERS = ",".join(EXCLUDE_LOGGERS)
)
DEFAULT_EXCLUDE_LOGGERS = ','.join(EXCLUDE_LOGGERS)
DEFAULT_TRANSPORT = 'threaded' DEFAULT_TRANSPORT = "threaded"
def select_transport(name=DEFAULT_TRANSPORT): def select_transport(name=DEFAULT_TRANSPORT):
return { return {
'requests_synchronous': raven.transport.RequestsHTTPTransport, "requests_synchronous": raven.transport.RequestsHTTPTransport,
'requests_threaded': raven.transport.ThreadedRequestsHTTPTransport, "requests_threaded": raven.transport.ThreadedRequestsHTTPTransport,
'synchronous': raven.transport.HTTPTransport, "synchronous": raven.transport.HTTPTransport,
'threaded': raven.transport.ThreadedHTTPTransport, "threaded": raven.transport.ThreadedHTTPTransport,
}.get(name, DEFAULT_TRANSPORT) }.get(name, DEFAULT_TRANSPORT)
def get_sentry_options(): def get_sentry_options():
return [ return [
SentryOption('dsn', '', str.strip), SentryOption("dsn", "", str.strip),
SentryOption('install_sys_hook', False, None), SentryOption("install_sys_hook", False, None),
SentryOption('transport', DEFAULT_TRANSPORT, select_transport), SentryOption("transport", DEFAULT_TRANSPORT, select_transport),
SentryOption('include_paths', '', split_multiple), SentryOption("include_paths", "", split_multiple),
SentryOption('exclude_paths', '', split_multiple), SentryOption("exclude_paths", "", split_multiple),
SentryOption('machine', defaults.NAME, None), SentryOption("machine", defaults.NAME, None),
SentryOption('auto_log_stacks', defaults.AUTO_LOG_STACKS, None), SentryOption("auto_log_stacks", defaults.AUTO_LOG_STACKS, None),
SentryOption('capture_locals', defaults.CAPTURE_LOCALS, None), SentryOption("capture_locals", defaults.CAPTURE_LOCALS, None),
SentryOption('string_max_length', defaults.MAX_LENGTH_STRING, None), SentryOption("string_max_length", defaults.MAX_LENGTH_STRING, None),
SentryOption('list_max_length', defaults.MAX_LENGTH_LIST, None), SentryOption("list_max_length", defaults.MAX_LENGTH_LIST, None),
SentryOption('site', None, None), SentryOption("site", None, None),
SentryOption('include_versions', True, None), SentryOption("include_versions", True, None),
SentryOption( SentryOption("ignore_exceptions", DEFAULT_IGNORED_EXCEPTIONS, split_multiple),
'ignore_exceptions', DEFAULT_IGNORED_EXCEPTIONS, split_multiple), SentryOption("processors", DEFAULT_PROCESSORS, split_multiple),
SentryOption('processors', DEFAULT_PROCESSORS, split_multiple), SentryOption("environment", None, None),
SentryOption('environment', None, None), SentryOption("release", None, None),
SentryOption('release', None, None),
] ]

View File

@ -18,56 +18,50 @@ except ImportError:
def get_request_info(request): def get_request_info(request):
''' """
Returns context data extracted from :param:`request`. Returns context data extracted from :param:`request`.
Heavily based on flask integration for Sentry: https://git.io/vP4i9. Heavily based on flask integration for Sentry: https://git.io/vP4i9.
''' """
urlparts = urllib.parse.urlsplit(request.url) urlparts = urllib.parse.urlsplit(request.url)
return { return {
'url': '%s://%s%s' % (urlparts.scheme, urlparts.netloc, urlparts.path), "url": "{}://{}{}".format(urlparts.scheme, urlparts.netloc, urlparts.path),
'query_string': urlparts.query, "query_string": urlparts.query,
'method': request.method, "method": request.method,
'headers': dict(get_headers(request.environ)), "headers": dict(get_headers(request.environ)),
'env': dict(get_environ(request.environ)), "env": dict(get_environ(request.environ)),
} }
def get_extra_context(): def get_extra_context():
''' """
Extracts additional context from the current request (if such is set). Extracts additional context from the current request (if such is set).
''' """
request = odoo.http.request request = odoo.http.request
try: try:
session = getattr(request, 'session', {}) session = getattr(request, "session", {})
except RuntimeError: except RuntimeError:
ctx = {} ctx = {}
else: else:
ctx = { ctx = {
'tags': { "tags": {"database": session.get("db", None)},
'database': session.get('db', None), "user": {
}, "login": session.get("login", None),
'user': { "uid": session.get("uid", None),
'login': session.get('login', None),
'uid': session.get('uid', None),
},
'extra': {
'context': session.get('context', {}),
}, },
"extra": {"context": session.get("context", {})},
} }
if request.httprequest: if request.httprequest:
ctx.update({ ctx.update({"request": get_request_info(request.httprequest)})
'request': get_request_info(request.httprequest),
})
return ctx return ctx
class LoggerNameFilter(logging.Filter): class LoggerNameFilter(logging.Filter):
''' """
Custom :class:`logging.Filter` which allows to filter loggers by name. Custom :class:`logging.Filter` which allows to filter loggers by name.
''' """
def __init__(self, loggers, name=''): def __init__(self, loggers, name=""):
super(LoggerNameFilter, self).__init__(name=name) super(LoggerNameFilter, self).__init__(name=name)
self._exclude_loggers = set(loggers) self._exclude_loggers = set(loggers)
@ -76,12 +70,12 @@ class LoggerNameFilter(logging.Filter):
class OdooSentryHandler(SentryHandler): class OdooSentryHandler(SentryHandler):
''' """
Customized :class:`raven.handlers.logging.SentryHandler`. Customized :class:`raven.handlers.logging.SentryHandler`.
Allows to add additional Odoo and HTTP request data to the event which is Allows to add additional Odoo and HTTP request data to the event which is
sent to Sentry. sent to Sentry.
''' """
def __init__(self, include_extra_context, *args, **kwargs): def __init__(self, include_extra_context, *args, **kwargs):
super(OdooSentryHandler, self).__init__(*args, **kwargs) super(OdooSentryHandler, self).__init__(*args, **kwargs)
@ -94,12 +88,10 @@ class OdooSentryHandler(SentryHandler):
class SanitizeOdooCookiesProcessor(SanitizePasswordsProcessor): class SanitizeOdooCookiesProcessor(SanitizePasswordsProcessor):
''' """
Custom :class:`raven.processors.Processor`. Custom :class:`raven.processors.Processor`.
Allows to sanitize sensitive Odoo cookies, namely the "session_id" cookie. Allows to sanitize sensitive Odoo cookies, namely the "session_id" cookie.
''' """
KEYS = FIELDS = frozenset([ KEYS = FIELDS = frozenset(["session_id"])
'session_id',
])

View File

@ -1,7 +1,4 @@
# Copyright 2016-2017 Versada <https://versada.eu/> # Copyright 2016-2017 Versada <https://versada.eu/>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import ( from . import test_client, test_logutils
test_client,
test_logutils,
)

View File

@ -20,25 +20,25 @@ def log_handler_by_class(logger, handler_cls):
def remove_logging_handler(logger_name, handler_cls): def remove_logging_handler(logger_name, handler_cls):
'''Removes handlers of specified classes from a :class:`logging.Logger` """Removes handlers of specified classes from a :class:`logging.Logger`
with a given name. with a given name.
:param string logger_name: name of the logger :param string logger_name: name of the logger
:param handler_cls: class of the handler to remove. You can pass a tuple of :param handler_cls: class of the handler to remove. You can pass a tuple of
classes to catch several classes classes to catch several classes
''' """
logger = logging.getLogger(logger_name) logger = logging.getLogger(logger_name)
for handler in log_handler_by_class(logger, handler_cls): for handler in log_handler_by_class(logger, handler_cls):
logger.removeHandler(handler) logger.removeHandler(handler)
class InMemoryClient(raven.Client): class InMemoryClient(raven.Client):
'''A :class:`raven.Client` subclass which simply stores events in a list. """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 Extended based on the one found in raven-python to avoid additional testing
dependencies: https://git.io/vyGO3 dependencies: https://git.io/vyGO3
''' """
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.events = [] self.events = []
@ -52,14 +52,12 @@ class InMemoryClient(raven.Client):
def has_event(self, event_level, event_msg): def has_event(self, event_level, event_msg):
for event in self.events: for event in self.events:
if (event.get('level') == event_level and if event.get("level") == event_level and event.get("message") == event_msg:
event.get('message') == event_msg):
return True return True
return False return False
class TestClientSetup(unittest.TestCase): class TestClientSetup(unittest.TestCase):
def setUp(self): def setUp(self):
super(TestClientSetup, self).setUp() super(TestClientSetup, self).setUp()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
@ -68,57 +66,54 @@ class TestClientSetup(unittest.TestCase):
# when the module is loaded. After that, subsequent calls to # when the module is loaded. After that, subsequent calls to
# setup_logging will not re-add our handler. We explicitly remove # setup_logging will not re-add our handler. We explicitly remove
# OdooSentryHandler handler so we can test with our in-memory client. # OdooSentryHandler handler so we can test with our in-memory client.
remove_logging_handler('', OdooSentryHandler) remove_logging_handler("", OdooSentryHandler)
def assertEventCaptured(self, client, event_level, event_msg): def assertEventCaptured(self, client, event_level, event_msg):
self.assertTrue( self.assertTrue(
client.has_event(event_level, event_msg), client.has_event(event_level, event_msg),
msg='Event: "%s" was not captured' % event_msg msg='Event: "%s" was not captured' % event_msg,
) )
def assertEventNotCaptured(self, client, event_level, event_msg): def assertEventNotCaptured(self, client, event_level, event_msg):
self.assertFalse( self.assertFalse(
client.has_event(event_level, event_msg), client.has_event(event_level, event_msg),
msg='Event: "%s" was captured' % event_msg msg='Event: "%s" was captured' % event_msg,
) )
def test_initialize_raven_sets_dsn(self): def test_initialize_raven_sets_dsn(self):
config = { config = {
'sentry_enabled': True, "sentry_enabled": True,
'sentry_dsn': 'http://public:secret@example.com/1', "sentry_dsn": "http://public:secret@example.com/1",
} }
client = initialize_raven(config, client_cls=InMemoryClient) client = initialize_raven(config, client_cls=InMemoryClient)
self.assertEqual(client.remote.base_url, 'http://example.com') self.assertEqual(client.remote.base_url, "http://example.com")
def test_capture_event(self): def test_capture_event(self):
config = { config = {
'sentry_enabled': True, "sentry_enabled": True,
'sentry_dsn': 'http://public:secret@example.com/1', "sentry_dsn": "http://public:secret@example.com/1",
} }
level, msg = logging.WARNING, 'Test event, can be ignored' level, msg = logging.WARNING, "Test event, can be ignored"
client = initialize_raven(config, client_cls=InMemoryClient) client = initialize_raven(config, client_cls=InMemoryClient)
self.logger.log(level, msg) self.logger.log(level, msg)
self.assertEventCaptured(client, level, msg) self.assertEventCaptured(client, level, msg)
def test_ignore_exceptions(self): def test_ignore_exceptions(self):
config = { config = {
'sentry_enabled': True, "sentry_enabled": True,
'sentry_dsn': 'http://public:secret@example.com/1', "sentry_dsn": "http://public:secret@example.com/1",
'sentry_ignore_exceptions': 'odoo.exceptions.UserError', "sentry_ignore_exceptions": "odoo.exceptions.UserError",
} }
level, msg = logging.WARNING, 'Test UserError' level, msg = logging.WARNING, "Test UserError"
client = initialize_raven(config, client_cls=InMemoryClient) client = initialize_raven(config, client_cls=InMemoryClient)
handlers = list( handlers = list(log_handler_by_class(logging.getLogger(), OdooSentryHandler))
log_handler_by_class(logging.getLogger(), OdooSentryHandler)
)
self.assertTrue(handlers) self.assertTrue(handlers)
handler = handlers[0] handler = handlers[0]
try: try:
raise exceptions.UserError(msg) raise exceptions.UserError(msg)
except exceptions.UserError: except exceptions.UserError:
exc_info = sys.exc_info() exc_info = sys.exc_info()
record = logging.LogRecord( record = logging.LogRecord(__name__, level, __file__, 42, msg, (), exc_info)
__name__, level, __file__, 42, msg, (), exc_info)
handler.emit(record) handler.emit(record)
self.assertEventNotCaptured(client, level, msg) self.assertEventNotCaptured(client, level, msg)

View File

@ -9,69 +9,62 @@ from ..logutils import SanitizeOdooCookiesProcessor
class TestOdooCookieSanitizer(unittest.TestCase): class TestOdooCookieSanitizer(unittest.TestCase):
def test_cookie_as_string(self): def test_cookie_as_string(self):
data = { data = {
'request': { "request": {
'cookies': 'website_lang=en_us;' "cookies": "website_lang=en_us;"
'session_id=hello;' "session_id=hello;"
'Session_ID=hello;' "Session_ID=hello;"
'foo=bar', "foo=bar"
}, }
} }
proc = SanitizeOdooCookiesProcessor(mock.Mock()) proc = SanitizeOdooCookiesProcessor(mock.Mock())
result = proc.process(data) result = proc.process(data)
self.assertTrue('request' in result) self.assertTrue("request" in result)
http = result['request'] http = result["request"]
self.assertEqual( self.assertEqual(
http['cookies'], http["cookies"],
'website_lang=en_us;' "website_lang=en_us;"
'session_id={m};' "session_id={m};"
'Session_ID={m};' "Session_ID={m};"
'foo=bar'.format( "foo=bar".format(m=proc.MASK),
m=proc.MASK,
),
) )
def test_cookie_as_string_with_partials(self): def test_cookie_as_string_with_partials(self):
data = { data = {"request": {"cookies": "website_lang=en_us;session_id;foo=bar"}}
'request': {
'cookies': 'website_lang=en_us;session_id;foo=bar',
},
}
proc = SanitizeOdooCookiesProcessor(mock.Mock()) proc = SanitizeOdooCookiesProcessor(mock.Mock())
result = proc.process(data) result = proc.process(data)
self.assertTrue('request' in result) self.assertTrue("request" in result)
http = result['request'] http = result["request"]
self.assertEqual( self.assertEqual(
http['cookies'], http["cookies"], "website_lang=en_us;session_id;foo=bar".format(m=proc.MASK)
'website_lang=en_us;session_id;foo=bar'.format(m=proc.MASK),
) )
def test_cookie_header(self): def test_cookie_header(self):
data = { data = {
'request': { "request": {
'headers': { "headers": {
'Cookie': 'foo=bar;' "Cookie": "foo=bar;"
'session_id=hello;' "session_id=hello;"
'Session_ID=hello;' "Session_ID=hello;"
'a_session_id_here=hello', "a_session_id_here=hello"
}, }
}, }
} }
proc = SanitizeOdooCookiesProcessor(mock.Mock()) proc = SanitizeOdooCookiesProcessor(mock.Mock())
result = proc.process(data) result = proc.process(data)
self.assertTrue('request' in result) self.assertTrue("request" in result)
http = result['request'] http = result["request"]
self.assertEqual( self.assertEqual(
http['headers']['Cookie'], http["headers"]["Cookie"],
'foo=bar;' "foo=bar;"
'session_id={m};' "session_id={m};"
'Session_ID={m};' "Session_ID={m};"
'a_session_id_here={m}'.format(m=proc.MASK)) "a_session_id_here={m}".format(m=proc.MASK),
)