mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 16:12:08 +02:00
225 lines
8.8 KiB
Python
225 lines
8.8 KiB
Python
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
|
|
# @author Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
|
# @author Simone Orsi <simahawk@gmail.com>
|
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
|
|
|
import json
|
|
import logging
|
|
import traceback
|
|
|
|
from psycopg2.errors import OperationalError
|
|
from werkzeug.urls import url_encode, url_join
|
|
|
|
from odoo import exceptions, registry
|
|
from odoo.http import Response, request
|
|
from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
|
|
|
|
from odoo.addons.base_rest.http import JSONEncoder
|
|
from odoo.addons.component.core import AbstractComponent
|
|
|
|
from ..exceptions import (
|
|
RESTServiceDispatchException,
|
|
RESTServiceMissingErrorException,
|
|
RESTServiceUserErrorException,
|
|
RESTServiceValidationErrorException,
|
|
)
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def json_dump(data):
|
|
"""Encode data to JSON as we like."""
|
|
return json.dumps(data, cls=JSONEncoder, indent=4, sort_keys=True, default=str)
|
|
|
|
|
|
class BaseRESTService(AbstractComponent):
|
|
_inherit = "base.rest.service"
|
|
# can be overridden to enable logging of requests to DB
|
|
_log_calls_in_db = False
|
|
|
|
def dispatch(self, method_name, *args, params=None):
|
|
if not self._db_logging_active(method_name):
|
|
return super().dispatch(method_name, *args, params=params)
|
|
return self._dispatch_with_db_logging(method_name, *args, params=params)
|
|
|
|
def _dispatch_with_db_logging(self, method_name, *args, params=None):
|
|
try:
|
|
with self.env.cr.savepoint():
|
|
result = super().dispatch(method_name, *args, params=params)
|
|
except exceptions.MissingError as orig_exception:
|
|
self._dispatch_exception(
|
|
method_name,
|
|
RESTServiceMissingErrorException,
|
|
orig_exception,
|
|
*args,
|
|
params=params,
|
|
)
|
|
except exceptions.ValidationError as orig_exception:
|
|
self._dispatch_exception(
|
|
method_name,
|
|
RESTServiceValidationErrorException,
|
|
orig_exception,
|
|
*args,
|
|
params=params,
|
|
)
|
|
except exceptions.UserError as orig_exception:
|
|
self._dispatch_exception(
|
|
method_name,
|
|
RESTServiceUserErrorException,
|
|
orig_exception,
|
|
*args,
|
|
params=params,
|
|
)
|
|
except Exception as orig_exception:
|
|
self._dispatch_exception(
|
|
method_name,
|
|
RESTServiceDispatchException,
|
|
orig_exception,
|
|
*args,
|
|
params=params,
|
|
)
|
|
self._log_dispatch_success(method_name, result, *args, params)
|
|
return result
|
|
|
|
def _log_dispatch_success(self, method_name, result, *args, params=None):
|
|
try:
|
|
with self.env.cr.savepoint():
|
|
log_entry = self._log_call_in_db(
|
|
self.env, request, method_name, *args, params, result=result
|
|
)
|
|
if log_entry and not isinstance(result, Response):
|
|
log_entry_url = self._get_log_entry_url(log_entry)
|
|
result["log_entry_url"] = log_entry_url
|
|
except Exception as e:
|
|
_logger.exception("Rest Log Error Creation: %s", e)
|
|
|
|
def _dispatch_exception(
|
|
self, method_name, exception_klass, orig_exception, *args, params=None
|
|
):
|
|
exc_msg, log_entry_url = None, None # in case it fails below
|
|
try:
|
|
exc_msg = self._get_exception_message(orig_exception)
|
|
tb = traceback.format_exc()
|
|
with registry(self.env.cr.dbname).cursor() as cr:
|
|
log_entry = self._log_call_in_db(
|
|
self.env(cr=cr),
|
|
request,
|
|
method_name,
|
|
*args,
|
|
params=params,
|
|
traceback=tb,
|
|
orig_exception=orig_exception,
|
|
)
|
|
log_entry_url = self._get_log_entry_url(log_entry)
|
|
except Exception as e:
|
|
_logger.exception("Rest Log Error Creation: %s", e)
|
|
# let the OperationalError bubble up to the retrying mechanism
|
|
# We can't wrap the OperationalError because we want to let it
|
|
# bubble up to the retrying mechanism, it will be handled by
|
|
# the default handler at the end of the chain.
|
|
if (
|
|
isinstance(orig_exception, OperationalError)
|
|
and orig_exception.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY
|
|
):
|
|
raise orig_exception
|
|
raise exception_klass(exc_msg, log_entry_url) from orig_exception
|
|
|
|
def _get_exception_message(self, exception):
|
|
return exception.args and exception.args[0] or str(exception)
|
|
|
|
def _get_log_entry_url(self, entry):
|
|
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
|
url_params = {
|
|
"action": self.env.ref("rest_log.action_rest_log").id,
|
|
"view_type": "form",
|
|
"model": entry._name,
|
|
"id": entry.id,
|
|
}
|
|
url = "/web?#%s" % url_encode(url_params)
|
|
return url_join(base_url, url)
|
|
|
|
@property
|
|
def _log_call_header_strip(self):
|
|
return ("Cookie", "Api-Key")
|
|
|
|
def _log_call_in_db_values(self, _request, *args, params=None, **kw):
|
|
httprequest = _request.httprequest
|
|
headers = self._log_call_sanitize_headers(dict(httprequest.headers or []))
|
|
params = dict(params or {})
|
|
if args:
|
|
params.update(args=args)
|
|
params = self._log_call_sanitize_params(params)
|
|
error, exception_name, exception_message = self._log_call_prepare_error(**kw)
|
|
result, state = self._log_call_prepare_result(kw.get("result"))
|
|
collection = self.work.collection
|
|
return {
|
|
"collection": collection._name,
|
|
"collection_id": collection.id,
|
|
"request_url": httprequest.url,
|
|
"request_method": httprequest.method,
|
|
"params": params,
|
|
"headers": headers,
|
|
"result": result,
|
|
"error": error,
|
|
"exception_name": exception_name,
|
|
"exception_message": exception_message,
|
|
"state": state,
|
|
}
|
|
|
|
def _log_call_prepare_result(self, result):
|
|
# NB: ``result`` might be an object of class ``odoo.http.Response``,
|
|
# for example when you try to download a file. In this case, we need to
|
|
# handle it properly, without the assumption that ``result`` is a dict.
|
|
if isinstance(result, Response):
|
|
status_code = result.status_code
|
|
result = {
|
|
"status": status_code,
|
|
"headers": self._log_call_sanitize_headers(dict(result.headers or [])),
|
|
}
|
|
state = "success" if status_code in range(200, 300) else "failed"
|
|
else:
|
|
state = "success" if result else "failed"
|
|
return result, state
|
|
|
|
def _log_call_prepare_error(self, traceback=None, orig_exception=None, **kw):
|
|
exception_name = None
|
|
exception_message = None
|
|
if orig_exception:
|
|
exception_name = orig_exception.__class__.__name__
|
|
if hasattr(orig_exception, "__module__"):
|
|
exception_name = orig_exception.__module__ + "." + exception_name
|
|
exception_message = self._get_exception_message(orig_exception)
|
|
return traceback, exception_name, exception_message
|
|
|
|
_log_call_in_db_keys_to_serialize = ("params", "headers", "result")
|
|
|
|
def _log_call_in_db(self, env, _request, method_name, *args, params=None, **kw):
|
|
values = self._log_call_in_db_values(_request, *args, params=params, **kw)
|
|
for k in self._log_call_in_db_keys_to_serialize:
|
|
values[k] = json_dump(values[k])
|
|
enabled_states = self._get_matching_active_conf(method_name)
|
|
if not values or enabled_states and values["state"] not in enabled_states:
|
|
return
|
|
return env["rest.log"].sudo().create(values)
|
|
|
|
def _log_call_sanitize_params(self, params: dict) -> dict:
|
|
if "password" in params:
|
|
params["password"] = "<redacted>"
|
|
return params
|
|
|
|
def _log_call_sanitize_headers(self, headers: dict) -> dict:
|
|
for header_key in self._log_call_header_strip:
|
|
if header_key in headers:
|
|
headers[header_key] = "<redacted>"
|
|
return headers
|
|
|
|
def _db_logging_active(self, method_name):
|
|
enabled = self._log_calls_in_db
|
|
if not enabled:
|
|
enabled = bool(self._get_matching_active_conf(method_name))
|
|
return request and enabled and self.env["rest.log"].logging_active()
|
|
|
|
def _get_matching_active_conf(self, method_name):
|
|
return self.env["rest.log"]._get_matching_active_conf(
|
|
self._collection, self._usage, method_name
|
|
)
|