diff --git a/requirements.txt b/requirements.txt index dfd6b25509f..ceed45a920d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ dataclasses numpy odoorpc openupgradelib +sentry_sdk>=2.0.0 diff --git a/sentry/README.rst b/sentry/README.rst new file mode 100644 index 00000000000..ef8728765ff --- /dev/null +++ b/sentry/README.rst @@ -0,0 +1,196 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +====== +Sentry +====== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:265ff8f1f7eb94e79f3c3d0962dacb5756960add99c42fc0be14eff28b57db14 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/19.0/sentry + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-19-0/server-tools-19-0-sentry + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows painless `Sentry `__ integration +with Odoo. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +The module can be installed just like any other Odoo module, by adding +the module's directory to Odoo *addons_path*. In order for the module to +correctly wrap the Odoo WSGI application, it also needs to be loaded as +a server-wide module. This can be done with the ``server_wide_modules`` +parameter in your Odoo config file or with the ``--load`` command-line +parameter. + +This module additionally requires the sentry-sdk Python package to be +available on the system. It can be installed using pip: + +:: + + pip install sentry-sdk + +Configuration +============= + +The following additional configuration options can be added to your Odoo +configuration file: + +[TABLE] + +Other `client +arguments `__ +can be configured by prepending the argument name with *sentry\_* in +your Odoo config file. Currently supported additional client arguments +are: +``include_local_variables, max_breadcrumbs, release, environment, server_name, shutdown_timeout, in_app_include, in_app_exclude, default_integrations, dist, sample_rate, send_default_pii, http_proxy, https_proxy, max_request_body_size, max_value_length, attach_stacktrace, ca_certs, propagate_traces, traces_sample_rate, auto_enabling_integrations``. + +Example Odoo configuration +-------------------------- + +Below is an example of Odoo configuration file with *Odoo Sentry* +options: + +:: + + [options] + sentry_dsn = https://@sentry.example.com/ + sentry_enabled = true + sentry_logging_level = warn + sentry_exclude_loggers = werkzeug + sentry_ignore_exceptions = odoo.exceptions.AccessDenied, + odoo.exceptions.AccessError,odoo.exceptions.MissingError, + odoo.exceptions.RedirectWarning,odoo.exceptions.UserError, + odoo.exceptions.ValidationError,odoo.exceptions.Warning, + odoo.exceptions.except_orm + sentry_include_context = true + sentry_environment = production + sentry_release = 1.3.2 + sentry_odoo_dir = /home/odoo/odoo/ + +Usage +===== + +Once configured and installed, the module will report any logging event +at and above the configured Sentry logging level, no additional actions +are necessary. + +Known issues / Roadmap +====================== + +- **No database separation** -- This module functions by intercepting + all Odoo logging records in a running Odoo process. This means that + once installed in one database, it will intercept and report errors + for all Odoo databases, which are used on that Odoo server. +- **Frontend integration** -- In the future, it would be nice to add + Odoo client-side error reporting to this module as well, by + integrating `raven-js `__. + Additionally, `Sentry user feedback + form `__ could be + integrated into the Odoo client error dialog window to allow users + shortly describe what they were doing when things went wrong. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Mohammed Barsi +* Versada +* Nicolas JEUDY +* Vauxoo + +Contributors +------------ + +- Mohammed Barsi barsi@mba.pe +- Andrius Preimantas andrius@versada.eu +- Naglis Jonaitis naglis@versada.eu +- Atte Isopuro atte.isopuro@avoin.systems +- Florian Mounier florian.mounier@kozea.fr +- Jon Ashton j@jonashton.com +- Mark Schuit mark@aprima.nl +- Atchuthan atchuthan.shanmugasundaram@akretion.com + +Other credits +------------- + +Other credits +~~~~~~~~~~~~~ + +- Vauxoo + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-barsi| image:: https://github.com/barsi.png?size=40px + :target: https://github.com/barsi + :alt: barsi +.. |maintainer-naglis| image:: https://github.com/naglis.png?size=40px + :target: https://github.com/naglis + :alt: naglis +.. |maintainer-versada| image:: https://github.com/versada.png?size=40px + :target: https://github.com/versada + :alt: versada +.. |maintainer-moylop260| image:: https://github.com/moylop260.png?size=40px + :target: https://github.com/moylop260 + :alt: moylop260 +.. |maintainer-fernandahf| image:: https://github.com/fernandahf.png?size=40px + :target: https://github.com/fernandahf + :alt: fernandahf + +Current `maintainers `__: + +|maintainer-barsi| |maintainer-naglis| |maintainer-versada| |maintainer-moylop260| |maintainer-fernandahf| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sentry/__init__.py b/sentry/__init__.py new file mode 100644 index 00000000000..7001103db4d --- /dev/null +++ b/sentry/__init__.py @@ -0,0 +1 @@ +from .hooks import post_load diff --git a/sentry/__manifest__.py b/sentry/__manifest__.py new file mode 100644 index 00000000000..9f86bf754ca --- /dev/null +++ b/sentry/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Sentry", + "summary": "Report Odoo errors to Sentry", + "version": "19.0.1.0.0", + "development_status": "Beta", + "category": "Extra Tools", + "website": "https://github.com/OCA/server-tools", + "author": "Mohammed Barsi," + "Versada," + "Nicolas JEUDY," + "Odoo Community Association (OCA)," + "Vauxoo", + "maintainers": ["barsi", "naglis", "versada", "moylop260", "fernandahf"], + "license": "AGPL-3", + "application": False, + "installable": True, + "external_dependencies": { + "python": [ + "sentry_sdk>=2.0.0", + ] + }, + "depends": [ + "base", + ], + "post_load": "post_load", +} diff --git a/sentry/const.py b/sentry/const.py new file mode 100644 index 00000000000..676835bedec --- /dev/null +++ b/sentry/const.py @@ -0,0 +1,153 @@ +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import collections +import logging + +from sentry_sdk.consts import DEFAULT_OPTIONS +from sentry_sdk.integrations.logging import LoggingIntegration + +import odoo.loglevels + + +def split_multiple(string, delimiter=",", strip_chars=None): + """Splits :param:`string` and strips :param:`strip_chars` from values.""" + if not string: + return [] + return [v.strip(strip_chars) for v in string.split(delimiter)] + + +def to_int_if_defined(value): + if value == "" or value is None: + return + return int(value) + + +def to_float_if_defined(value): + if value == "" or value is None: + return + return float(value) + + +SentryOption = collections.namedtuple("SentryOption", ["key", "default", "converter"]) + +# Mapping of Odoo logging level -> Python stdlib logging library log level. +LOG_LEVEL_MAP = { + getattr(odoo.loglevels, f"LOG_{x}"): getattr(logging, x) + for x in ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET") +} +DEFAULT_LOG_LEVEL = "warn" + +ODOO_USER_EXCEPTIONS = [ + "odoo.exceptions.AccessDenied", + "odoo.exceptions.AccessError", + "odoo.exceptions.DeferredException", + "odoo.exceptions.MissingError", + "odoo.exceptions.RedirectWarning", + "odoo.exceptions.UserError", + "odoo.exceptions.ValidationError", + "odoo.exceptions.Warning", + "odoo.exceptions.except_orm", +] +DEFAULT_IGNORED_EXCEPTIONS = ",".join(ODOO_USER_EXCEPTIONS) + +EXCLUDE_LOGGERS = ("werkzeug",) +DEFAULT_EXCLUDE_LOGGERS = ",".join(EXCLUDE_LOGGERS) + +DEFAULT_ENVIRONMENT = "develop" + + +def get_sentry_logging(level=DEFAULT_LOG_LEVEL): + if level not in LOG_LEVEL_MAP: + level = DEFAULT_LOG_LEVEL + + return LoggingIntegration( + # Gather warnings into breadcrumbs regardless of actual logging level + level=logging.WARNING, + event_level=LOG_LEVEL_MAP[level], + ) + + +def _get_default(key, fallback=None): + """Retrieves a default value from sentry_sdk DEFAULT_OPTIONS defensively. + + The keys available in DEFAULT_OPTIONS may change between sentry_sdk versions, + so a fallback is provided for each option. + """ + return DEFAULT_OPTIONS.get(key, fallback) + + +def get_sentry_options(): + res = [ + SentryOption("dsn", "", str.strip), + SentryOption("logging_level", DEFAULT_LOG_LEVEL, get_sentry_logging), + SentryOption( + "include_local_variables", + _get_default("include_local_variables", False), + None, + ), + SentryOption( + "max_breadcrumbs", + _get_default("max_breadcrumbs", 100), + to_int_if_defined, + ), + SentryOption("release", _get_default("release"), None), + SentryOption("environment", _get_default("environment"), None), + SentryOption("server_name", _get_default("server_name"), None), + SentryOption("shutdown_timeout", _get_default("shutdown_timeout", 2), None), + SentryOption( + "in_app_include", + _get_default("in_app_include", []), + split_multiple, + ), + SentryOption( + "in_app_exclude", + _get_default("in_app_exclude", []), + split_multiple, + ), + SentryOption( + "default_integrations", + _get_default("default_integrations", True), + None, + ), + SentryOption("dist", _get_default("dist"), None), + SentryOption( + "sample_rate", + _get_default("sample_rate", 1.0), + to_float_if_defined, + ), + SentryOption("send_default_pii", _get_default("send_default_pii", False), None), + SentryOption("http_proxy", _get_default("http_proxy"), None), + SentryOption("https_proxy", _get_default("https_proxy"), None), + SentryOption("ignore_exceptions", DEFAULT_IGNORED_EXCEPTIONS, split_multiple), + SentryOption( + "max_request_body_size", + _get_default("max_request_body_size", "medium"), + None, + ), + SentryOption( + "max_value_length", + _get_default("max_value_length", 1024), + to_int_if_defined, + ), + SentryOption( + "attach_stacktrace", _get_default("attach_stacktrace", False), None + ), + SentryOption("ca_certs", _get_default("ca_certs"), None), + SentryOption("propagate_traces", _get_default("propagate_traces", True), None), + SentryOption( + "traces_sample_rate", + _get_default("traces_sample_rate"), + to_float_if_defined, + ), + ] + + if "auto_enabling_integrations" in DEFAULT_OPTIONS: + res.append( + SentryOption( + "auto_enabling_integrations", + DEFAULT_OPTIONS["auto_enabling_integrations"], + None, + ) + ) + + return res diff --git a/sentry/generalutils.py b/sentry/generalutils.py new file mode 100644 index 00000000000..e13d13cc899 --- /dev/null +++ b/sentry/generalutils.py @@ -0,0 +1,62 @@ +try: + from collections.abc import Mapping +except ImportError: # pragma: no cover + # Python < 3.3 + from collections.abc 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(isinstance(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] diff --git a/sentry/hooks.py b/sentry/hooks.py new file mode 100644 index 00000000000..8774c61bf20 --- /dev/null +++ b/sentry/hooks.py @@ -0,0 +1,156 @@ +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import warnings +from collections import abc + +import odoo.http +from odoo.service.server import 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): + """Prevent the capture of any exceptions in + the DEFAULT_IGNORED_EXCEPTIONS list + -- or -- + Add context to event if include_context is True + and sanitize sensitive data""" + + exc_info = hint.get("exc_info") + if exc_info is None and "log_record" in hint: + # Odoo handles UserErrors by logging the raw exception rather + # than a message string in odoo/http.py + try: + module_name = hint["log_record"].msg.__module__ + class_name = hint["log_record"].msg.__class__.__name__ + qualified_name = module_name + "." + class_name + except AttributeError: + qualified_name = "not found" + + if qualified_name in const.DEFAULT_IGNORED_EXCEPTIONS: + return None + + if event.setdefault("tags", {}).get("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" + ) + if config.get("sentry_transport"): + warnings.warn( + "`sentry_transport` has been deprecated. " + "Its not neccesary send it, will use `HttpTranport` by default.", + DeprecationWarning, + stacklevel=1, + ) + options = {} + for option in const.get_sentry_options(): + value = config.get(f"sentry_{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 + + # sentry_sdk >= 2.0: ThreadingIntegration no longer accepts propagate_hub + options["integrations"] = [ + options["logging_level"], + ThreadingIntegration(), + ] + # 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) + + # The server app is already registered so patch it here + if server: + server.app = SentryWsgiMiddleware(server.app) + + # Patch the wsgi server in case of further registration + odoo.http.Application = SentryWsgiMiddleware(odoo.http.Application) + + # sentry_sdk >= 2.0: new_scope() replaces the deprecated push_scope() + with sentry_sdk.new_scope() as scope: + scope.set_extra("debug", False) + sentry_sdk.capture_message("Starting Odoo Server", "info") + + return client + + +def post_load(): + initialize_sentry(odoo_config) diff --git a/sentry/i18n/it.po b/sentry/i18n/it.po new file mode 100644 index 00000000000..73388557f6d --- /dev/null +++ b/sentry/i18n/it.po @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" diff --git a/sentry/i18n/sentry.pot b/sentry/i18n/sentry.pot new file mode 100644 index 00000000000..aadee09bfed --- /dev/null +++ b/sentry/i18n/sentry.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/sentry/logutils.py b/sentry/logutils.py new file mode 100644 index 00000000000..b0b0958f3f1 --- /dev/null +++ b/sentry/logutils.py @@ -0,0 +1,117 @@ +# Copyright 2016-2017 Versada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import os.path +import urllib.parse + +from werkzeug import datastructures + +from .generalutils import get_environ +from .processor import SanitizePasswordsProcessor + + +def get_request_info(request): + """ + Returns context data extracted from :param:`request`. + + Heavily based on flask integration for Sentry: https://git.io/vP4i9. + """ + urlparts = urllib.parse.urlsplit(request.url) + return { + "url": f"{urlparts.scheme}://{urlparts.netloc}{urlparts.path}", + "query_string": urlparts.query, + "method": request.method, + "headers": dict(datastructures.EnvironHeaders(request.environ)), + "env": dict(get_environ(request.environ)), + } + + +def get_extra_context(request): + """ + Extracts additional context from the current request (if such is set). + """ + try: + session = getattr(request, "session", {}) + except RuntimeError: + ctx = {} + else: + ctx = { + "tags": { + "database": session.get("db", None), + }, + "user": { + "email": session.get("login", None), + "id": session.get("uid", None), + }, + "extra": { + "context": session.get("context", {}), + }, + } + if request.httprequest: + ctx.update({"request": get_request_info(request.httprequest)}) + return ctx + + +class SanitizeOdooCookiesProcessor(SanitizePasswordsProcessor): + """Custom :class:`raven.processors.Processor`. + Allows to sanitize sensitive Odoo cookies, namely the "session_id" cookie. + """ + + 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( + f"Cannot identify HEAD for git repository at {path}" + ) + + with open(head_path) as fp: + head = str(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( + f"{path} does not seem to be the root of a git repository" + ) + + # 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 str(revision) + + raise InvalidGitRepository(f"Unable to find ref to head {head} in repository") + + with open(revision_file) as fh: + return str(fh.read()).strip() diff --git a/sentry/processor.py b/sentry/processor.py new file mode 100644 index 00000000000..e8298f7b22f --- /dev/null +++ b/sentry/processor.py @@ -0,0 +1,134 @@ +"""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. +""" + +import re + +from .generalutils import string_types, varmap + + +class SanitizeKeysProcessor: + """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 = str(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().sanitize(item, value) + if isinstance(value, string_types()) and self.VALUES_RE.match(value): + return self.MASK + return value diff --git a/sentry/pyproject.toml b/sentry/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/sentry/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sentry/readme/CONFIGURE.md b/sentry/readme/CONFIGURE.md new file mode 100644 index 00000000000..37484ab34f0 --- /dev/null +++ b/sentry/readme/CONFIGURE.md @@ -0,0 +1,30 @@ +The following additional configuration options can be added to your Odoo +configuration file: + +[TABLE] + +Other [client +arguments](https://docs.sentry.io/platforms/python/configuration/) can +be configured by prepending the argument name with *sentry\_* in your +Odoo config file. Currently supported additional client arguments are: +`include_local_variables, max_breadcrumbs, release, environment, server_name, shutdown_timeout, in_app_include, in_app_exclude, default_integrations, dist, sample_rate, send_default_pii, http_proxy, https_proxy, max_request_body_size, max_value_length, attach_stacktrace, ca_certs, propagate_traces, traces_sample_rate, auto_enabling_integrations`. + +## Example Odoo configuration + +Below is an example of Odoo configuration file with *Odoo Sentry* +options: + + [options] + sentry_dsn = https://@sentry.example.com/ + sentry_enabled = true + sentry_logging_level = warn + sentry_exclude_loggers = werkzeug + sentry_ignore_exceptions = odoo.exceptions.AccessDenied, + odoo.exceptions.AccessError,odoo.exceptions.MissingError, + odoo.exceptions.RedirectWarning,odoo.exceptions.UserError, + odoo.exceptions.ValidationError,odoo.exceptions.Warning, + odoo.exceptions.except_orm + sentry_include_context = true + sentry_environment = production + sentry_release = 1.3.2 + sentry_odoo_dir = /home/odoo/odoo/ diff --git a/sentry/readme/CONTRIBUTORS.md b/sentry/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..8781987734c --- /dev/null +++ b/sentry/readme/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +- Mohammed Barsi +- Andrius Preimantas +- Naglis Jonaitis +- Atte Isopuro +- Florian Mounier +- Jon Ashton +- Mark Schuit +- Atchuthan diff --git a/sentry/readme/CREDITS.md b/sentry/readme/CREDITS.md new file mode 100644 index 00000000000..6a3a31dd4c3 --- /dev/null +++ b/sentry/readme/CREDITS.md @@ -0,0 +1,3 @@ +## Other credits + +- Vauxoo diff --git a/sentry/readme/DESCRIPTION.md b/sentry/readme/DESCRIPTION.md new file mode 100644 index 00000000000..f8813c39a69 --- /dev/null +++ b/sentry/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allows painless [Sentry](https://sentry.io/) integration +with Odoo. diff --git a/sentry/readme/INSTALL.md b/sentry/readme/INSTALL.md new file mode 100644 index 00000000000..8561c1034d0 --- /dev/null +++ b/sentry/readme/INSTALL.md @@ -0,0 +1,11 @@ +The module can be installed just like any other Odoo module, by adding +the module's directory to Odoo *addons_path*. In order for the module to +correctly wrap the Odoo WSGI application, it also needs to be loaded as +a server-wide module. This can be done with the `server_wide_modules` +parameter in your Odoo config file or with the `--load` command-line +parameter. + +This module additionally requires the sentry-sdk Python package to be +available on the system. It can be installed using pip: + + pip install sentry-sdk diff --git a/sentry/readme/ROADMAP.md b/sentry/readme/ROADMAP.md new file mode 100644 index 00000000000..4b9ee37f7ec --- /dev/null +++ b/sentry/readme/ROADMAP.md @@ -0,0 +1,11 @@ +- **No database separation** -- This module functions by intercepting + all Odoo logging records in a running Odoo process. This means that + once installed in one database, it will intercept and report errors + for all Odoo databases, which are used on that Odoo server. +- **Frontend integration** -- In the future, it would be nice to add + Odoo client-side error reporting to this module as well, by + integrating [raven-js](https://github.com/getsentry/raven-js). + Additionally, [Sentry user feedback + form](https://docs.sentry.io/learn/user-feedback/) could be integrated + into the Odoo client error dialog window to allow users shortly + describe what they were doing when things went wrong. diff --git a/sentry/readme/USAGE.md b/sentry/readme/USAGE.md new file mode 100644 index 00000000000..bba13c4ab82 --- /dev/null +++ b/sentry/readme/USAGE.md @@ -0,0 +1,3 @@ +Once configured and installed, the module will report any logging event +at and above the configured Sentry logging level, no additional actions +are necessary. diff --git a/sentry/static/description/index.html b/sentry/static/description/index.html new file mode 100644 index 00000000000..e6fa544ca20 --- /dev/null +++ b/sentry/static/description/index.html @@ -0,0 +1,531 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Sentry

+ +

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

+

This module allows painless Sentry integration +with Odoo.

+

Table of contents

+ +
+

Installation

+

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

+

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

+
+pip install sentry-sdk
+
+
+
+

Configuration

+

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

+

[TABLE]

+

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

+
+

Example Odoo configuration

+

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

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

Usage

+

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

+
+
+

Known issues / Roadmap

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

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

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

Contributors

+ +
+
+

Other credits

+
+

Other credits

+
    +
  • Vauxoo
  • +
+
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainers:

+

barsi naglis versada moylop260 fernandahf

+

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

+

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

+
+
+
+
+ +