diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index f9e2462919..2d0d521699 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -37,12 +37,15 @@ "delivery", ], "data": [ + "data/ir_config_parameter_data.xml", + "data/ir_cron_data.xml", "security/ir.model.access.csv", "views/shopfloor_menu.xml", "views/stock_picking_type.xml", "views/stock_location.xml", "views/stock_move_line.xml", "views/shopfloor_profile_views.xml", + "views/shopfloor_log_views.xml", "views/menus.xml", ], "demo": [ diff --git a/shopfloor/data/ir_config_parameter_data.xml b/shopfloor/data/ir_config_parameter_data.xml new file mode 100644 index 0000000000..2127ea5cbe --- /dev/null +++ b/shopfloor/data/ir_config_parameter_data.xml @@ -0,0 +1,7 @@ + + + + shopfloor.log.retention.days + 30 + + diff --git a/shopfloor/data/ir_cron_data.xml b/shopfloor/data/ir_cron_data.xml new file mode 100644 index 0000000000..7c5809584b --- /dev/null +++ b/shopfloor/data/ir_cron_data.xml @@ -0,0 +1,15 @@ + + + + Auto-vacuum Shopfloor Logs + + + + 1 + days + -1 + + code + model.autovacuum() + + diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py index 7f9b986965..1bfd3ece74 100644 --- a/shopfloor/models/__init__.py +++ b/shopfloor/models/__init__.py @@ -1,4 +1,5 @@ from . import shopfloor_menu +from . import shopfloor_log from . import stock_picking_type from . import shopfloor_profile from . import stock_inventory diff --git a/shopfloor/models/shopfloor_log.py b/shopfloor/models/shopfloor_log.py new file mode 100644 index 0000000000..a17e9a1432 --- /dev/null +++ b/shopfloor/models/shopfloor_log.py @@ -0,0 +1,58 @@ +import logging +from datetime import datetime, timedelta + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class ShopfloorLog(models.Model): + _name = "shopfloor.log" + _description = "Shopfloor Logging" + _order = "id desc" + + DEFAULT_RETENTION = 30 # days + + request_url = fields.Char(readonly=True, string="Request URL") + request_method = fields.Char(readonly=True) + params = fields.Text(readonly=True) + headers = fields.Text(readonly=True) + result = fields.Text(readonly=True) + error = fields.Text(readonly=True) + state = fields.Selection( + selection=[("success", "Success"), ("failed", "Failed")], readonly=True, + ) + + def _logs_retention_days(self): + retention = self.DEFAULT_RETENTION + param = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("shopfloor.log.retention.days") + ) + if param: + try: + retention = int(param) + except ValueError: + _logger.exception( + "Could not convert System Parameter" + " 'shopfloor.log.retention.days' to integer," + " reverting to the" + " default configuration." + ) + return retention + + def logging_active(self): + retention = self._logs_retention_days() + return retention > 0 + + def autovacuum(self): + """Delete logs which have exceeded their retention duration + + Called from a cron. + """ + deadline = datetime.now() - timedelta(days=self._logs_retention_days()) + logs = self.search([("create_date", "<=", deadline)]) + if logs: + logs.unlink() + return True diff --git a/shopfloor/readme/CONFIGURE.rst b/shopfloor/readme/CONFIGURE.rst index 8442c179fd..fe0cc8cdee 100644 --- a/shopfloor/readme/CONFIGURE.rst +++ b/shopfloor/readme/CONFIGURE.rst @@ -1 +1,17 @@ writeme + +Logs retention +-------------- + +Logs are kept in database for every REST requests made by a client application. +They can be used for debugging and monitoring of the activity. + +The Logs menu is shown only with Developer tools (``?debug=1``) activated. + +By default, Shopfloor logs are kept 30 days. +You can change the duration of the retention by changing the System Parameter +``shopfloor.log.retention.days``. + +If the value is set to 0, the logs are not stored at all. + +Logged data is: request URL and method, parameters, headers, result or error. diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv index 6aca8c733c..bdf728b8e5 100644 --- a/shopfloor/security/ir.model.access.csv +++ b/shopfloor/security/ir.model.access.csv @@ -1,5 +1,6 @@ "id","name","model_id/id","group_id/id","perm_read","perm_write","perm_create","perm_unlink" -"access_shopfloor_menu_users","shopfloor menu","model_shopfloor_menu",,1,0,0,0 +"access_shopfloor_menu_users","shopfloor menu","model_shopfloor_menu","stock.group_stock_user",1,0,0,0 "access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager",1,1,1,1 -"access_shopfloor_profile_users","shopfloor profile","model_shopfloor_profile",,1,0,0,0 +"access_shopfloor_profile_users","shopfloor profile","model_shopfloor_profile","stock.group_stock_user",1,0,0,0 "access_shopfloor_profile_stock_manager","shopfloor profile inventory manager","model_shopfloor_profile","stock.group_stock_manager",1,1,1,1 +"access_shopfloor_log","access_shopfloor_log","model_shopfloor_log","stock.group_stock_manager",1,0,0,0 diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index 3841079f56..51465f1f16 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -1,5 +1,6 @@ -from odoo import _, exceptions +from odoo import _, exceptions, registry from odoo.exceptions import MissingError +from odoo.http import request from odoo.osv import expression from odoo.addons.base_rest.controllers.main import _PseudoCollection @@ -22,6 +23,63 @@ class BaseShopfloorService(AbstractComponent): _collection = "shopfloor.service" _actions_collection_name = "shopfloor.action" _expose_model = None + # can be overridden to disable logging of requests to DB + _log_calls_in_db = True + + def dispatch(self, method_name, _id=None, params=None): + if not self._db_logging_active(): + return super().dispatch(method_name, _id=_id, params=params) + return self._dispatch_with_db_logging(method_name, _id=_id, params=params) + + def _db_logging_active(self): + return ( + request + and self._log_calls_in_db + and self.env["shopfloor.log"].logging_active() + ) + + # TODO logging to DB should be an extra module for base_rest + def _dispatch_with_db_logging(self, method_name, _id=None, params=None): + try: + result = super().dispatch(method_name, _id=_id, params=params) + except Exception as err: + self.env.cr.rollback() + with registry(self.env.cr.dbname).cursor() as cr: + env = self.env(cr=cr) + self._log_call_in_db(env, request, _id, params, error=err) + raise + self._log_call_in_db(self.env, request, _id, params, result=result) + return result + + @property + def _log_call_header_strip(self): + return ("Cookie", "Api-Key") + + def _log_call_in_db_values(self, _request, _id, params, result=None, error=None): + httprequest = _request.httprequest + headers = dict(httprequest.headers) + for header_key in self._log_call_header_strip: + if header_key in headers: + headers[header_key] = "" + if _id: + params = dict(params, _id=_id) + return { + "request_url": httprequest.url, + "request_method": httprequest.method, + "params": params, + "headers": headers, + "result": result, + "error": error, + "state": "success" if result else "failed", + } + + def _log_call_in_db(self, env, _request, _id, params, result=None, error=None): + values = self._log_call_in_db_values( + _request, _id, params, result=result, error=error + ) + if not values: + return + env["shopfloor.log"].sudo().create(values) def _get(self, _id): domain = expression.normalize_domain(self._get_base_search_domain()) diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml index 29876a3a77..f1a619ed6b 100644 --- a/shopfloor/views/menus.xml +++ b/shopfloor/views/menus.xml @@ -18,4 +18,11 @@ parent="menu_shopfloor_settings" sequence="20" /> + diff --git a/shopfloor/views/shopfloor_log_views.xml b/shopfloor/views/shopfloor_log_views.xml new file mode 100644 index 0000000000..6ebb576f7c --- /dev/null +++ b/shopfloor/views/shopfloor_log_views.xml @@ -0,0 +1,125 @@ + + + + shopfloor.log tree + shopfloor.log + + + + + + + + + + + + shopfloor.log form + shopfloor.log + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + shopfloor.log search + shopfloor.log + + + + + + + + + + + + + + + + + + + Shopfloor Logs + shopfloor.log + ir.actions.act_window + tree,form + +