Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,46 @@
# REST Log
Odoo addon: rest_log
## Installation
```bash
pip install odoo-bringout-oca-rest-framework-rest_log
```
## Dependencies
This addon depends on:
- base_rest
## Manifest Information
- **Name**: REST Log
- **Version**: 16.0.1.0.3
- **Category**: N/A
- **License**: LGPL-3
- **Installable**: False
## Source
Based on [OCA/rest-framework](https://github.com/OCA/rest-framework) branch 16.0, addon `rest_log`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Rest_log Module - rest_log
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for rest_log. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [base_rest](../../odoo-bringout-oca-rest-framework-base_rest)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon rest_log or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-rest-framework-rest_log"
# or
uv pip install odoo-bringout-oca-rest-framework-rest_log"
```

View file

@ -0,0 +1,12 @@
# Models
Detected core models and extensions in rest_log.
```mermaid
classDiagram
class rest_log
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: rest_log. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon rest_log
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,42 @@
# Security
Access control and security definitions in rest_log.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../rest_log/security/ir.model.access.csv)**
- 1 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[groups.xml](../rest_log/security/groups.xml)**
- 1 security groups defined
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[groups.xml](../rest_log/security/groups.xml)**
- Security groups, categories, and XML-based rules
- **[ir.model.access.csv](../rest_log/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon rest_log
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,42 @@
[project]
name = "odoo-bringout-oca-rest-framework-rest_log"
version = "16.0.0"
description = "REST Log - Track REST API calls into DB"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-rest-framework-base_rest>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["rest_log"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,148 @@
========
REST Log
========
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ed7eb7cec756c78c39eb3dc04ca382ea64926defada6d97aa72e859db389d8b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
:target: https://github.com/OCA/rest-framework/tree/16.0/rest_log
:alt: OCA/rest-framework
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-rest_log
: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/rest-framework&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
When exposing REST services is often useful to see what's happening
especially in case of errors.
This module add DB logging for REST requests.
It also inject in the response the URL of the log entry created.
NOTE: this feature was implemented initially inside shopfloor app.
Up to version 13.0.1.2.1 of this module,
if shopfloor is installed, log records will be copied from its table.
**Table of contents**
.. contents::
:local:
Configuration
=============
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, REST logs are kept 30 days.
You can change the duration of the retention by changing the System Parameter
``rest.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.
Logs activation
~~~~~~~~~~~~~~~
You have 2 ways to activate logging:
* on the service component set `_log_calls_in_db = True`
* via configuration
In the 1st case, calls will be always be logged.
In the 2nd case you can set ``rest.log.active`` param as::
`collection_name` # enable for all endpoints of the collection
`collection_name.usage` # enable for specific endpoints
`collection_name.usage.endpoint` # enable for specific endpoints
`collection_name*:state` # enable only for specific state (success, failed)
Changelog
=========
13.0.1.0.0
~~~~~~~~~~
First official version.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/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 <https://github.com/OCA/rest-framework/issues/new?body=module:%20rest_log%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
* ACSONE
Contributors
~~~~~~~~~~~~
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Simone Orsi <simahawk@gmail.com>
Other credits
~~~~~~~~~~~~~
**Financial support**
* Cosanum
* Camptocamp R&D
* ACSONE R&D
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-simahawk| image:: https://github.com/simahawk.png?size=40px
:target: https://github.com/simahawk
:alt: simahawk
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-simahawk|
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/rest_log>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,2 @@
from . import models
from . import components

View file

@ -0,0 +1,23 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Copyright 2021 ACSONE SA/NV (http://www.acsone.eu)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "REST Log",
"summary": "Track REST API calls into DB",
"version": "16.0.1.0.3",
"development_status": "Beta",
"website": "https://github.com/OCA/rest-framework",
"author": "Camptocamp, ACSONE, Odoo Community Association (OCA)",
"maintainers": ["simahawk"],
"license": "LGPL-3",
"depends": ["base_rest"],
"data": [
"data/ir_config_parameter_data.xml",
"data/ir_cron_data.xml",
"security/groups.xml",
"security/ir.model.access.csv",
"views/rest_log_views.xml",
"views/menu.xml",
],
}

View file

@ -0,0 +1 @@
from . import service

View file

@ -0,0 +1,225 @@
# 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
)

View file

@ -0,0 +1,11 @@
<?xml version='1.0' encoding='utf-8' ?>
<odoo noupdate="1">
<record id="config_rest_log_retention_days" model="ir.config_parameter">
<field name="key">rest.log.retention.days</field>
<field name="value">30</field>
</record>
<record id="config_rest_log_enabled" model="ir.config_parameter">
<field name="key">rest.log.active</field>
<field name="value" />
</record>
</odoo>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="ir_cron_autovacuum_rest_log" model="ir.cron">
<field name="name">Auto-vacuum REST Logs</field>
<field ref="model_rest_log" name="model_id" />
<field eval="True" name="active" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
<field name="state">code</field>
<field name="code">model.autovacuum()</field>
</record>
</odoo>

View file

@ -0,0 +1,31 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# @author Simone Orsi <simahawk@gmail.com>
from odoo import exceptions as odoo_exceptions
class RESTServiceDispatchException(Exception):
rest_json_info = {}
def __init__(self, message, log_entry_url):
super().__init__(message)
self.rest_json_info = {"log_entry_url": log_entry_url}
class RESTServiceMissingErrorException(
RESTServiceDispatchException, odoo_exceptions.MissingError
):
"""Missing error wrapped exception."""
class RESTServiceUserErrorException(
RESTServiceDispatchException, odoo_exceptions.UserError
):
"""User error wrapped exception."""
class RESTServiceValidationErrorException(
RESTServiceDispatchException, odoo_exceptions.ValidationError
):
"""Validation error wrapped exception."""

View file

@ -0,0 +1,53 @@
# Copyright 2021 Camptocamp SA (http://www.camptocamp.com)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging
_logger = logging.getLogger(__name__)
def post_init_hook(cr, version):
"""Preserve log entries from old implementation in shopfloor."""
cr.execute("SELECT 1 FROM pg_class WHERE relname = 'shopfloor_log'")
if not cr.fetchone():
# shopfloor_log was already removed
return
_logger.info("Copy shopfloor.log records to rest.log")
cr.execute(
"""
INSERT INTO rest_log (
request_url,
request_method,
params,
headers,
result,
error,
exception_name,
exception_message,
state,
severity,
create_uid,
create_date,
write_uid,
write_date
)
SELECT
request_url,
request_method,
params,
headers,
result,
error,
exception_name,
exception_message,
state,
severity,
create_uid,
create_date,
write_uid,
write_date
FROM shopfloor_log;
"""
)
_logger.info("Delete legacy records in shopfloor_log")
cr.execute("""DELETE FROM shopfloor_log""")

View file

@ -0,0 +1,227 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * rest_log
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.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"
#. module: rest_log
#: model:ir.actions.server,name:rest_log.ir_cron_autovacuum_rest_log_ir_actions_server
#: model:ir.cron,cron_name:rest_log.ir_cron_autovacuum_rest_log
msgid "Auto-vacuum REST Logs"
msgstr "Automatsko čišćenje REST dnevnika"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__collection
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Collection"
msgstr "Kolekcija"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__collection_id
msgid "Collection ID"
msgstr "ID kolekcije"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__create_uid
msgid "Created by"
msgstr "Kreirao"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__create_date
msgid "Created on"
msgstr "Kreirano"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Date"
msgstr "Datum"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__display_name
msgid "Display Name"
msgstr "Prikazani naziv"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__error
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Error"
msgstr "Greška"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__exception_name
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Exception"
msgstr "Opis"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__exception_message
msgid "Exception Message"
msgstr "Poruka o iznimci"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Exception message"
msgstr "Poruka o iznimci"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__state__failed
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Failed"
msgstr "Neuspješan"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__functional
msgid "Functional"
msgstr "Funkcionalna"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Functional errors"
msgstr "Funkcionalne greške"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Group By"
msgstr "Grupiši po"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__headers
msgid "Headers"
msgstr "Zaglavlja"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__id
msgid "ID"
msgstr "ID"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log____last_update
msgid "Last Modified on"
msgstr "Zadnje mijenjano"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__write_uid
msgid "Last Updated by"
msgstr "Zadnji ažurirao"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__write_date
msgid "Last Updated on"
msgstr "Zadnje ažurirano"
#. module: rest_log
#: model:ir.ui.menu,name:rest_log.menu_rest_api_log
msgid "Logs"
msgstr "Dnevnici"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Logs generated today"
msgstr "Dnevnici generirani danas"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Parameters"
msgstr "Parametri"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__params
msgid "Params"
msgstr "Parametri"
#. module: rest_log
#: model:ir.model,name:rest_log.model_rest_log
msgid "REST API Logging"
msgstr "REST API evidentiranje"
#. module: rest_log
#: model:res.groups,name:rest_log.group_rest_log_manager
msgid "REST Log Manager"
msgstr "Upravljač REST dnevnika"
#. module: rest_log
#: model:ir.actions.act_window,name:rest_log.action_rest_log
msgid "REST Logs"
msgstr "REST dnevnici"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__request_method
msgid "Request Method"
msgstr "Metoda zahtjeva"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__request_url
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Request URL"
msgstr "URL zahtjeva"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__result
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Result"
msgstr "Rezultat"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__severe
msgid "Severe"
msgstr "Ozbiljna"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Severe errors"
msgstr "Ozbiljne greške"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__severity
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Severity"
msgstr "Ozbiljnost"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__state
msgid "State"
msgstr "Status"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Status"
msgstr "Status"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__state__success
msgid "Success"
msgstr "Uspjeh"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Today"
msgstr "Danas"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "User"
msgstr "Korisnik"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "View collection"
msgstr "Pregled kolekcije"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__warning
msgid "Warning"
msgstr "Upozorenje"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Warning errors"
msgstr "Upozoravajuće greške"

View file

@ -0,0 +1,230 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * rest_log
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-01-18 09:34+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\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"
"X-Generator: Weblate 4.17\n"
#. module: rest_log
#: model:ir.actions.server,name:rest_log.ir_cron_autovacuum_rest_log_ir_actions_server
#: model:ir.cron,cron_name:rest_log.ir_cron_autovacuum_rest_log
msgid "Auto-vacuum REST Logs"
msgstr "Registri REST auto-pulenti"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__collection
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Collection"
msgstr "Raccolta"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__collection_id
msgid "Collection ID"
msgstr "ID raccolta"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__create_uid
msgid "Created by"
msgstr "Creato da"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__create_date
msgid "Created on"
msgstr "Creato il"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Date"
msgstr "Data"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__error
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Error"
msgstr "Errore"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__exception_name
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Exception"
msgstr "Eccezione"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__exception_message
msgid "Exception Message"
msgstr "Messaggio eccezione"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Exception message"
msgstr "Messaggio eccezione"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__state__failed
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Failed"
msgstr "Fallito"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__functional
msgid "Functional"
msgstr "Funzionale"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Functional errors"
msgstr "Errori funzionali"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Group By"
msgstr "Raggruppa per"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__headers
msgid "Headers"
msgstr "Intestazioni"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__id
msgid "ID"
msgstr "ID"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log____last_update
msgid "Last Modified on"
msgstr "Ultima modifica il"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: rest_log
#: model:ir.ui.menu,name:rest_log.menu_rest_api_log
msgid "Logs"
msgstr "Log"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Logs generated today"
msgstr "Log generati oggi"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Parameters"
msgstr "Parametri"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__params
msgid "Params"
msgstr "Parametri"
#. module: rest_log
#: model:ir.model,name:rest_log.model_rest_log
msgid "REST API Logging"
msgstr "Registrazione API REST"
#. module: rest_log
#: model:res.groups,name:rest_log.group_rest_log_manager
msgid "REST Log Manager"
msgstr "Gestore log REST"
#. module: rest_log
#: model:ir.actions.act_window,name:rest_log.action_rest_log
msgid "REST Logs"
msgstr "Log REST"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__request_method
msgid "Request Method"
msgstr "Metodo richiesta"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__request_url
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Request URL"
msgstr "URL richiesta"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__result
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Result"
msgstr "Risultato"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__severe
msgid "Severe"
msgstr "Grave"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Severe errors"
msgstr "Errori gravi"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__severity
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Severity"
msgstr "Gravità"
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__state
msgid "State"
msgstr "Stato"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Status"
msgstr "Stato"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__state__success
msgid "Success"
msgstr "Successo"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Today"
msgstr "Oggi"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "User"
msgstr "Utente"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "View collection"
msgstr "Visualizza raccolta"
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__warning
msgid "Warning"
msgstr "Attenzione"
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Warning errors"
msgstr "Errori attenzione"

View file

@ -0,0 +1,227 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * rest_log
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.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"
#. module: rest_log
#: model:ir.actions.server,name:rest_log.ir_cron_autovacuum_rest_log_ir_actions_server
#: model:ir.cron,cron_name:rest_log.ir_cron_autovacuum_rest_log
msgid "Auto-vacuum REST Logs"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__collection
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Collection"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__collection_id
msgid "Collection ID"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__create_uid
msgid "Created by"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__create_date
msgid "Created on"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Date"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__display_name
msgid "Display Name"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__error
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Error"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__exception_name
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Exception"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__exception_message
msgid "Exception Message"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Exception message"
msgstr ""
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__state__failed
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Failed"
msgstr ""
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__functional
msgid "Functional"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Functional errors"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Group By"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__headers
msgid "Headers"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__id
msgid "ID"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log____last_update
msgid "Last Modified on"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__write_uid
msgid "Last Updated by"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__write_date
msgid "Last Updated on"
msgstr ""
#. module: rest_log
#: model:ir.ui.menu,name:rest_log.menu_rest_api_log
msgid "Logs"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Logs generated today"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Parameters"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__params
msgid "Params"
msgstr ""
#. module: rest_log
#: model:ir.model,name:rest_log.model_rest_log
msgid "REST API Logging"
msgstr ""
#. module: rest_log
#: model:res.groups,name:rest_log.group_rest_log_manager
msgid "REST Log Manager"
msgstr ""
#. module: rest_log
#: model:ir.actions.act_window,name:rest_log.action_rest_log
msgid "REST Logs"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__request_method
msgid "Request Method"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__request_url
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Request URL"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__result
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "Result"
msgstr ""
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__severe
msgid "Severe"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Severe errors"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__severity
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Severity"
msgstr ""
#. module: rest_log
#: model:ir.model.fields,field_description:rest_log.field_rest_log__state
msgid "State"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Status"
msgstr ""
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__state__success
msgid "Success"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Today"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "User"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_form_view
msgid "View collection"
msgstr ""
#. module: rest_log
#: model:ir.model.fields.selection,name:rest_log.selection__rest_log__severity__warning
msgid "Warning"
msgstr ""
#. module: rest_log
#: model_terms:ir.ui.view,arch_db:rest_log.rest_log_search_view
msgid "Warning errors"
msgstr ""

View file

@ -0,0 +1 @@
from . import rest_log

View file

@ -0,0 +1,196 @@
# 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 logging
from datetime import datetime, timedelta
from odoo import api, fields, models, tools
_logger = logging.getLogger(__name__)
class RESTLog(models.Model):
_name = "rest.log"
_description = "REST API Logging"
_order = "id desc"
DEFAULT_RETENTION = 30 # days
EXCEPTION_SEVERITY_MAPPING = {
"odoo.exceptions.UserError": "functional",
"odoo.exceptions.ValidationError": "functional",
# something broken somewhere
"ValueError": "severe",
"AttributeError": "severe",
"UnboundLocalError": "severe",
}
collection = fields.Char(index=True)
collection_id = fields.Integer(index=True, string="Collection ID")
request_url = fields.Char(readonly=True, string="Request URL")
request_method = fields.Char(readonly=True)
params = fields.Text(readonly=True)
# TODO: make these fields serialized and use a computed field for displaying
headers = fields.Text(readonly=True)
result = fields.Text(readonly=True)
error = fields.Text(readonly=True)
exception_name = fields.Char(readonly=True, string="Exception")
exception_message = fields.Text(readonly=True)
state = fields.Selection(
selection=[("success", "Success"), ("failed", "Failed")], readonly=True
)
severity = fields.Selection(
selection=[
("functional", "Functional"),
("warning", "Warning"),
("severe", "Severe"),
],
compute="_compute_severity",
store=True,
# Grant specific override services' dispatch_exception override
# or via UI: user can classify errors as preferred on demand
# (maybe using mass_edit)
readonly=False,
)
@api.depends("state", "exception_name", "error")
def _compute_severity(self):
for rec in self:
rec.severity = rec.severity or rec._get_severity()
def _get_severity(self):
if not self.exception_name:
return False
mapping = self._get_exception_severity_mapping()
return mapping.get(self.exception_name, "warning")
def _get_exception_severity_mapping_param(self):
param = (
self.env["ir.config_parameter"]
.sudo()
.get_param("rest.log.severity.exception.mapping")
)
return param.strip() if param else ""
@tools.ormcache("self._get_exception_severity_mapping_param()")
def _get_exception_severity_mapping(self):
mapping = self.EXCEPTION_SEVERITY_MAPPING.copy()
param = self._get_exception_severity_mapping_param()
if not param:
return mapping
# param should be in the form
# `[module.dotted.path.]ExceptionName:severity,ExceptionName:severity`
for rule in param.split(","):
if not rule.strip():
continue
exc_name = severity = None
try:
exc_name, severity = [x.strip() for x in rule.split(":")]
if not exc_name or not severity:
raise ValueError
except ValueError:
_logger.info(
"Could not convert System Parameter"
" 'rest.log.severity.exception.mapping' to mapping."
" The following rule will be ignored: %s",
rule,
)
if exc_name and severity:
mapping[exc_name] = severity
return mapping
def _logs_retention_days(self):
retention = self.DEFAULT_RETENTION
param = (
self.env["ir.config_parameter"].sudo().get_param("rest.log.retention.days")
)
if param:
try:
retention = int(param)
except ValueError:
_logger.exception(
"Could not convert System Parameter"
" 'rest.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
def _get_log_active_param(self):
param = self.env["ir.config_parameter"].sudo().get_param("rest.log.active")
return param.strip() if param else ""
@tools.ormcache("self._get_log_active_param()")
def _get_log_active_conf(self):
"""Compute log active configuration.
Possible configuration contains a CSV like this:
`collection_name` -> enable for all endpoints of the collection
`collection_name.usage` -> enable for specific endpoints
`collection_name.usage.endpoint` -> enable for specific endpoints
`collection_name*:state` -> enable only for specific state (success, failed)
By default matching keys are enabled for all states.
:return: mapping by matching key / enabled states
"""
param = self._get_log_active_param()
conf = {}
lines = [x.strip() for x in param.split(",") if x.strip()]
for line in lines:
bits = [x.strip() for x in line.split(":") if x.strip()]
if len(bits) > 1:
match_key = bits[0]
# fmt: off
states = (bits[1], )
# fmt: on
else:
match_key = line
states = ("success", "failed")
conf[match_key] = states
return conf
@api.model
def _get_matching_active_conf(self, collection, usage, method_name):
"""Retrieve conf matching current service and method."""
conf = self._get_log_active_conf()
candidates = (
collection + "." + usage + "." + method_name,
collection + "." + usage,
collection,
)
for candidate in candidates:
if conf.get(candidate):
return conf.get(candidate)
def action_view_collection(self):
"""Open collection if we have a real record.
NOTE: use an action instead of a `Reference` field with computed method
because it would force use to have glue modules to provide a selection
for every model we want to support.
"""
# TODO: on the next round, compute the collection name
# to be used for the button label.
# No ID or no real model
if self.collection not in self.env or not self.collection_id:
return
action = self.env[self.collection].get_formview_action()
action["res_id"] = self.collection_id
return action

View file

@ -0,0 +1,33 @@
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, REST logs are kept 30 days.
You can change the duration of the retention by changing the System Parameter
``rest.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.
Logs activation
~~~~~~~~~~~~~~~
You have 2 ways to activate logging:
* on the service component set `_log_calls_in_db = True`
* via configuration
In the 1st case, calls will be always be logged.
In the 2nd case you can set ``rest.log.active`` param as::
`collection_name` # enable for all endpoints of the collection
`collection_name.usage` # enable for specific endpoints
`collection_name.usage.endpoint` # enable for specific endpoints
`collection_name*:state` # enable only for specific state (success, failed)

View file

@ -0,0 +1,2 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Simone Orsi <simahawk@gmail.com>

View file

@ -0,0 +1,5 @@
**Financial support**
* Cosanum
* Camptocamp R&D
* ACSONE R&D

View file

@ -0,0 +1,9 @@
When exposing REST services is often useful to see what's happening
especially in case of errors.
This module add DB logging for REST requests.
It also inject in the response the URL of the log entry created.
NOTE: this feature was implemented initially inside shopfloor app.
Up to version 13.0.1.2.1 of this module,
if shopfloor is installed, log records will be copied from its table.

View file

@ -0,0 +1,4 @@
13.0.1.0.0
~~~~~~~~~~
First official version.

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 ACSONE SA/NV
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo noupdate="1">
<record id="group_rest_log_manager" model="res.groups">
<field name="name">REST Log Manager</field>
<field
name="users"
eval="[(4, ref('base.user_root')),(4, ref('base.user_admin'))]"
/>
</record>
</odoo>

View file

@ -0,0 +1,2 @@
"id","name","model_id/id","group_id/id","perm_read","perm_write","perm_create","perm_unlink"
"access_rest_log","access_rest_log","model_rest_log","rest_log.group_rest_log_manager",1,0,0,0
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_rest_log access_rest_log model_rest_log rest_log.group_rest_log_manager 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,489 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>REST Log</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="rest-log">
<h1 class="title">REST Log</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ed7eb7cec756c78c39eb3dc04ca382ea64926defada6d97aa72e859db389d8b2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/rest-framework/tree/16.0/rest_log"><img alt="OCA/rest-framework" src="https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-rest_log"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>When exposing REST services is often useful to see whats happening
especially in case of errors.</p>
<p>This module add DB logging for REST requests.
It also inject in the response the URL of the log entry created.</p>
<p>NOTE: this feature was implemented initially inside shopfloor app.
Up to version 13.0.1.2.1 of this module,
if shopfloor is installed, log records will be copied from its table.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a><ul>
<li><a class="reference internal" href="#logs-retention" id="toc-entry-2">Logs retention</a></li>
<li><a class="reference internal" href="#logs-activation" id="toc-entry-3">Logs activation</a></li>
</ul>
</li>
<li><a class="reference internal" href="#changelog" id="toc-entry-4">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-5">13.0.1.0.0</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-6">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-7">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-8">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-9">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-10">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-11">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<div class="section" id="logs-retention">
<h2><a class="toc-backref" href="#toc-entry-2">Logs retention</a></h2>
<p>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.</p>
<p>The Logs menu is shown only with Developer tools (<tt class="docutils literal"><span class="pre">?debug=1</span></tt>) activated.</p>
<p>By default, REST logs are kept 30 days.
You can change the duration of the retention by changing the System Parameter
<tt class="docutils literal">rest.log.retention.days</tt>.</p>
<p>If the value is set to 0, the logs are not stored at all.</p>
<p>Logged data is: request URL and method, parameters, headers, result or error.</p>
</div>
<div class="section" id="logs-activation">
<h2><a class="toc-backref" href="#toc-entry-3">Logs activation</a></h2>
<p>You have 2 ways to activate logging:</p>
<ul class="simple">
<li>on the service component set <cite>_log_calls_in_db = True</cite></li>
<li>via configuration</li>
</ul>
<p>In the 1st case, calls will be always be logged.</p>
<p>In the 2nd case you can set <tt class="docutils literal">rest.log.active</tt> param as:</p>
<pre class="literal-block">
`collection_name` # enable for all endpoints of the collection
`collection_name.usage` # enable for specific endpoints
`collection_name.usage.endpoint` # enable for specific endpoints
`collection_name*:state` # enable only for specific state (success, failed)
</pre>
</div>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-4">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-5">13.0.1.0.0</a></h2>
<p>First official version.</p>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-6">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rest-framework/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/rest-framework/issues/new?body=module:%20rest_log%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-7">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-8">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
<li>ACSONE</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-9">Contributors</a></h2>
<ul class="simple">
<li>Guewen Baconnier &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;</li>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simahawk&#64;gmail.com">simahawk&#64;gmail.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#toc-entry-10">Other credits</a></h2>
<p><strong>Financial support</strong></p>
<ul class="simple">
<li>Cosanum</li>
<li>Camptocamp R&amp;D</li>
<li>ACSONE R&amp;D</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-11">Maintainers</a></h2>
<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>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/simahawk"><img alt="simahawk" src="https://github.com/simahawk.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rest-framework/tree/16.0/rest_log">OCA/rest-framework</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>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
from . import test_db_logging

View file

@ -0,0 +1,73 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# @author Simone Orsi <simahawk@gmail.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import contextlib
from psycopg2 import errorcodes
from psycopg2.errors import OperationalError
from odoo import exceptions
from odoo.addons.base_rest import restapi
from odoo.addons.component.core import Component
from odoo.addons.website.tools import MockRequest
class TestDBLoggingMixin(object):
@staticmethod
def _get_service(class_or_instance, collection=None):
# pylint: disable=R7980
class LoggedService(Component):
_inherit = "base.rest.service"
_name = "test.log.service"
_usage = "logmycalls"
_collection = class_or_instance._collection_name
_description = "Test log my calls"
_log_calls_in_db = True
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_out_schema"),
auth="public",
)
def get(self, _id):
"""Get some information"""
return {"name": "Mr Logger"}
def _get_out_schema(self):
return {"name": {"type": "string", "required": True}}
@restapi.method([(["/fail/<string:how>"], "GET")], auth="public")
def fail(self, how):
"""Test a failure"""
exc = {
"value": ValueError,
"validation": exceptions.ValidationError,
"user": exceptions.UserError,
"retryable": FakeConcurrentUpdateError,
}
raise exc[how]("Failed as you wanted!")
class_or_instance.comp_registry.load_components("rest_log")
# class_or_instance._build_services(class_or_instance, LoggedService)
# TODO: WTH _build_services does not load the component?
LoggedService._build_component(class_or_instance.comp_registry)
return class_or_instance._get_service_component(
class_or_instance, "logmycalls", collection=collection
)
@contextlib.contextmanager
def _get_mocked_request(self, env=None, httprequest=None, extra_headers=None):
with MockRequest(env or self.env) as mocked_request:
mocked_request.httprequest = httprequest or mocked_request.httprequest
headers = {"Cookie": "IaMaCookie!", "Api-Key": "I_MUST_STAY_SECRET"}
headers.update(extra_headers or {})
mocked_request.httprequest.headers = headers
yield mocked_request
class FakeConcurrentUpdateError(OperationalError):
@property
def pgcode(self):
return errorcodes.SERIALIZATION_FAILURE

View file

@ -0,0 +1,439 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
# from urllib.parse import urlparse
import json
from unittest import mock
from odoo import exceptions
from odoo.http import Response
from odoo.tools import mute_logger
from odoo.addons.base_rest.controllers.main import _PseudoCollection
from odoo.addons.base_rest.tests.common import TransactionRestServiceRegistryCase
from odoo.addons.component.tests.common import new_rollbacked_env
from odoo.addons.rest_log import exceptions as log_exceptions # pylint: disable=W7950
from .common import FakeConcurrentUpdateError, TestDBLoggingMixin
class TestDBLogging(TransactionRestServiceRegistryCase, TestDBLoggingMixin):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._setup_registry(cls)
cls.service = cls._get_service(cls)
cls.log_model = cls.env["rest.log"].sudo()
@classmethod
def tearDownClass(cls):
# pylint: disable=W8110
cls._teardown_registry(cls)
super().tearDownClass()
def test_log_enabled_conf_parsing(self):
key1 = "coll1.service1.endpoint"
key2 = "coll1.service2.endpoint:failed"
key3 = "coll2.service1.endpoint:success"
self.env["ir.config_parameter"].sudo().set_param(
"rest.log.active", ",".join((key1, key2, key3))
)
expected = {
# fmt:off
"coll1.service1.endpoint": ("success", "failed"),
"coll1.service2.endpoint": ("failed", ),
"coll2.service1.endpoint": ("success", ),
# fmt: on
}
self.assertEqual(self.env["rest.log"]._get_log_active_conf(), expected)
def test_log_enabled(self):
self.service._log_calls_in_db = False
with self._get_mocked_request():
# no conf no flag
self.assertFalse(self.service._db_logging_active("avg_endpoint"))
# by conf for collection
self.env["ir.config_parameter"].sudo().set_param(
"rest.log.active", self.service._collection
)
self.assertTrue(self.service._db_logging_active("avg_endpoint"))
# by conf for usage
self.env["ir.config_parameter"].sudo().set_param(
"rest.log.active", self.service._collection + "." + self.service._usage
)
self.assertTrue(self.service._db_logging_active("avg_endpoint"))
# by conf for usage and endpoint
self.env["ir.config_parameter"].sudo().set_param(
"rest.log.active",
self.service._collection + "." + self.service._usage + ".avg_endpoint",
)
self.assertTrue(self.service._db_logging_active("avg_endpoint"))
self.assertFalse(self.service._db_logging_active("not_so_avg_endpoint"))
# no conf, service class flag
self.env["ir.config_parameter"].sudo().set_param("rest.log.active", "")
self.service._log_calls_in_db = True
self.assertTrue(self.service._db_logging_active("avg_endpoint"))
def test_no_log_entry(self):
self.service._log_calls_in_db = False
log_entry_count = self.log_model.search_count([])
with self._get_mocked_request():
resp = self.service.dispatch("get", 100)
self.assertNotIn("log_entry_url", resp)
self.assertFalse(self.log_model.search_count([]) > log_entry_count)
def test_log_entry(self):
log_entry_count = self.log_model.search_count([])
with self._get_mocked_request():
resp = self.service.dispatch("get", 100)
self.assertIn("log_entry_url", resp)
self.assertTrue(self.log_model.search_count([]) > log_entry_count)
def test_log_entry_values_success(self):
params = {"some": "value"}
kw = {"result": {"data": "worked!"}}
# test full data request only once, other tests will skip this part
httprequest = mock.Mock(
url="https://my.odoo.test/service/endpoint", method="POST"
)
extra_headers = {"KEEP-ME": "FOO"}
with self._get_mocked_request(
httprequest=httprequest, extra_headers=extra_headers
) as mocked_request:
entry = self.service._log_call_in_db(
self.env, mocked_request, "avg_method", params=params, **kw
)
expected = {
"collection": self.service._collection,
"request_url": httprequest.url,
"request_method": httprequest.method,
"state": "success",
"error": False,
"exception_name": False,
"severity": False,
}
self.assertRecordValues(entry, [expected])
expected_json = {
"result": {"data": "worked!"},
"params": dict(params),
"headers": {
"Cookie": "<redacted>",
"Api-Key": "<redacted>",
"KEEP-ME": "FOO",
},
}
for k, v in expected_json.items():
self.assertEqual(json.loads(entry[k]), v)
def test_log_entry_values_failed(self):
params = {"some": "value"}
# no result, will fail
kw = {"result": {}}
with self._get_mocked_request() as mocked_request:
entry = self.service._log_call_in_db(
self.env, mocked_request, "avg_method", params=params, **kw
)
expected = {
"collection": self.service._collection,
"state": "failed",
"result": "{}",
"error": False,
"exception_name": False,
"severity": False,
}
self.assertRecordValues(entry, [expected])
def _test_log_entry_values_failed_with_exception_default(self, severity=None):
params = {"some": "value"}
fake_tb = """
[...]
File "/somewhere/in/your/custom/code/file.py", line 503, in write
[...]
ValueError: Ops, something went wrong
"""
orig_exception = ValueError("Ops, something went wrong")
kw = {"result": {}, "traceback": fake_tb, "orig_exception": orig_exception}
with self._get_mocked_request() as mocked_request:
entry = self.service._log_call_in_db(
self.env, mocked_request, "avg_method", params=params, **kw
)
expected = {
"collection": self.service._collection,
"state": "failed",
"result": "{}",
"error": fake_tb,
"exception_name": "ValueError",
"exception_message": "Ops, something went wrong",
"severity": severity or "severe",
}
self.assertRecordValues(entry, [expected])
def test_log_entry_values_failed_with_exception_default(self):
self._test_log_entry_values_failed_with_exception_default()
def test_log_entry_values_failed_with_exception_functional(self):
params = {"some": "value"}
fake_tb = """
[...]
File "/somewhere/in/your/custom/code/file.py", line 503, in write
[...]
UserError: You are doing something wrong Dave!
"""
orig_exception = exceptions.UserError("You are doing something wrong Dave!")
kw = {"result": {}, "traceback": fake_tb, "orig_exception": orig_exception}
with self._get_mocked_request() as mocked_request:
entry = self.service._log_call_in_db(
self.env, mocked_request, "avg_method", params=params, **kw
)
expected = {
"collection": self.service._collection,
"state": "failed",
"result": "{}",
"error": fake_tb,
"exception_name": "odoo.exceptions.UserError",
"exception_message": "You are doing something wrong Dave!",
"severity": "functional",
}
self.assertRecordValues(entry, [expected])
# test that we can still change severity as we like
entry.severity = "severe"
self.assertEqual(entry.severity, "severe")
def test_log_entry_severity_mapping_param(self):
# test override of mapping via config param
mapping = self.log_model._get_exception_severity_mapping()
self.assertEqual(mapping, self.log_model.EXCEPTION_SEVERITY_MAPPING)
self.assertEqual(mapping["ValueError"], "severe")
self.assertEqual(mapping["odoo.exceptions.UserError"], "functional")
value = "ValueError: warning, odoo.exceptions.UserError: severe"
self.env["ir.config_parameter"].sudo().create(
{"key": "rest.log.severity.exception.mapping", "value": value}
)
mapping = self.log_model._get_exception_severity_mapping()
self.assertEqual(mapping["ValueError"], "warning")
self.assertEqual(mapping["odoo.exceptions.UserError"], "severe")
self._test_log_entry_values_failed_with_exception_default("warning")
@mute_logger("odoo.addons.rest_log.models.rest_log")
def test_log_entry_severity_mapping_param_bad_values(self):
# bad values are discarded
value = """
ValueError: warning,
odoo.exceptions.UserError::badvalue,
VeryBadValue|error
"""
self.env["ir.config_parameter"].sudo().create(
{"key": "rest.log.severity.exception.mapping", "value": value}
)
mapping = self.log_model._get_exception_severity_mapping()
expected = self.log_model.EXCEPTION_SEVERITY_MAPPING.copy()
expected["ValueError"] = "warning"
self.assertEqual(mapping, expected)
def test_log_entry_values_success_with_response(self):
with self._get_mocked_request() as mocked_request:
res = Response(
b"A test .pdf file to download",
headers=[
("Content-Type", "application/pdf"),
("X-Content-Type-Options", "nosniff"),
("Content-Disposition", "attachment; filename*=UTF-8''test.pdf"),
("Content-Length", 28),
],
)
res.status_code = 200
entry = self.service._log_call_in_db(
self.env, mocked_request, "method", result=res
)
self.assertEqual(entry.state, "success")
self.assertEqual(
json.loads(entry.result),
{
"headers": {
"Content-Disposition": "attachment; filename*=UTF-8''test.pdf",
"Content-Length": "28",
"Content-Type": "application/pdf",
"X-Content-Type-Options": "nosniff",
},
"status": 200,
},
)
def test_log_entry_values_failure_with_response(self):
with self._get_mocked_request() as mocked_request:
res = Response(b"", headers=[])
res.status_code = 418
entry = self.service._log_call_in_db(
self.env, mocked_request, "method", result=res
)
self.assertEqual(entry.state, "failed")
self.assertEqual(
json.loads(entry.result),
{
"headers": {
"Content-Length": "0",
"Content-Type": "text/html; charset=utf-8",
},
"status": 418,
},
)
class TestDBLoggingExceptionBase(
TransactionRestServiceRegistryCase, TestDBLoggingMixin
):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._setup_registry(cls)
@classmethod
def tearDownClass(cls):
# pylint: disable=W8110
cls._teardown_registry(cls)
super().tearDownClass()
def _test_exception(self, test_type, wrapping_exc, exc_name, severity):
log_model = self.env["rest.log"].sudo()
initial_entries = log_model.search([])
entry_url_from_exc = None
# Context: we are running in a transaction case which uses savepoints.
# The log machinery is going to rollback the transation when catching errors.
# Hence we need a completely separated env for the service.
with new_rollbacked_env() as new_env:
# Init fake collection w/ new env
collection = _PseudoCollection(self._collection_name, new_env)
service = self._get_service(self, collection=collection)
with self._get_mocked_request(env=new_env):
try:
service.dispatch("fail", test_type)
except Exception as err:
# Not using `assertRaises` to inspect the exception directly
self.assertTrue(isinstance(err, wrapping_exc))
self.assertEqual(
service._get_exception_message(err), "Failed as you wanted!"
)
entry_url_from_exc = err.rest_json_info["log_entry_url"]
with new_rollbacked_env() as new_env:
log_model = new_env["rest.log"].sudo()
entry = log_model.search([]) - initial_entries
expected = {
"collection": service._collection,
"state": "failed",
"result": "null",
"exception_name": exc_name,
"exception_message": "Failed as you wanted!",
"severity": severity,
}
self.assertRecordValues(entry, [expected])
self.assertEqual(entry_url_from_exc, service._get_log_entry_url(entry))
class TestDBLoggingExceptionUserError(TestDBLoggingExceptionBase):
@staticmethod
def _get_test_controller(class_or_instance, root_path=None):
# Override to avoid registering twice the same controller route.
return super()._get_test_controller(
class_or_instance, root_path="/test_log_exception_user/"
)
def test_log_exception_user(self):
self._test_exception(
"user",
log_exceptions.RESTServiceUserErrorException,
"odoo.exceptions.UserError",
"functional",
)
class TestDBLoggingExceptionValidationError(TestDBLoggingExceptionBase):
@staticmethod
def _get_test_controller(class_or_instance, root_path=None):
return super()._get_test_controller(
class_or_instance, root_path="/test_log_exception_validation/"
)
def test_log_exception_validation(self):
self._test_exception(
"validation",
log_exceptions.RESTServiceValidationErrorException,
"odoo.exceptions.ValidationError",
"functional",
)
class TestDBLoggingExceptionValueError(TestDBLoggingExceptionBase):
@staticmethod
def _get_test_controller(class_or_instance, root_path=None):
return super()._get_test_controller(
class_or_instance, root_path="/test_log_exception_value/"
)
def test_log_exception_value(self):
self._test_exception(
"value", log_exceptions.RESTServiceDispatchException, "ValueError", "severe"
)
class TestDBLoggingRetryableError(
TransactionRestServiceRegistryCase, TestDBLoggingMixin
):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._setup_registry(cls)
@classmethod
def tearDownClass(cls):
# pylint: disable=W8110
cls._teardown_registry(cls)
super().tearDownClass()
def _test_exception(self, test_type, wrapping_exc, exc_name, severity):
log_model = self.env["rest.log"].sudo()
initial_entries = log_model.search([])
# Context: we are running in a transaction case which uses savepoints.
# The log machinery is going to rollback the transation when catching errors.
# Hence we need a completely separated env for the service.
with new_rollbacked_env() as new_env:
# Init fake collection w/ new env
collection = _PseudoCollection(self._collection_name, new_env)
service = self._get_service(self, collection=collection)
with self._get_mocked_request(env=new_env):
try:
service.dispatch("fail", test_type)
except Exception as err:
# Not using `assertRaises` to inspect the exception directly
self.assertTrue(isinstance(err, wrapping_exc))
self.assertEqual(
service._get_exception_message(err), "Failed as you wanted!"
)
with new_rollbacked_env() as new_env:
log_model = new_env["rest.log"].sudo()
entry = log_model.search([]) - initial_entries
expected = {
"collection": service._collection,
"state": "failed",
"result": "null",
"exception_name": exc_name,
"exception_message": "Failed as you wanted!",
"severity": severity,
}
self.assertRecordValues(entry, [expected])
@staticmethod
def _get_test_controller(class_or_instance, root_path=None):
return super()._get_test_controller(
class_or_instance, root_path="/test_log_exception_retryable/"
)
def test_log_exception_retryable(self):
# retryable error must bubble up to the retrying mechanism
self._test_exception(
"retryable",
FakeConcurrentUpdateError,
"odoo.addons.rest_log.tests.common.FakeConcurrentUpdateError",
"warning",
)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="menu_rest_api_log" model="ir.ui.menu">
<field name="parent_id" ref="base_rest.menu_rest_api_root" />
<field name="name">Logs</field>
<field name="sequence" eval="80" />
<field name="action" ref="rest_log.action_rest_log" />
</record>
</odoo>

View file

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="rest_log_tree_view" model="ir.ui.view">
<field name="name">rest.log tree</field>
<field name="model">rest.log</field>
<field name="arch" type="xml">
<tree decoration-danger="state == 'failed'">
<field name="collection" optional="hide" />
<field name="collection_id" optional="hide" />
<field name="create_uid" />
<field name="create_date" />
<field name="request_method" />
<field name="request_url" />
<field name="state" />
<field name="exception_name" />
<field name="exception_message" />
<field name="severity" />
</tree>
</field>
</record>
<record id="rest_log_form_view" model="ir.ui.view">
<field name="name">rest.log form</field>
<field name="model">rest.log</field>
<field name="arch" type="xml">
<form>
<field name="collection_id" invisible="1" />
<header>
<field name="state" widget="statusbar" />
</header>
<sheet>
<div
class="oe_button_box"
name="button_box"
attrs="{'invisible': ['|', ('id','=',False), ('collection_id', '=', False)]}"
>
<button
type="object"
name="action_view_collection"
icon="fa-eye"
attrs="{'invisible': [('collection_id', '=', False)]}"
>
View collection
</button>
</div>
<group>
<group>
<field name="collection" />
<field name="request_url" />
<field name="request_method" />
</group>
<group>
<field name="create_uid" />
<field name="create_date" />
</group>
</group>
<group string="Parameters" name="parameters">
<field name="params" widget="ace" />
<field name="headers" widget="ace" />
</group>
<group
string="Result"
name="result"
attrs="{'invisible': [('state', '!=', 'success')]}"
>
<group>
<field name="result" nolabel="1" widget="ace" colspan="2" />
</group>
</group>
<group
string="Error"
name="error"
attrs="{'invisible': [('state', '!=', 'failed')]}"
>
<group colspan="2">
<field name="exception_name" />
<field name="exception_message" />
<field name="severity" />
</group>
<group colspan="4">
<field
name="error"
nolabel="1"
widget="ace"
options="{'mode': 'python'}"
colspan="2"
/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="rest_log_search_view" model="ir.ui.view">
<field name="name">rest.log search</field>
<field name="model">rest.log</field>
<field name="arch" type="xml">
<search>
<field name="collection" />
<field name="collection_id" />
<field name="state" />
<field name="request_url" />
<field name="request_method" />
<field name="params" />
<field name="headers" />
<field name="exception_name" />
<field name="severity" />
<filter
string="Today"
name="today"
date="create_date"
help="Logs generated today"
/>
<separator />
<filter
string="Failed"
name="filter_status_failed"
domain="[('state', '=', 'failed')]"
/>
<filter
string="Severe errors"
name="filter_severity_severe"
domain="[('severity', '=', 'severe')]"
/>
<filter
string="Warning errors"
name="filter_severity_warning"
domain="[('severity', '=', 'warning')]"
/>
<filter
string="Functional errors"
name="filter_severity_functional"
domain="[('severity', '=', 'functional')]"
/>
<group expand="0" string="Group By">
<filter
string="Collection"
name="by_collection"
domain="[]"
context="{'group_by': 'collection'}"
/>
<filter
string="User"
name="by_user"
domain="[]"
context="{'group_by': 'create_uid'}"
/>
<filter
string="Request URL"
name="groupby_request_url"
domain="[]"
context="{'group_by': 'request_url'}"
/>
<filter
string="Status"
name="status"
domain="[]"
context="{'group_by': 'state'}"
/>
<filter
string="Exception"
name="exception_name"
domain="[]"
context="{'group_by': 'exception_name'}"
/>
<filter
string="Exception message"
name="exception_message"
domain="[]"
context="{'group_by': 'exception_message'}"
/>
<filter
string="Severity"
name="severity"
domain="[]"
context="{'group_by': 'severity'}"
/>
<filter
string="Date"
name="groupby_create_date"
domain="[]"
context="{'group_by': 'create_date'}"
groups="base.group_no_one"
/>
</group>
</search>
</field>
</record>
<record id="action_rest_log" model="ir.actions.act_window">
<field name="name">REST Logs</field>
<field name="res_model">rest.log</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="rest_log_search_view" />
</record>
</odoo>