[IMP] sentry: migrate sentry-raven to new api sentry-sdk
parent
b93b9b8e22
commit
f176b8bd9d
|
@ -33,6 +33,20 @@ Odoo.
|
||||||
.. contents::
|
.. contents::
|
||||||
:local:
|
: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
|
Configuration
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
@ -67,16 +81,6 @@ configuration file:
|
||||||
odoo.exceptions.Warning,
|
odoo.exceptions.Warning,
|
||||||
odoo.exceptions.except_orm``
|
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``
|
``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
|
HTTP request and user session (if available). This has no effect
|
||||||
for Cron jobs, as no request/session is available inside a Cron job.
|
for Cron jobs, as no request/session is available inside a Cron job.
|
||||||
|
@ -94,11 +98,14 @@ configuration file:
|
||||||
============================= ==================================================================== ==========================================================
|
============================= ==================================================================== ==========================================================
|
||||||
|
|
||||||
Other `client arguments
|
Other `client arguments
|
||||||
<https://docs.sentry.io/clients/python/advanced/#client-arguments>`_ can be
|
<https://docs.sentry.io/platforms/python/configuration/>`_ can be
|
||||||
configured by prepending the argument name with *sentry_* in your Odoo config
|
configured by prepending the argument name with *sentry_* in your Odoo config
|
||||||
file. Currently supported additional client arguments are: ``install_sys_hook,
|
file. Currently supported additional client arguments are: ``with_locals,
|
||||||
include_paths, exclude_paths, machine, auto_log_stacks, capture_locals,
|
max_breadcrumbs, release, environment, server_name, shutdown_timeout,
|
||||||
string_max_length, list_max_length, site, include_versions, environment``.
|
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
|
Example Odoo configuration
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -110,14 +117,15 @@ Below is an example of Odoo configuration file with *Odoo Sentry* options::
|
||||||
sentry_enabled = true
|
sentry_enabled = true
|
||||||
sentry_logging_level = warn
|
sentry_logging_level = warn
|
||||||
sentry_exclude_loggers = werkzeug
|
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_ignore_exceptions = odoo.exceptions.AccessDenied,
|
||||||
sentry_processors = raven.processors.SanitizePasswordsProcessor,odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor
|
odoo.exceptions.AccessError,odoo.exceptions.MissingError,
|
||||||
sentry_transport = threaded
|
odoo.exceptions.RedirectWarning,odoo.exceptions.UserError,
|
||||||
|
odoo.exceptions.ValidationError,odoo.exceptions.Warning,
|
||||||
|
odoo.exceptions.except_orm
|
||||||
sentry_include_context = true
|
sentry_include_context = true
|
||||||
sentry_environment = production
|
sentry_environment = production
|
||||||
sentry_auto_log_stacks = false
|
|
||||||
sentry_odoo_dir = /home/odoo/odoo/
|
|
||||||
sentry_release = 1.3.2
|
sentry_release = 1.3.2
|
||||||
|
sentry_odoo_dir = /home/odoo/odoo/
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
|
@ -127,7 +135,7 @@ above the configured Sentry logging level, no additional actions are necessary.
|
||||||
|
|
||||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||||
:alt: Try me on Runbot
|
:alt: Try me on Runbot
|
||||||
:target: https://runbot.odoo-community.org/runbot/149/13.0
|
:target: https://runbot.odoo-community.org/runbot/149/14.0
|
||||||
|
|
||||||
Known issues / Roadmap
|
Known issues / Roadmap
|
||||||
======================
|
======================
|
||||||
|
@ -163,6 +171,7 @@ Authors
|
||||||
* Mohammed Barsi
|
* Mohammed Barsi
|
||||||
* Versada
|
* Versada
|
||||||
* Nicolas JEUDY
|
* Nicolas JEUDY
|
||||||
|
* Vauxoo
|
||||||
|
|
||||||
Contributors
|
Contributors
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
@ -172,6 +181,11 @@ Contributors
|
||||||
* Naglis Jonaitis <naglis@versada.eu>
|
* Naglis Jonaitis <naglis@versada.eu>
|
||||||
* Atte Isopuro <atte.isopuro@avoin.systems>
|
* Atte Isopuro <atte.isopuro@avoin.systems>
|
||||||
|
|
||||||
|
Other credits
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Vauxoo
|
||||||
|
|
||||||
Maintainers
|
Maintainers
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -185,6 +199,26 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
mission is to support the collaborative development of Odoo features and
|
mission is to support the collaborative development of Odoo features and
|
||||||
promote its widespread use.
|
promote its widespread use.
|
||||||
|
|
||||||
|
.. |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 <https://odoo-community.org/page/maintainer-role>`__:
|
||||||
|
|
||||||
|
|maintainer-barsi| |maintainer-naglis| |maintainer-versada| |maintainer-moylop260| |maintainer-fernandahf|
|
||||||
|
|
||||||
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/14.0/sentry>`_ project on GitHub.
|
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/14.0/sentry>`_ project on GitHub.
|
||||||
|
|
||||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||||
|
|
|
@ -1,86 +1 @@
|
||||||
# Copyright 2016-2017 Versada <https://versada.eu/>
|
from .hooks import post_load
|
||||||
# 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
|
|
||||||
|
|
||||||
from collections import abc
|
|
||||||
|
|
||||||
_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 = {}
|
|
||||||
for option in const.get_sentry_options():
|
|
||||||
value = config.get("sentry_%s" % option.key, option.default)
|
|
||||||
if isinstance(option.converter, abc.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
|
|
||||||
|
|
||||||
if not options.get("release"):
|
|
||||||
options["release"] = config.get(
|
|
||||||
"sentry_release", get_odoo_commit(config.get("sentry_odoo_dir"))
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
|
@ -3,16 +3,25 @@
|
||||||
{
|
{
|
||||||
"name": "Sentry",
|
"name": "Sentry",
|
||||||
"summary": "Report Odoo errors to Sentry",
|
"summary": "Report Odoo errors to Sentry",
|
||||||
"version": "14.0.1.0.2",
|
"version": "14.0.1.0.0",
|
||||||
"category": "Extra Tools",
|
"category": "Extra Tools",
|
||||||
"website": "https://github.com/OCA/server-tools",
|
"website": "https://github.com/OCA/server-tools",
|
||||||
"author": "Mohammed Barsi,"
|
"author": "Mohammed Barsi,"
|
||||||
"Versada,"
|
"Versada,"
|
||||||
"Nicolas JEUDY,"
|
"Nicolas JEUDY,"
|
||||||
"Odoo Community Association (OCA)",
|
"Odoo Community Association (OCA),"
|
||||||
|
"Vauxoo",
|
||||||
|
"maintainers": ["barsi", "naglis", "versada", "moylop260", "fernandahf"],
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"application": False,
|
"application": False,
|
||||||
"installable": True,
|
"installable": True,
|
||||||
"external_dependencies": {"python": ["raven"]},
|
"external_dependencies": {
|
||||||
"depends": ["base"],
|
"python": [
|
||||||
|
"sentry_sdk",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"depends": [
|
||||||
|
"base",
|
||||||
|
],
|
||||||
|
"post_load": "post_load",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
# 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).
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from sentry_sdk import HttpTransport
|
||||||
|
from sentry_sdk.consts import DEFAULT_OPTIONS
|
||||||
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||||
|
|
||||||
import odoo.loglevels
|
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):
|
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."""
|
||||||
|
@ -43,43 +40,67 @@ ODOO_USER_EXCEPTIONS = [
|
||||||
]
|
]
|
||||||
DEFAULT_IGNORED_EXCEPTIONS = ",".join(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",)
|
EXCLUDE_LOGGERS = ("werkzeug",)
|
||||||
DEFAULT_EXCLUDE_LOGGERS = ",".join(EXCLUDE_LOGGERS)
|
DEFAULT_EXCLUDE_LOGGERS = ",".join(EXCLUDE_LOGGERS)
|
||||||
|
|
||||||
|
DEFAULT_ENVIRONMENT = "develop"
|
||||||
|
|
||||||
DEFAULT_TRANSPORT = "threaded"
|
DEFAULT_TRANSPORT = "threaded"
|
||||||
|
|
||||||
|
|
||||||
def select_transport(name=DEFAULT_TRANSPORT):
|
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 {
|
return {
|
||||||
"requests_synchronous": raven.transport.RequestsHTTPTransport,
|
"threaded": HttpTransport,
|
||||||
"requests_threaded": raven.transport.ThreadedRequestsHTTPTransport,
|
}.get(name, HttpTransport)
|
||||||
"synchronous": raven.transport.HTTPTransport,
|
|
||||||
"threaded": raven.transport.ThreadedHTTPTransport,
|
|
||||||
}.get(name, DEFAULT_TRANSPORT)
|
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():
|
def get_sentry_options():
|
||||||
return [
|
return [
|
||||||
SentryOption("dsn", "", str.strip),
|
SentryOption("dsn", "", str.strip),
|
||||||
SentryOption("install_sys_hook", False, None),
|
SentryOption("transport", DEFAULT_OPTIONS["transport"], select_transport),
|
||||||
SentryOption("transport", DEFAULT_TRANSPORT, select_transport),
|
SentryOption("logging_level", DEFAULT_LOG_LEVEL, get_sentry_logging),
|
||||||
SentryOption("include_paths", "", split_multiple),
|
SentryOption("with_locals", DEFAULT_OPTIONS["with_locals"], None),
|
||||||
SentryOption("exclude_paths", "", split_multiple),
|
SentryOption("max_breadcrumbs", DEFAULT_OPTIONS["max_breadcrumbs"], None),
|
||||||
SentryOption("machine", defaults.NAME, None),
|
SentryOption("release", DEFAULT_OPTIONS["release"], None),
|
||||||
SentryOption("auto_log_stacks", defaults.AUTO_LOG_STACKS, None),
|
SentryOption("environment", DEFAULT_OPTIONS["environment"], None),
|
||||||
SentryOption("capture_locals", defaults.CAPTURE_LOCALS, None),
|
SentryOption("server_name", DEFAULT_OPTIONS["server_name"], None),
|
||||||
SentryOption("string_max_length", defaults.MAX_LENGTH_STRING, None),
|
SentryOption("shutdown_timeout", DEFAULT_OPTIONS["shutdown_timeout"], None),
|
||||||
SentryOption("list_max_length", defaults.MAX_LENGTH_LIST, None),
|
SentryOption("integrations", DEFAULT_OPTIONS["integrations"], None),
|
||||||
SentryOption("site", None, None),
|
SentryOption(
|
||||||
SentryOption("include_versions", True, None),
|
"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("ignore_exceptions", DEFAULT_IGNORED_EXCEPTIONS, split_multiple),
|
||||||
SentryOption("processors", DEFAULT_PROCESSORS, split_multiple),
|
SentryOption("request_bodies", DEFAULT_OPTIONS["request_bodies"], None),
|
||||||
SentryOption("environment", None, None),
|
SentryOption("attach_stacktrace", DEFAULT_OPTIONS["attach_stacktrace"], None),
|
||||||
SentryOption("release", None, 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,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
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 = {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]
|
|
@ -0,0 +1,123 @@
|
||||||
|
# Copyright 2016-2017 Versada <https://versada.eu/>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections import abc
|
||||||
|
|
||||||
|
import odoo.http
|
||||||
|
from odoo.service import wsgi_server
|
||||||
|
from odoo.tools import config as odoo_config
|
||||||
|
|
||||||
|
from . import const
|
||||||
|
from .logutils import (
|
||||||
|
InvalidGitRepository,
|
||||||
|
SanitizeOdooCookiesProcessor,
|
||||||
|
fetch_git_sha,
|
||||||
|
get_extra_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
HAS_SENTRY_SDK = True
|
||||||
|
try:
|
||||||
|
import sentry_sdk
|
||||||
|
from sentry_sdk.integrations.logging import ignore_logger
|
||||||
|
from sentry_sdk.integrations.threading import ThreadingIntegration
|
||||||
|
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
|
||||||
|
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 = {}
|
||||||
|
for option in const.get_sentry_options():
|
||||||
|
value = config.get("sentry_%s" % option.key, option.default)
|
||||||
|
if isinstance(option.converter, abc.Callable):
|
||||||
|
value = option.converter(value)
|
||||||
|
options[option.key] = value
|
||||||
|
|
||||||
|
exclude_loggers = const.split_multiple(
|
||||||
|
config.get("sentry_exclude_loggers", const.DEFAULT_EXCLUDE_LOGGERS)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not options.get("release"):
|
||||||
|
options["release"] = config.get(
|
||||||
|
"sentry_release", get_odoo_commit(config.get("sentry_odoo_dir"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
|
@ -1,20 +1,14 @@
|
||||||
# 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).
|
||||||
|
|
||||||
import logging
|
import os.path
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import odoo.http
|
from sentry_sdk._compat import text_type
|
||||||
|
from werkzeug import datastructures
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
from .generalutils import get_environ
|
||||||
try:
|
from .processor import SanitizePasswordsProcessor
|
||||||
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):
|
def get_request_info(request):
|
||||||
|
@ -28,70 +22,99 @@ def get_request_info(request):
|
||||||
"url": "{}://{}{}".format(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(datastructures.EnvironHeaders(request.environ)),
|
||||||
"env": dict(get_environ(request.environ)),
|
"env": dict(get_environ(request.environ)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_extra_context():
|
def get_extra_context(request):
|
||||||
"""
|
"""
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
session = getattr(request, "session", {})
|
session = getattr(request, "session", {})
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
ctx = {}
|
ctx = {}
|
||||||
else:
|
else:
|
||||||
ctx = {
|
ctx = {
|
||||||
"tags": {"database": session.get("db", None)},
|
"tags": {
|
||||||
"user": {
|
"database": session.get("db", None),
|
||||||
"login": session.get("login", None),
|
},
|
||||||
"uid": session.get("uid", None),
|
"user": {
|
||||||
|
"email": session.get("login", None),
|
||||||
|
"id": session.get("uid", None),
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"context": session.get("context", {}),
|
||||||
},
|
},
|
||||||
"extra": {"context": session.get("context", {})},
|
|
||||||
}
|
}
|
||||||
if request.httprequest:
|
if request.httprequest:
|
||||||
ctx.update({"request": get_request_info(request.httprequest)})
|
ctx.update({"request": get_request_info(request.httprequest)})
|
||||||
return ctx
|
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):
|
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(["session_id"])
|
KEYS = frozenset(
|
||||||
|
[
|
||||||
|
"session_id",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidGitRepository(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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,)
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(head_path, "r") as fp:
|
||||||
|
head = text_type(fp.read()).strip()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
""" 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
|
|
@ -29,16 +29,6 @@ configuration file:
|
||||||
odoo.exceptions.Warning,
|
odoo.exceptions.Warning,
|
||||||
odoo.exceptions.except_orm``
|
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``
|
``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
|
HTTP request and user session (if available). This has no effect
|
||||||
for Cron jobs, as no request/session is available inside a Cron job.
|
for Cron jobs, as no request/session is available inside a Cron job.
|
||||||
|
@ -56,11 +46,14 @@ configuration file:
|
||||||
============================= ==================================================================== ==========================================================
|
============================= ==================================================================== ==========================================================
|
||||||
|
|
||||||
Other `client arguments
|
Other `client arguments
|
||||||
<https://docs.sentry.io/clients/python/advanced/#client-arguments>`_ can be
|
<https://docs.sentry.io/platforms/python/configuration/>`_ can be
|
||||||
configured by prepending the argument name with *sentry_* in your Odoo config
|
configured by prepending the argument name with *sentry_* in your Odoo config
|
||||||
file. Currently supported additional client arguments are: ``install_sys_hook,
|
file. Currently supported additional client arguments are: ``with_locals,
|
||||||
include_paths, exclude_paths, machine, auto_log_stacks, capture_locals,
|
max_breadcrumbs, release, environment, server_name, shutdown_timeout,
|
||||||
string_max_length, list_max_length, site, include_versions, environment``.
|
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
|
Example Odoo configuration
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -72,11 +65,12 @@ Below is an example of Odoo configuration file with *Odoo Sentry* options::
|
||||||
sentry_enabled = true
|
sentry_enabled = true
|
||||||
sentry_logging_level = warn
|
sentry_logging_level = warn
|
||||||
sentry_exclude_loggers = werkzeug
|
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_ignore_exceptions = odoo.exceptions.AccessDenied,
|
||||||
sentry_processors = raven.processors.SanitizePasswordsProcessor,odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor
|
odoo.exceptions.AccessError,odoo.exceptions.MissingError,
|
||||||
sentry_transport = threaded
|
odoo.exceptions.RedirectWarning,odoo.exceptions.UserError,
|
||||||
|
odoo.exceptions.ValidationError,odoo.exceptions.Warning,
|
||||||
|
odoo.exceptions.except_orm
|
||||||
sentry_include_context = true
|
sentry_include_context = true
|
||||||
sentry_environment = production
|
sentry_environment = production
|
||||||
sentry_auto_log_stacks = false
|
|
||||||
sentry_odoo_dir = /home/odoo/odoo/
|
|
||||||
sentry_release = 1.3.2
|
sentry_release = 1.3.2
|
||||||
|
sentry_odoo_dir = /home/odoo/odoo/
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
* Andrius Preimantas <andrius@versada.eu>
|
* Andrius Preimantas <andrius@versada.eu>
|
||||||
* Naglis Jonaitis <naglis@versada.eu>
|
* Naglis Jonaitis <naglis@versada.eu>
|
||||||
* Atte Isopuro <atte.isopuro@avoin.systems>
|
* Atte Isopuro <atte.isopuro@avoin.systems>
|
||||||
|
* Florian Mounier <florian.mounier@akretion.com>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
* Vauxoo
|
|
@ -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
|
|
@ -3,4 +3,4 @@ above the configured Sentry logging level, no additional actions are necessary.
|
||||||
|
|
||||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||||
:alt: Try me on Runbot
|
:alt: Try me on Runbot
|
||||||
:target: https://runbot.odoo-community.org/runbot/149/13.0
|
:target: https://runbot.odoo-community.org/runbot/149/14.0
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
|
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
|
||||||
<title>Sentry</title>
|
<title>Sentry</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
|
@ -373,23 +373,38 @@ Odoo.</p>
|
||||||
<p><strong>Table of contents</strong></p>
|
<p><strong>Table of contents</strong></p>
|
||||||
<div class="contents local topic" id="contents">
|
<div class="contents local topic" id="contents">
|
||||||
<ul class="simple">
|
<ul class="simple">
|
||||||
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a><ul>
|
<li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
|
||||||
<li><a class="reference internal" href="#example-odoo-configuration" id="id2">Example Odoo configuration</a></li>
|
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a><ul>
|
||||||
|
<li><a class="reference internal" href="#example-odoo-configuration" id="id3">Example Odoo configuration</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li>
|
<li><a class="reference internal" href="#usage" id="id4">Usage</a></li>
|
||||||
<li><a class="reference internal" href="#known-issues-roadmap" id="id4">Known issues / Roadmap</a></li>
|
<li><a class="reference internal" href="#known-issues-roadmap" id="id5">Known issues / Roadmap</a></li>
|
||||||
<li><a class="reference internal" href="#bug-tracker" id="id5">Bug Tracker</a></li>
|
<li><a class="reference internal" href="#bug-tracker" id="id6">Bug Tracker</a></li>
|
||||||
<li><a class="reference internal" href="#credits" id="id6">Credits</a><ul>
|
<li><a class="reference internal" href="#credits" id="id7">Credits</a><ul>
|
||||||
<li><a class="reference internal" href="#authors" id="id7">Authors</a></li>
|
<li><a class="reference internal" href="#authors" id="id8">Authors</a></li>
|
||||||
<li><a class="reference internal" href="#contributors" id="id8">Contributors</a></li>
|
<li><a class="reference internal" href="#contributors" id="id9">Contributors</a></li>
|
||||||
<li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li>
|
<li><a class="reference internal" href="#other-credits" id="id10">Other credits</a></li>
|
||||||
|
<li><a class="reference internal" href="#maintainers" id="id11">Maintainers</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section" id="installation">
|
||||||
|
<h1><a class="toc-backref" href="#id1">Installation</a></h1>
|
||||||
|
<p>The module can be installed just like any other Odoo module, by adding the
|
||||||
|
module’s directory to Odoo <em>addons_path</em>. 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 <tt class="docutils literal">server_wide_modules</tt> parameter in your
|
||||||
|
Odoo config file or with the <tt class="docutils literal"><span class="pre">--load</span></tt> command-line parameter.</p>
|
||||||
|
<p>This module additionally requires the sentry-sdk Python package to be available on
|
||||||
|
the system. It can be installed using pip:</p>
|
||||||
|
<pre class="literal-block">
|
||||||
|
pip install sentry-sdk
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
<div class="section" id="configuration">
|
<div class="section" id="configuration">
|
||||||
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
|
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
|
||||||
<p>The following additional configuration options can be added to your Odoo
|
<p>The following additional configuration options can be added to your Odoo
|
||||||
configuration file:</p>
|
configuration file:</p>
|
||||||
<table border="1" class="docutils">
|
<table border="1" class="docutils">
|
||||||
|
@ -442,21 +457,6 @@ odoo.exceptions.ValidationError,
|
||||||
odoo.exceptions.Warning,
|
odoo.exceptions.Warning,
|
||||||
odoo.exceptions.except_orm</tt></td>
|
odoo.exceptions.except_orm</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr><td><tt class="docutils literal">sentry_processors</tt></td>
|
|
||||||
<td>A string of comma-separated processor classes which will be applied
|
|
||||||
on an event before sending it to Sentry.</td>
|
|
||||||
<td><tt class="docutils literal">raven.processors.SanitizePasswordsProcessor,
|
|
||||||
odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor</tt></td>
|
|
||||||
</tr>
|
|
||||||
<tr><td><tt class="docutils literal">sentry_transport</tt></td>
|
|
||||||
<td>Transport class which will be used to send events to Sentry.
|
|
||||||
Possible values: <em>threaded</em>: spawns an async worker for processing
|
|
||||||
messages, <em>synchronous</em>: a synchronous blocking transport;
|
|
||||||
<em>requests_threaded</em>: an asynchronous transport using the <em>requests</em>
|
|
||||||
library; <em>requests_synchronous</em> - blocking transport using the
|
|
||||||
<em>requests</em> library.</td>
|
|
||||||
<td><tt class="docutils literal">threaded</tt></td>
|
|
||||||
</tr>
|
|
||||||
<tr><td><tt class="docutils literal">sentry_include_context</tt></td>
|
<tr><td><tt class="docutils literal">sentry_include_context</tt></td>
|
||||||
<td>If enabled, additional context data will be extracted from current
|
<td>If enabled, additional context data will be extracted from current
|
||||||
HTTP request and user session (if available). This has no effect
|
HTTP request and user session (if available). This has no effect
|
||||||
|
@ -480,13 +480,16 @@ Overridden by <em>sentry_release</em></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p>Other <a class="reference external" href="https://docs.sentry.io/clients/python/advanced/#client-arguments">client arguments</a> can be
|
<p>Other <a class="reference external" href="https://docs.sentry.io/platforms/python/configuration/">client arguments</a> can be
|
||||||
configured by prepending the argument name with <em>sentry_</em> in your Odoo config
|
configured by prepending the argument name with <em>sentry_</em> in your Odoo config
|
||||||
file. Currently supported additional client arguments are: <tt class="docutils literal">install_sys_hook,
|
file. Currently supported additional client arguments are: <tt class="docutils literal">with_locals,
|
||||||
include_paths, exclude_paths, machine, auto_log_stacks, capture_locals,
|
max_breadcrumbs, release, environment, server_name, shutdown_timeout,
|
||||||
string_max_length, list_max_length, site, include_versions, environment</tt>.</p>
|
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</tt>.</p>
|
||||||
<div class="section" id="example-odoo-configuration">
|
<div class="section" id="example-odoo-configuration">
|
||||||
<h2><a class="toc-backref" href="#id2">Example Odoo configuration</a></h2>
|
<h2><a class="toc-backref" href="#id3">Example Odoo configuration</a></h2>
|
||||||
<p>Below is an example of Odoo configuration file with <em>Odoo Sentry</em> options:</p>
|
<p>Below is an example of Odoo configuration file with <em>Odoo Sentry</em> options:</p>
|
||||||
<pre class="literal-block">
|
<pre class="literal-block">
|
||||||
[options]
|
[options]
|
||||||
|
@ -494,25 +497,26 @@ sentry_dsn = https://<public_key>:<secret_key>@sentry.example.co
|
||||||
sentry_enabled = true
|
sentry_enabled = true
|
||||||
sentry_logging_level = warn
|
sentry_logging_level = warn
|
||||||
sentry_exclude_loggers = werkzeug
|
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_ignore_exceptions = odoo.exceptions.AccessDenied,
|
||||||
sentry_processors = raven.processors.SanitizePasswordsProcessor,odoo.addons.sentry.logutils.SanitizeOdooCookiesProcessor
|
odoo.exceptions.AccessError,odoo.exceptions.MissingError,
|
||||||
sentry_transport = threaded
|
odoo.exceptions.RedirectWarning,odoo.exceptions.UserError,
|
||||||
|
odoo.exceptions.ValidationError,odoo.exceptions.Warning,
|
||||||
|
odoo.exceptions.except_orm
|
||||||
sentry_include_context = true
|
sentry_include_context = true
|
||||||
sentry_environment = production
|
sentry_environment = production
|
||||||
sentry_auto_log_stacks = false
|
|
||||||
sentry_odoo_dir = /home/odoo/odoo/
|
|
||||||
sentry_release = 1.3.2
|
sentry_release = 1.3.2
|
||||||
|
sentry_odoo_dir = /home/odoo/odoo/
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="usage">
|
<div class="section" id="usage">
|
||||||
<h1><a class="toc-backref" href="#id3">Usage</a></h1>
|
<h1><a class="toc-backref" href="#id4">Usage</a></h1>
|
||||||
<p>Once configured and installed, the module will report any logging event at and
|
<p>Once configured and installed, the module will report any logging event at and
|
||||||
above the configured Sentry logging level, no additional actions are necessary.</p>
|
above the configured Sentry logging level, no additional actions are necessary.</p>
|
||||||
<a class="reference external image-reference" href="https://runbot.odoo-community.org/runbot/149/13.0"><img alt="Try me on Runbot" src="https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas" /></a>
|
<a class="reference external image-reference" href="https://runbot.odoo-community.org/runbot/149/14.0"><img alt="Try me on Runbot" src="https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas" /></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="known-issues-roadmap">
|
<div class="section" id="known-issues-roadmap">
|
||||||
<h1><a class="toc-backref" href="#id4">Known issues / Roadmap</a></h1>
|
<h1><a class="toc-backref" href="#id5">Known issues / Roadmap</a></h1>
|
||||||
<ul class="simple">
|
<ul class="simple">
|
||||||
<li><strong>No database separation</strong> – This module functions by intercepting all Odoo
|
<li><strong>No database separation</strong> – This module functions by intercepting all Odoo
|
||||||
logging records in a running Odoo process. This means that once installed in
|
logging records in a running Odoo process. This means that once installed in
|
||||||
|
@ -527,7 +531,7 @@ describe what they were doing when things went wrong.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="bug-tracker">
|
<div class="section" id="bug-tracker">
|
||||||
<h1><a class="toc-backref" href="#id5">Bug Tracker</a></h1>
|
<h1><a class="toc-backref" href="#id6">Bug Tracker</a></h1>
|
||||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>.
|
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>.
|
||||||
In case of trouble, please check there if your issue has already been reported.
|
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
|
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||||
|
@ -535,17 +539,18 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
|
||||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="credits">
|
<div class="section" id="credits">
|
||||||
<h1><a class="toc-backref" href="#id6">Credits</a></h1>
|
<h1><a class="toc-backref" href="#id7">Credits</a></h1>
|
||||||
<div class="section" id="authors">
|
<div class="section" id="authors">
|
||||||
<h2><a class="toc-backref" href="#id7">Authors</a></h2>
|
<h2><a class="toc-backref" href="#id8">Authors</a></h2>
|
||||||
<ul class="simple">
|
<ul class="simple">
|
||||||
<li>Mohammed Barsi</li>
|
<li>Mohammed Barsi</li>
|
||||||
<li>Versada</li>
|
<li>Versada</li>
|
||||||
<li>Nicolas JEUDY</li>
|
<li>Nicolas JEUDY</li>
|
||||||
|
<li>Vauxoo</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="contributors">
|
<div class="section" id="contributors">
|
||||||
<h2><a class="toc-backref" href="#id8">Contributors</a></h2>
|
<h2><a class="toc-backref" href="#id9">Contributors</a></h2>
|
||||||
<ul class="simple">
|
<ul class="simple">
|
||||||
<li>Mohammed Barsi <<a class="reference external" href="mailto:barsintod@gmail.com">barsintod@gmail.com</a>></li>
|
<li>Mohammed Barsi <<a class="reference external" href="mailto:barsintod@gmail.com">barsintod@gmail.com</a>></li>
|
||||||
<li>Andrius Preimantas <<a class="reference external" href="mailto:andrius@versada.eu">andrius@versada.eu</a>></li>
|
<li>Andrius Preimantas <<a class="reference external" href="mailto:andrius@versada.eu">andrius@versada.eu</a>></li>
|
||||||
|
@ -553,13 +558,21 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
|
||||||
<li>Atte Isopuro <<a class="reference external" href="mailto:atte.isopuro@avoin.systems">atte.isopuro@avoin.systems</a>></li>
|
<li>Atte Isopuro <<a class="reference external" href="mailto:atte.isopuro@avoin.systems">atte.isopuro@avoin.systems</a>></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section" id="other-credits">
|
||||||
|
<h2><a class="toc-backref" href="#id10">Other credits</a></h2>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Vauxoo</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<div class="section" id="maintainers">
|
<div class="section" id="maintainers">
|
||||||
<h2><a class="toc-backref" href="#id9">Maintainers</a></h2>
|
<h2><a class="toc-backref" href="#id11">Maintainers</a></h2>
|
||||||
<p>This module is maintained by the OCA.</p>
|
<p>This module is maintained by the OCA.</p>
|
||||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
mission is to support the collaborative development of Odoo features and
|
mission is to support the collaborative development of Odoo features and
|
||||||
promote its widespread use.</p>
|
promote its widespread use.</p>
|
||||||
|
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
|
||||||
|
<p><a class="reference external" href="https://github.com/barsi"><img alt="barsi" src="https://github.com/barsi.png?size=40px" /></a> <a class="reference external" href="https://github.com/naglis"><img alt="naglis" src="https://github.com/naglis.png?size=40px" /></a> <a class="reference external" href="https://github.com/versada"><img alt="versada" src="https://github.com/versada.png?size=40px" /></a> <a class="reference external" href="https://github.com/moylop260"><img alt="moylop260" src="https://github.com/moylop260.png?size=40px" /></a> <a class="reference external" href="https://github.com/fernandahf"><img alt="fernandahf" src="https://github.com/fernandahf.png?size=40px" /></a></p>
|
||||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/14.0/sentry">OCA/server-tools</a> project on GitHub.</p>
|
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/14.0/sentry">OCA/server-tools</a> project on GitHub.</p>
|
||||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,150 +3,148 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
|
||||||
|
|
||||||
import raven
|
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
from sentry_sdk.integrations.logging import _IGNORED_LOGGERS
|
||||||
|
from sentry_sdk.transport import HttpTransport
|
||||||
|
|
||||||
from odoo import exceptions
|
from odoo import exceptions
|
||||||
|
from odoo.tests import TransactionCase
|
||||||
|
from odoo.tools import config
|
||||||
|
|
||||||
from .. import initialize_raven
|
from ..hooks import initialize_sentry
|
||||||
from ..logutils import OdooSentryHandler
|
|
||||||
|
|
||||||
GIT_SHA = "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
|
GIT_SHA = "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
|
||||||
RELEASE = "test@1.2.3"
|
RELEASE = "test@1.2.3"
|
||||||
|
|
||||||
|
|
||||||
def log_handler_by_class(logger, handler_cls):
|
def remove_handler_ignore(handler_name):
|
||||||
for handler in logger.handlers:
|
"""Removes handlers of handlers ignored list."""
|
||||||
if isinstance(handler, handler_cls):
|
_IGNORED_LOGGERS.discard(handler_name)
|
||||||
yield handler
|
|
||||||
|
|
||||||
|
|
||||||
def remove_logging_handler(logger_name, handler_cls):
|
class TestException(exceptions.UserError):
|
||||||
"""Removes handlers of specified classes from a :class:`logging.Logger`
|
pass
|
||||||
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):
|
class InMemoryTransport(HttpTransport):
|
||||||
"""A :class:`raven.Client` subclass which simply stores events in a list.
|
"""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
|
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, *args, **kwargs):
|
||||||
self.events = []
|
self.events = []
|
||||||
super(InMemoryClient, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
def is_enabled(self):
|
def capture_event(self, event, *args, **kwargs):
|
||||||
return True
|
self.events.append(event)
|
||||||
|
|
||||||
def send(self, **kwargs):
|
|
||||||
self.events.append(kwargs)
|
|
||||||
|
|
||||||
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 event.get("message") == event_msg:
|
if (
|
||||||
|
event.get("level") == event_level
|
||||||
|
and event.get("logentry", {}).get("message") == event_msg
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def flush(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
class TestClientSetup(unittest.TestCase):
|
def kill(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestClientSetup(TransactionCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestClientSetup, self).setUp()
|
super(TestClientSetup, self).setUp()
|
||||||
self.logger = logging.getLogger(__name__)
|
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
|
||||||
|
|
||||||
# Sentry is enabled by default, so the default handler will be added
|
def log(self, level, msg, exc_info=None):
|
||||||
# when the module is loaded. After that, subsequent calls to
|
record = logging.LogRecord(__name__, level, __file__, 42, msg, (), exc_info)
|
||||||
# setup_logging will not re-add our handler. We explicitly remove
|
self.handler.emit(record)
|
||||||
# OdooSentryHandler handler so we can test with our in-memory client.
|
|
||||||
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.transport.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.transport.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 = {
|
self.assertEqual(self.client.dsn, self.dsn)
|
||||||
"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):
|
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"
|
level, msg = logging.WARNING, "Test event, can be ignored"
|
||||||
client = initialize_raven(config, client_cls=InMemoryClient)
|
self.log(level, msg)
|
||||||
self.logger.log(level, msg)
|
level = "warning"
|
||||||
self.assertEventCaptured(client, level, msg)
|
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):
|
def test_ignore_exceptions(self):
|
||||||
config = {
|
config.options["sentry_ignore_exceptions"] = "odoo.exceptions.UserError"
|
||||||
"sentry_enabled": True,
|
client = initialize_sentry(config)._client
|
||||||
"sentry_dsn": "http://public:secret@example.com/1",
|
client.transport = InMemoryTransport({"dsn": self.dsn})
|
||||||
"sentry_ignore_exceptions": "odoo.exceptions.UserError",
|
level, msg = logging.WARNING, "Test exception"
|
||||||
}
|
|
||||||
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:
|
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(__name__, level, __file__, 42, msg, (), exc_info)
|
self.log(level, msg, exc_info)
|
||||||
handler.emit(record)
|
level = "warning"
|
||||||
self.assertEventNotCaptured(client, level, msg)
|
self.assertEventNotCaptured(client, level, msg)
|
||||||
|
|
||||||
@patch("odoo.addons.sentry.get_odoo_commit", return_value=GIT_SHA)
|
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)
|
||||||
|
|
||||||
|
@patch("odoo.addons.sentry.hooks.get_odoo_commit", return_value=GIT_SHA)
|
||||||
def test_config_odoo_dir(self, get_odoo_commit):
|
def test_config_odoo_dir(self, get_odoo_commit):
|
||||||
config = {
|
config.options["sentry_odoo_dir"] = "/opt/odoo/core"
|
||||||
"sentry_enabled": True,
|
client = initialize_sentry(config)._client
|
||||||
"sentry_dsn": "http://public:secret@example.com/1",
|
|
||||||
"sentry_odoo_dir": "/opt/odoo/core",
|
|
||||||
}
|
|
||||||
client = initialize_raven(config, client_cls=InMemoryClient)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
client.release,
|
client.options["release"],
|
||||||
GIT_SHA,
|
GIT_SHA,
|
||||||
"Failed to use 'sentry_odoo_dir' parameter appropriately",
|
"Failed to use 'sentry_odoo_dir' parameter appropriately",
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("odoo.addons.sentry.get_odoo_commit", return_value=GIT_SHA)
|
@patch("odoo.addons.sentry.hooks.get_odoo_commit", return_value=GIT_SHA)
|
||||||
def test_config_release(self, get_odoo_commit):
|
def test_config_release(self, get_odoo_commit):
|
||||||
config = {
|
config.options["sentry_odoo_dir"] = "/opt/odoo/core"
|
||||||
"sentry_enabled": True,
|
config.options["sentry_release"] = RELEASE
|
||||||
"sentry_dsn": "http://public:secret@example.com/1",
|
client = initialize_sentry(config)._client
|
||||||
"sentry_odoo_dir": "/opt/odoo/core",
|
|
||||||
"sentry_release": RELEASE,
|
|
||||||
}
|
|
||||||
client = initialize_raven(config, client_cls=InMemoryClient)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
client.release,
|
client.options["release"],
|
||||||
RELEASE,
|
RELEASE,
|
||||||
"Failed to use 'sentry_release' parameter appropriately",
|
"Failed to use 'sentry_release' parameter appropriately",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
# 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).
|
||||||
|
|
||||||
import unittest
|
from odoo.tests import TransactionCase
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from ..logutils import SanitizeOdooCookiesProcessor
|
from ..logutils import SanitizeOdooCookiesProcessor
|
||||||
|
|
||||||
|
|
||||||
class TestOdooCookieSanitizer(unittest.TestCase):
|
class TestOdooCookieSanitizer(TransactionCase):
|
||||||
def test_cookie_as_string(self):
|
def test_cookie_as_string(self):
|
||||||
data = {
|
data = {
|
||||||
"request": {
|
"request": {
|
||||||
|
@ -19,7 +17,7 @@ class TestOdooCookieSanitizer(unittest.TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proc = SanitizeOdooCookiesProcessor(mock.Mock())
|
proc = SanitizeOdooCookiesProcessor()
|
||||||
result = proc.process(data)
|
result = proc.process(data)
|
||||||
|
|
||||||
self.assertTrue("request" in result)
|
self.assertTrue("request" in result)
|
||||||
|
@ -35,14 +33,14 @@ class TestOdooCookieSanitizer(unittest.TestCase):
|
||||||
def test_cookie_as_string_with_partials(self):
|
def test_cookie_as_string_with_partials(self):
|
||||||
data = {"request": {"cookies": "website_lang=en_us;session_id;foo=bar"}}
|
data = {"request": {"cookies": "website_lang=en_us;session_id;foo=bar"}}
|
||||||
|
|
||||||
proc = SanitizeOdooCookiesProcessor(mock.Mock())
|
proc = SanitizeOdooCookiesProcessor()
|
||||||
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={m};foo=bar".format(m=proc.MASK),
|
"website_lang=en_us;session_id;foo=bar",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cookie_header(self):
|
def test_cookie_header(self):
|
||||||
|
@ -57,7 +55,7 @@ class TestOdooCookieSanitizer(unittest.TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proc = SanitizeOdooCookiesProcessor(mock.Mock())
|
proc = SanitizeOdooCookiesProcessor()
|
||||||
result = proc.process(data)
|
result = proc.process(data)
|
||||||
|
|
||||||
self.assertTrue("request" in result)
|
self.assertTrue("request" in result)
|
||||||
|
|
Loading…
Reference in New Issue