From 4ca00388eae73ddcc39182ae64b4a8e8436f842e Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Tue, 15 Apr 2025 11:50:18 +0200 Subject: [PATCH 01/24] [REF] fastapi_log: Extract common features to `api_log` This way other APIs might use the new module `api_log` to store logs. --- api_log/README.rst | 89 ++++++ api_log/__init__.py | 1 + api_log/__manifest__.py | 20 ++ api_log/models/__init__.py | 1 + api_log/models/api_log.py | 212 +++++++++++++ api_log/readme/CONTRIBUTORS.md | 5 + api_log/readme/DESCRIPTION.md | 1 + api_log/security/ir_model_access.xml | 17 + api_log/security/res_groups.xml | 17 + api_log/static/description/index.html | 431 ++++++++++++++++++++++++++ api_log/tests/__init__.py | 1 + api_log/tests/common.py | 8 + api_log/tests/test_api_log.py | 30 ++ api_log/views/api_log_views.xml | 110 +++++++ 14 files changed, 943 insertions(+) create mode 100644 api_log/README.rst create mode 100644 api_log/__init__.py create mode 100644 api_log/__manifest__.py create mode 100644 api_log/models/__init__.py create mode 100644 api_log/models/api_log.py create mode 100644 api_log/readme/CONTRIBUTORS.md create mode 100644 api_log/readme/DESCRIPTION.md create mode 100644 api_log/security/ir_model_access.xml create mode 100644 api_log/security/res_groups.xml create mode 100644 api_log/static/description/index.html create mode 100644 api_log/tests/__init__.py create mode 100644 api_log/tests/common.py create mode 100644 api_log/tests/test_api_log.py create mode 100644 api_log/views/api_log_views.xml diff --git a/api_log/README.rst b/api_log/README.rst new file mode 100644 index 000000000..84b9d1e11 --- /dev/null +++ b/api_log/README.rst @@ -0,0 +1,89 @@ +======= +API Log +======= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/api_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-api_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| + +This module allows to store request and response logs for any API. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com +- Guewen Baconnier guewen.baconnier@camptocamp.com +- Simone Orsi simahawk@gmail.com +- `PyTech `__: + + - Simone Rubino + +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-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/api_log/__init__.py b/api_log/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/api_log/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/api_log/__manifest__.py b/api_log/__manifest__.py new file mode 100644 index 000000000..84193a908 --- /dev/null +++ b/api_log/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "API Log", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "license": "AGPL-3", + "summary": "Log API requests in database", + "category": "Tools", + "depends": ["web"], + "website": "https://github.com/OCA/rest-framework", + "data": [ + "security/res_groups.xml", + "security/ir_model_access.xml", + "views/api_log_views.xml", + ], + "maintainers": ["paradoxxxzero"], +} diff --git a/api_log/models/__init__.py b/api_log/models/__init__.py new file mode 100644 index 000000000..0f340289c --- /dev/null +++ b/api_log/models/__init__.py @@ -0,0 +1 @@ +from . import api_log diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py new file mode 100644 index 000000000..c2cb5bf0d --- /dev/null +++ b/api_log/models/api_log.py @@ -0,0 +1,212 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import json +import time +from traceback import format_exception + +from werkzeug.exceptions import HTTPException as WerkzeugHTTPException + +from odoo import api, fields, models + + +class APILog(models.Model): + _name = "api.log" + _description = "Log for API" + _order = "id desc" + + # Request + request_url = fields.Char() + request_method = fields.Char() + request_headers = fields.Json() + request_body = fields.Binary(attachment=False) + request_date = fields.Datetime() + request_time = fields.Float() + + # Response + response_status_code = fields.Integer() + response_headers = fields.Json() + response_body = fields.Binary(attachment=False) + response_date = fields.Datetime() + response_time = fields.Float() + + stack_trace = fields.Text() + + # Derived fields + name = fields.Char(compute="_compute_name", store=True) + time = fields.Float(compute="_compute_time", store=True) + request_preview = fields.Text(compute="_compute_request_preview") + response_preview = fields.Text(compute="_compute_response_preview") + # Binary fields are useful to download the payload in case of file download/upload + request_b64 = fields.Binary( + string="Request Content", compute="_compute_request_b64" + ) + response_b64 = fields.Binary( + string="Response Content", compute="_compute_response_b64" + ) + request_headers_preview = fields.Text(compute="_compute_headers_preview") + response_headers_preview = fields.Text(compute="_compute_headers_preview") + request_content_type = fields.Char( + compute="_compute_request_headers_derived", store=True + ) + request_content_length = fields.Integer( + compute="_compute_request_headers_derived", store=True + ) + referrer = fields.Char(compute="_compute_request_headers_derived", store=True) + response_content_type = fields.Char( + compute="_compute_response_headers_derived", store=True + ) + response_content_length = fields.Integer( + compute="_compute_response_headers_derived", store=True + ) + + def _headers_to_dict(self, headers): + return {key.lower(): value for key, value in headers.items()} + + def _current_time(self): + return time.time_ns() / 1e9 + + @api.model + def _get_http_request(self, request): + return request.httprequest + + @api.model + def _get_request_body(self, request): + """Take extra care with the request's body because it might get consumed.""" + httprequest = self._get_http_request(request) + return httprequest.data + + @api.model + def _prepare_log_request(self, request): + httprequest = self._get_http_request(request) + log_request_values = { + "request_url": httprequest.url, + "request_method": httprequest.method, + "request_headers": self._headers_to_dict(httprequest.headers), + "request_body": self._get_request_body(request), + "request_date": fields.Datetime.now(), + "request_time": self._current_time(), + } + return log_request_values + + @api.model + def log_request(self, request, override_log_values=None): + log_request_values = self._prepare_log_request(request) + log_request_values.update(override_log_values or {}) + return self.sudo().create(log_request_values) + + def _prepare_log_response(self, response): + return { + "response_status_code": response.status_code, + "response_headers": self._headers_to_dict(response.headers), + "response_body": response.data, + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + + def log_response(self, response): + log_response_values = self._prepare_log_response(response) + return self.sudo().write(log_response_values) + + def _prepare_log_exception(self, exception): + values = { + "stack_trace": "".join(format_exception(exception)), + "response_body": str(exception), + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + + if isinstance(exception, WerkzeugHTTPException): + values.update( + { + "response_status_code": exception.code, + "response_headers": self._headers_to_dict(exception.get_headers()), + "response_body": exception.get_body(), + } + ) + return values + + def log_exception(self, exception): + try: + exc_handling_response = self.env.registry["ir.http"]._handle_error( + exception + ) + self.log_response(exc_handling_response) + except Exception as handling_exception: + exception = handling_exception + log_exception_values = self._prepare_log_exception(exception) + return self.sudo().write(log_exception_values) + + @api.depends("request_url", "request_method", "request_date") + def _compute_name(self): + for log in self: + log.name = ( + f"{log.request_date.isoformat()} - " + f"[{log.request_method}] {log.request_url}" + ) + + @api.depends("request_time", "response_time") + def _compute_time(self): + for log in self: + if log.request_time and log.response_time: + log.time = log.response_time - log.request_time + else: + log.time = 0 + + @api.depends("request_headers") + def _compute_request_headers_derived(self): + for log in self: + headers = log.request_headers or {} + log.request_content_type = headers.get("content-type", "") + log.request_content_length = headers.get("content-length", 0) + log.referrer = headers.get("referer", "") + + @api.depends("response_headers") + def _compute_response_headers_derived(self): + for log in self: + headers = log.response_headers or {} + log.response_content_type = headers.get("content-type", "") + log.response_content_length = headers.get("content-length", 0) + + @api.depends("request_body") + def _compute_request_preview(self): + for log in self.with_context(bin_size=False): + log.request_preview = log._body_preview(log.request_body) + + @api.depends("response_body") + def _compute_response_preview(self): + for log in self.with_context(bin_size=False): + log.response_preview = log._body_preview(log.response_body) + + def _body_preview(self, body): + # Display the first 1000 characters of the body if it's a text content + body_preview = False + if body: + try: + body_preview = body.decode("utf-8", errors="ignore") + if len(body_preview) > 1000: + body_preview = body_preview[:1000] + "...\n(...)" + except UnicodeDecodeError: + body_preview = False + return body_preview + + @api.depends("request_headers", "response_headers") + def _compute_headers_preview(self): + for log in self: + log.request_headers_preview = log._headers_preview(log.request_headers) + log.response_headers_preview = log._headers_preview(log.response_headers) + + def _headers_preview(self, headers): + return json.dumps(headers, sort_keys=True, indent=4) if headers else False + + @api.depends("request_body") + def _compute_request_b64(self): + for log in self: + log.request_b64 = base64.b64encode(log.request_body or b"") + + @api.depends("response_body") + def _compute_response_b64(self): + for log in self: + log.response_b64 = base64.b64encode(log.response_body or b"") diff --git a/api_log/readme/CONTRIBUTORS.md b/api_log/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..599c28bb2 --- /dev/null +++ b/api_log/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Florian Mounier +- Guewen Baconnier +- Simone Orsi +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/api_log/readme/DESCRIPTION.md b/api_log/readme/DESCRIPTION.md new file mode 100644 index 000000000..6018fc343 --- /dev/null +++ b/api_log/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to store request and response logs for any API. diff --git a/api_log/security/ir_model_access.xml b/api_log/security/ir_model_access.xml new file mode 100644 index 000000000..a092c0d3a --- /dev/null +++ b/api_log/security/ir_model_access.xml @@ -0,0 +1,17 @@ + + + + + API Log: Read access + + + + + + + + diff --git a/api_log/security/res_groups.xml b/api_log/security/res_groups.xml new file mode 100644 index 000000000..8b9ddf38b --- /dev/null +++ b/api_log/security/res_groups.xml @@ -0,0 +1,17 @@ + + + + + + API Log Access + + + + diff --git a/api_log/static/description/index.html b/api_log/static/description/index.html new file mode 100644 index 000000000..6da2b40b8 --- /dev/null +++ b/api_log/static/description/index.html @@ -0,0 +1,431 @@ + + + + + +API Log + + + +
+

API Log

+ + +

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This module allows to store request and response logs for any API.

+

Table of contents

+ +
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

paradoxxxzero

+

This module is part of the OCA/rest-framework project on GitHub.

+

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

+
+
+
+ + diff --git a/api_log/tests/__init__.py b/api_log/tests/__init__.py new file mode 100644 index 000000000..7f84a8e4f --- /dev/null +++ b/api_log/tests/__init__.py @@ -0,0 +1 @@ +from . import test_api_log diff --git a/api_log/tests/common.py b/api_log/tests/common.py new file mode 100644 index 000000000..e54316db6 --- /dev/null +++ b/api_log/tests/common.py @@ -0,0 +1,8 @@ +from odoo.tests.common import HttpCase + + +class CommonAPILog(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.log_model = cls.env["api.log"] diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py new file mode 100644 index 000000000..abed32678 --- /dev/null +++ b/api_log/tests/test_api_log.py @@ -0,0 +1,30 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import requests + +from odoo.http import Request, Response + +from odoo.addons.api_log.tests.common import CommonAPILog + + +class TestAPILog(CommonAPILog): + def test_log_request(self): + base_url = self.base_url() + httprequest = requests.Request( + url=base_url, + method="GET", + ) + request = Request(httprequest) + log = self.log_model.log_request(request) + + self.assertEqual(log.request_url, base_url) + self.assertEqual(log.request_method, "GET") + + def test_log_response(self): + response = Response() + log = self.log_model.create({}) + log.log_response(response) + + self.assertEqual(log.response_status_code, 200) diff --git a/api_log/views/api_log_views.xml b/api_log/views/api_log_views.xml new file mode 100644 index 000000000..cc09bbdaf --- /dev/null +++ b/api_log/views/api_log_views.xml @@ -0,0 +1,110 @@ + + + + + API Log + api.log + tree,form + + + + api.log.form + api.log + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + api.log.tree + api.log + + + + + + + + + + + + + + + api.log.search + api.log + + + + + + + + + + + + + + + + +
From f9b857f3d29a14bd28fcb0764b0e411cd55e2574 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 28 Apr 2025 12:42:53 +0200 Subject: [PATCH 02/24] [IMP] api_log: Hide sensitive headers --- api_log/models/api_log.py | 23 ++++++++++++++++++++++- api_log/tests/test_api_log.py | 12 ++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py index c2cb5bf0d..b1d9dbb5f 100644 --- a/api_log/models/api_log.py +++ b/api_log/models/api_log.py @@ -62,8 +62,29 @@ class APILog(models.Model): compute="_compute_response_headers_derived", store=True ) + @api.model + def _headers_hidden_keys(self): + """Header keys that should not be logged. + + They might contains sensitive data. + """ + return ( + "Api-Key", + "Cookie", + ) + + @api.model + def _sanitize_headers_dict(self, headers_dict): + keys_to_hide = self._headers_hidden_keys() + for key in headers_dict: + if key in keys_to_hide: + headers_dict[key] = "" + return headers_dict + + @api.model def _headers_to_dict(self, headers): - return {key.lower(): value for key, value in headers.items()} + headers_dict = {key: value for key, value in headers.items()} + return self._sanitize_headers_dict(headers_dict) def _current_time(self): return time.time_ns() / 1e9 diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index abed32678..52035a4c9 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -12,7 +12,15 @@ class TestAPILog(CommonAPILog): def test_log_request(self): base_url = self.base_url() + secret_api_key = "my-secret-api-key" + secret_cookie = "my-secret-biscuit" + public_header_value = "public_header_value" httprequest = requests.Request( + headers={ + "Api-Key": secret_api_key, + "Cookie": secret_cookie, + "Public-Header": public_header_value, + }, url=base_url, method="GET", ) @@ -21,6 +29,10 @@ def test_log_request(self): self.assertEqual(log.request_url, base_url) self.assertEqual(log.request_method, "GET") + headers_values = log.request_headers.values() + self.assertNotIn(secret_api_key, headers_values) + self.assertNotIn(secret_cookie, headers_values) + self.assertIn(public_header_value, headers_values) def test_log_response(self): response = Response() From 7f83203dd1ac679d5c4aaff20384d50d426fe467 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 25 Jun 2025 11:42:48 +0200 Subject: [PATCH 03/24] [IMP] api_log: Add collection of logs --- api_log/models/__init__.py | 1 + api_log/models/api_log.py | 34 +++++++++++++++++ api_log/models/api_log_collection.py | 55 ++++++++++++++++++++++++++++ api_log/tests/common.py | 5 ++- api_log/tests/test_api_log.py | 5 ++- api_log/views/api_log_views.xml | 8 ++++ 6 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 api_log/models/api_log_collection.py diff --git a/api_log/models/__init__.py b/api_log/models/__init__.py index 0f340289c..2f4388e55 100644 --- a/api_log/models/__init__.py +++ b/api_log/models/__init__.py @@ -1 +1,2 @@ +from . import api_log_collection from . import api_log diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py index b1d9dbb5f..71206ea8f 100644 --- a/api_log/models/api_log.py +++ b/api_log/models/api_log.py @@ -1,5 +1,6 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import base64 @@ -17,6 +18,20 @@ class APILog(models.Model): _description = "Log for API" _order = "id desc" + collection_ref = fields.Reference( + selection="_selection_collection_ref", + ) + collection_model = fields.Char( + compute="_compute_collection", + store=True, + index=True, + ) + collection_id = fields.Integer( + compute="_compute_collection", + store=True, + index=True, + ) + # Request request_url = fields.Char() request_method = fields.Char() @@ -62,6 +77,25 @@ class APILog(models.Model): compute="_compute_response_headers_derived", store=True ) + @api.model + def _selection_collection_ref(self): + return [] + + @api.depends( + "collection_ref", + ) + def _compute_collection(self): + for log in self: + collection = log.collection_ref + if collection: + collection_model = collection._name + collection_id = collection.id + else: + collection_model = False + collection_id = False + log.collection_model = collection_model + log.collection_id = collection_id + @api.model def _headers_hidden_keys(self): """Header keys that should not be logged. diff --git a/api_log/models/api_log_collection.py b/api_log/models/api_log_collection.py new file mode 100644 index 000000000..a094b6e0e --- /dev/null +++ b/api_log/models/api_log_collection.py @@ -0,0 +1,55 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class APILogCollection(models.AbstractModel): + _name = "api.log_collection.mixin" + _description = "Collection of API logs" + + log_requests = fields.Boolean( + help="Log requests in database.", + ) + + log_ids = fields.One2many( + comodel_name="api.log", + compute="_compute_log_ids", + string="Logs", + ) + + def _get_logs_domain(self): + """Domain to find the logs in `self`.""" + return [ + ("collection_model", "=", self._name), + ("collection_id", "in", self.ids), + ] + + def _compute_log_ids(self): + all_logs = self.env["api.log"].search_read( + domain=self._get_logs_domain(), + fields=[ + "collection_id", + ], + load=None, + ) + log_ids_by_collection_id = {} + for log in all_logs: + log_ids_by_collection_id.setdefault(log["collection_id"], []).append( + log["id"] + ) + + for collection in self: + collection.log_ids = log_ids_by_collection_id.get(collection.id) + + def action_logs(self): + return { + "type": "ir.actions.act_window", + "res_model": "api.log", + "name": "Logs", + "view_type": "form", + "view_mode": "tree,form", + "target": "current", + "domain": self._get_logs_domain(), + "context": dict(self.env.context), + } diff --git a/api_log/tests/common.py b/api_log/tests/common.py index e54316db6..e02138286 100644 --- a/api_log/tests/common.py +++ b/api_log/tests/common.py @@ -1,7 +1,10 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from odoo.tests.common import HttpCase -class CommonAPILog(HttpCase): +class Common(HttpCase): @classmethod def setUpClass(cls): super().setUpClass() diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index 52035a4c9..3a3868231 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -1,15 +1,16 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import requests from odoo.http import Request, Response -from odoo.addons.api_log.tests.common import CommonAPILog +from odoo.addons.api_log.tests.common import Common -class TestAPILog(CommonAPILog): +class TestAPILog(Common): def test_log_request(self): base_url = self.base_url() secret_api_key = "my-secret-api-key" diff --git a/api_log/views/api_log_views.xml b/api_log/views/api_log_views.xml index cc09bbdaf..4e8ba689f 100644 --- a/api_log/views/api_log_views.xml +++ b/api_log/views/api_log_views.xml @@ -45,6 +45,7 @@ + api.log + @@ -78,6 +80,7 @@ + + Date: Thu, 21 Aug 2025 11:47:19 +0200 Subject: [PATCH 04/24] [IMP] api_log: Inject Log record identifier --- api_log/README.rst | 4 ++++ api_log/models/api_log.py | 10 +++++++++- api_log/readme/DESCRIPTION.md | 3 +++ api_log/static/description/index.html | 3 +++ api_log/tests/test_api_log.py | 8 ++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/api_log/README.rst b/api_log/README.rst index 84b9d1e11..9cc98c41c 100644 --- a/api_log/README.rst +++ b/api_log/README.rst @@ -30,6 +30,10 @@ API Log This module allows to store request and response logs for any API. +When a response is logged, the header ``API_LOG_ENTRY_ID`` is injected +in the response header. This header stores the identifier of the log +record produced from the response. + **Table of contents** .. contents:: diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py index 71206ea8f..106ffa330 100644 --- a/api_log/models/api_log.py +++ b/api_log/models/api_log.py @@ -152,10 +152,16 @@ def log_request(self, request, override_log_values=None): log_request_values.update(override_log_values or {}) return self.sudo().create(log_request_values) + def _inject_log_entry(self, values_dict): + values_dict["API-Log-Entry-ID"] = str(self.id) + return values_dict + def _prepare_log_response(self, response): + self._inject_log_entry(response.headers) + headers_dict = self._headers_to_dict(response.headers) return { "response_status_code": response.status_code, - "response_headers": self._headers_to_dict(response.headers), + "response_headers": headers_dict, "response_body": response.data, "response_date": fields.Datetime.now(), "response_time": self._current_time(), @@ -166,8 +172,10 @@ def log_response(self, response): return self.sudo().write(log_response_values) def _prepare_log_exception(self, exception): + exception.headers = getattr(exception, "headers", {}) values = { "stack_trace": "".join(format_exception(exception)), + "response_headers": self._inject_log_entry(exception.headers), "response_body": str(exception), "response_date": fields.Datetime.now(), "response_time": self._current_time(), diff --git a/api_log/readme/DESCRIPTION.md b/api_log/readme/DESCRIPTION.md index 6018fc343..e620776ad 100644 --- a/api_log/readme/DESCRIPTION.md +++ b/api_log/readme/DESCRIPTION.md @@ -1 +1,4 @@ This module allows to store request and response logs for any API. + +When a response is logged, the header `API_LOG_ENTRY_ID` is injected in the response header. +This header stores the identifier of the log record produced from the response. diff --git a/api_log/static/description/index.html b/api_log/static/description/index.html index 6da2b40b8..60378ff49 100644 --- a/api_log/static/description/index.html +++ b/api_log/static/description/index.html @@ -371,6 +371,9 @@

API Log

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This module allows to store request and response logs for any API.

+

When a response is logged, the header API_LOG_ENTRY_ID is injected +in the response header. This header stores the identifier of the log +record produced from the response.

Table of contents

    diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index 3a3868231..45ddd5958 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -41,3 +41,11 @@ def test_log_response(self): log.log_response(response) self.assertEqual(log.response_status_code, 200) + self.assertEqual(log.response_headers["API-Log-Entry-ID"], str(log.id)) + self.assertEqual(response.headers["API-Log-Entry-ID"], str(log.id)) + + def test_log_exception(self): + log = self.log_model.create({}) + log.log_exception(Exception()) + + self.assertEqual(log.response_headers["API-Log-Entry-ID"], str(log.id)) From 8afac079422aacb2f71b994675cf72afe7c6842c Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 19 Mar 2026 10:07:21 +0100 Subject: [PATCH 05/24] [FIX] api_log: Name of empty log entry --- api_log/models/api_log.py | 5 +++-- api_log/tests/test_api_log.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py index 106ffa330..12ffd4477 100644 --- a/api_log/models/api_log.py +++ b/api_log/models/api_log.py @@ -6,6 +6,7 @@ import base64 import json import time +from datetime import date from traceback import format_exception from werkzeug.exceptions import HTTPException as WerkzeugHTTPException @@ -206,8 +207,8 @@ def log_exception(self, exception): def _compute_name(self): for log in self: log.name = ( - f"{log.request_date.isoformat()} - " - f"[{log.request_method}] {log.request_url}" + f"{(log.request_date or date.min).isoformat()} - " + f"[{log.request_method or ''}] {log.request_url or ''}" ) @api.depends("request_time", "response_time") diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index 45ddd5958..0c4f05596 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -35,6 +35,10 @@ def test_log_request(self): self.assertNotIn(secret_cookie, headers_values) self.assertIn(public_header_value, headers_values) + def test_name_empty_log(self): + log = self.log_model.create({}) + self.assertEqual("0001-01-01 - [] ", log.name) + def test_log_response(self): response = Response() log = self.log_model.create({}) From 736448b1d9dfeb436bec4d12234c1e3f9722f1f3 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 19 Mar 2026 10:43:54 +0100 Subject: [PATCH 06/24] [FIX] api_log: Log readonly exception --- api_log/README.rst | 8 +- api_log/i18n/api_log.pot | 246 ++++++++++++++++++++++++++ api_log/models/api_log.py | 3 +- api_log/static/description/icon.png | Bin 0 -> 10254 bytes api_log/static/description/index.html | 26 +-- api_log/tests/common.py | 12 ++ api_log/tests/test_api_log.py | 33 +++- 7 files changed, 314 insertions(+), 14 deletions(-) create mode 100644 api_log/i18n/api_log.pot create mode 100644 api_log/static/description/icon.png diff --git a/api_log/README.rst b/api_log/README.rst index 9cc98c41c..7fd4876a9 100644 --- a/api_log/README.rst +++ b/api_log/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ======= API Log ======= @@ -7,13 +11,13 @@ API Log !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !! source digest: sha256:a1948d1ff8b66338522f1c1a2988d5ab8b0a7ff97fcbe9589879bd67b30719ce !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github diff --git a/api_log/i18n/api_log.pot b/api_log/i18n/api_log.pot new file mode 100644 index 000000000..d8f682b12 --- /dev/null +++ b/api_log/i18n/api_log.pot @@ -0,0 +1,246 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * api_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: api_log +#: model:ir.actions.act_window,name:api_log.api_log_action +#: model:ir.ui.menu,name:api_log.menu_api_log +msgid "API Log" +msgstr "" + +#. module: api_log +#: model:res.groups,name:api_log.group_api_log +msgid "API Log Access" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__collection_id +#: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search +msgid "Collection" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__collection_model +msgid "Collection Model" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__collection_ref +msgid "Collection Ref" +msgstr "" + +#. module: api_log +#: model:ir.model,name:api_log.model_api_log_collection_mixin +msgid "Collection of API logs" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__create_uid +msgid "Created by" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__create_date +msgid "Created on" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__display_name +msgid "Display Name" +msgstr "" + +#. module: api_log +#: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search +msgid "Error" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__id +msgid "ID" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log____last_update +msgid "Last Modified on" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__write_date +msgid "Last Updated on" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log_collection_mixin__log_requests +msgid "Log Requests" +msgstr "" + +#. module: api_log +#: model:ir.model,name:api_log.model_api_log +msgid "Log for API" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,help:api_log.field_api_log_collection_mixin__log_requests +msgid "Log requests in database." +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log_collection_mixin__log_ids +msgid "Logs" +msgstr "" + +#. module: api_log +#: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search +msgid "Method" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__name +msgid "Name" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__referrer +msgid "Referrer" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_body +msgid "Request Body" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_b64 +msgid "Request Content" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_content_length +msgid "Request Content Length" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_content_type +msgid "Request Content Type" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_date +msgid "Request Date" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_headers +msgid "Request Headers" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_headers_preview +msgid "Request Headers Preview" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_method +msgid "Request Method" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_preview +msgid "Request Preview" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_time +msgid "Request Time" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__request_url +msgid "Request Url" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_body +msgid "Response Body" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_b64 +msgid "Response Content" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_content_length +msgid "Response Content Length" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_content_type +msgid "Response Content Type" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_date +msgid "Response Date" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_headers +msgid "Response Headers" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_headers_preview +msgid "Response Headers Preview" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_preview +msgid "Response Preview" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_status_code +msgid "Response Status Code" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__response_time +msgid "Response Time" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__stack_trace +msgid "Stack Trace" +msgstr "" + +#. module: api_log +#: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search +msgid "Success" +msgstr "" + +#. module: api_log +#: model:ir.model.fields,field_description:api_log.field_api_log__time +msgid "Time" +msgstr "" + +#. module: api_log +#: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search +msgid "Url" +msgstr "" diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py index 12ffd4477..c29187c35 100644 --- a/api_log/models/api_log.py +++ b/api_log/models/api_log.py @@ -173,7 +173,8 @@ def log_response(self, response): return self.sudo().write(log_response_values) def _prepare_log_exception(self, exception): - exception.headers = getattr(exception, "headers", {}) + if not hasattr(exception, "headers"): + exception.headers = dict() values = { "stack_trace": "".join(format_exception(exception)), "response_headers": self._inject_log_entry(exception.headers), diff --git a/api_log/static/description/icon.png b/api_log/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 -API Log +README.rst -
    -

    API Log

    +
    + + +Odoo Community Association + +
    +

    API Log

    -

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This module allows to store request and response logs for any API.

    When a response is logged, the header API_LOG_ENTRY_ID is injected in the response header. This header stores the identifier of the log @@ -387,7 +392,7 @@

    API Log

-

Bug Tracker

+

Bug Tracker

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

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Akretion
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -430,5 +435,6 @@

Maintainers

+ diff --git a/api_log/tests/common.py b/api_log/tests/common.py index e02138286..a0ee8e4cc 100644 --- a/api_log/tests/common.py +++ b/api_log/tests/common.py @@ -1,11 +1,23 @@ # Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import contextlib + +from odoo.http import Response from odoo.tests.common import HttpCase +from odoo.addons.website.tools import MockRequest + class Common(HttpCase): @classmethod def setUpClass(cls): super().setUpClass() cls.log_model = cls.env["api.log"] + + @contextlib.contextmanager + def _mock_request_exc_handling(self, *args, **kwargs): + """Enhance the standard mock of a request with exception handling.""" + with MockRequest(*args, **kwargs) as mock_request: + mock_request.dispatcher.handle_error = lambda exc: Response() + yield mock_request diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index 0c4f05596..1e139e920 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -50,6 +50,37 @@ def test_log_response(self): def test_log_exception(self): log = self.log_model.create({}) - log.log_exception(Exception()) + + with self._mock_request_exc_handling(self.env): + log.log_exception(Exception()) self.assertEqual(log.response_headers["API-Log-Entry-ID"], str(log.id)) + + def test_log_exception_readonly_headers(self): + """ + If the exception's headers are readonly, + they can be logged. + """ + # Arrange + log = self.log_model.create({}) + exc_headers = { + "answer": 42, + } + + class ReadOnlyException(Exception): + @property + def headers(self): + return exc_headers.copy() + + ro_exception = ReadOnlyException() + # pre-condition + with self.assertRaises(AttributeError) as ae: + ro_exception.headers = dict() + self.assertIn("can't set attribute", str(ae.exception)) + + # Act + with self._mock_request_exc_handling(self.env): + log.log_exception(ro_exception) + + # Assert + self.assertLess(exc_headers.items(), log.response_headers.items()) From bc61821ead1b3deb8721fb9e1be3092ed8236c4c Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:44:00 +0100 Subject: [PATCH 07/24] [IMP] api_log: pre-commit execution --- api_log/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 api_log/pyproject.toml diff --git a/api_log/pyproject.toml b/api_log/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/api_log/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" From da3210d329dfa8c5953ea6aab7e3009d511fb9df Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:44:09 +0100 Subject: [PATCH 08/24] [MIG] api_log: Migration to 17.0 --- api_log/README.rst | 16 ++++++--------- api_log/__manifest__.py | 2 +- api_log/static/description/index.html | 28 +++++++++++---------------- api_log/tests/test_api_log.py | 2 ++ api_log/views/api_log_views.xml | 13 +++++-------- 5 files changed, 25 insertions(+), 36 deletions(-) diff --git a/api_log/README.rst b/api_log/README.rst index 7fd4876a9..8ed72c6ef 100644 --- a/api_log/README.rst +++ b/api_log/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ======= API Log ======= @@ -17,17 +13,17 @@ API Log .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/16.0/api_log + :target: https://github.com/OCA/rest-framework/tree/17.0/api_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-api_log + :target: https://translation.odoo-community.org/projects/rest-framework-17-0/rest-framework-17-0-api_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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -49,7 +45,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -92,6 +88,6 @@ Current `maintainer `__: |maintainer-paradoxxxzero| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/api_log/__manifest__.py b/api_log/__manifest__.py index 84193a908..0ef813876 100644 --- a/api_log/__manifest__.py +++ b/api_log/__manifest__.py @@ -4,7 +4,7 @@ { "name": "API Log", - "version": "16.0.1.0.0", + "version": "17.0.1.0.0", "author": "Akretion, Odoo Community Association (OCA)", "license": "AGPL-3", "summary": "Log API requests in database", diff --git a/api_log/static/description/index.html b/api_log/static/description/index.html index 8cd4fa5ce..668b8b3d4 100644 --- a/api_log/static/description/index.html +++ b/api_log/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +API Log -
+
+

API Log

- - -Odoo Community Association - -
-

API Log

-

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This module allows to store request and response logs for any API.

When a response is logged, the header API_LOG_ENTRY_ID is injected in the response header. This header stores the identifier of the log @@ -392,23 +387,23 @@

API Log

-

Bug Tracker

+

Bug Tracker

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

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Akretion
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -430,11 +425,10 @@

Maintainers

promote its widespread use.

Current maintainer:

paradoxxxzero

-

This module is part of the OCA/rest-framework project on GitHub.

+

This module is part of the OCA/rest-framework project on GitHub.

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

-
diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index 1e139e920..3f500dcaa 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -6,6 +6,7 @@ import requests from odoo.http import Request, Response +from odoo.tests import HOST from odoo.addons.api_log.tests.common import Common @@ -25,6 +26,7 @@ def test_log_request(self): url=base_url, method="GET", ) + httprequest.remote_addr = HOST request = Request(httprequest) log = self.log_model.log_request(request) diff --git a/api_log/views/api_log_views.xml b/api_log/views/api_log_views.xml index 4e8ba689f..114044191 100644 --- a/api_log/views/api_log_views.xml +++ b/api_log/views/api_log_views.xml @@ -17,7 +17,7 @@
-
+

@@ -30,7 +30,7 @@ @@ -40,16 +40,13 @@ - + @@ -90,7 +87,7 @@ From 08aa23517ca73e2e63fd5603f254183d74e5e8be Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 25 Jun 2025 11:44:12 +0200 Subject: [PATCH 09/24] [ADD] api_log_mail: Notify user about logged exceptions --- api_log_mail/README.rst | 97 +++++ api_log_mail/__init__.py | 1 + api_log_mail/__manifest__.py | 19 + api_log_mail/i18n/api_log_mail.pot | 197 +++++++++ api_log_mail/models/__init__.py | 4 + api_log_mail/models/api_log.py | 41 ++ api_log_mail/models/api_log_collection.py | 21 + api_log_mail/readme/CONFIGURE.md | 1 + api_log_mail/readme/CONTRIBUTORS.md | 2 + api_log_mail/readme/DESCRIPTION.md | 1 + api_log_mail/static/description/icon.png | Bin 0 -> 10254 bytes api_log_mail/static/description/index.html | 441 +++++++++++++++++++++ 12 files changed, 825 insertions(+) create mode 100644 api_log_mail/README.rst create mode 100644 api_log_mail/__init__.py create mode 100644 api_log_mail/__manifest__.py create mode 100644 api_log_mail/i18n/api_log_mail.pot create mode 100644 api_log_mail/models/__init__.py create mode 100644 api_log_mail/models/api_log.py create mode 100644 api_log_mail/models/api_log_collection.py create mode 100644 api_log_mail/readme/CONFIGURE.md create mode 100644 api_log_mail/readme/CONTRIBUTORS.md create mode 100644 api_log_mail/readme/DESCRIPTION.md create mode 100644 api_log_mail/static/description/icon.png create mode 100644 api_log_mail/static/description/index.html diff --git a/api_log_mail/README.rst b/api_log_mail/README.rst new file mode 100644 index 000000000..9c6aca9f9 --- /dev/null +++ b/api_log_mail/README.rst @@ -0,0 +1,97 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +==================== +API Log notification +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:83771581b864da569541fa5883ae7a96848e85113aab2c285e9fd39a39350763 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/api_log_mail + :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-api_log_mail + :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| + +This module allows to create an activity when an exception is logged in +an API logs collection. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In any log collection that has logging enabled, insert an activity type +in "Error Activity type". + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* PyTech + +Contributors +------------ + +- `PyTech `__: + + - Simone Rubino + +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-SirPyTech| image:: https://github.com/SirPyTech.png?size=40px + :target: https://github.com/SirPyTech + :alt: SirPyTech + +Current `maintainer `__: + +|maintainer-SirPyTech| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/api_log_mail/__init__.py b/api_log_mail/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/api_log_mail/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/api_log_mail/__manifest__.py b/api_log_mail/__manifest__.py new file mode 100644 index 000000000..d334b7f9c --- /dev/null +++ b/api_log_mail/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "API Log notification", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "PyTech, Odoo Community Association (OCA)", + "maintainers": [ + "SirPyTech", + ], + "website": "https://github.com/OCA/rest-framework", + "summary": "Notify logged exceptions.", + "category": "Tools", + "depends": [ + "api_log", + "mail", + ], +} diff --git a/api_log_mail/i18n/api_log_mail.pot b/api_log_mail/i18n/api_log_mail.pot new file mode 100644 index 000000000..79f878664 --- /dev/null +++ b/api_log_mail/i18n/api_log_mail.pot @@ -0,0 +1,197 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * api_log_mail +# +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: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_ids +msgid "Activities" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_state +msgid "Activity State" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log_collection_mixin__api_log_mail_exception_activity_type_id +#: model:ir.model.fields,help:api_log_mail.field_fastapi_endpoint__api_log_mail_exception_activity_type_id +msgid "An activity of this type will be created when an error is logged." +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log_collection_mixin__api_log_mail_exception_template_id +#: model:ir.model.fields,help:api_log_mail.field_fastapi_endpoint__api_log_mail_exception_template_id +msgid "An email based on this template will be sent when an error is logged." +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: api_log_mail +#: model:ir.model,name:api_log_mail.model_api_log_collection_mixin +msgid "Collection of API logs" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log_collection_mixin__api_log_mail_exception_activity_type_id +#: model:ir.model.fields,field_description:api_log_mail.field_fastapi_endpoint__api_log_mail_exception_activity_type_id +msgid "Error Activity type" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log_collection_mixin__api_log_mail_exception_template_id +#: model:ir.model.fields,field_description:api_log_mail.field_fastapi_endpoint__api_log_mail_exception_template_id +msgid "Error E-mail Template" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__has_message +msgid "Has Message" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: api_log_mail +#: model:ir.model,name:api_log_mail.model_api_log +msgid "Log for API" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_ids +msgid "Messages" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,field_description:api_log_mail.field_api_log__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: api_log_mail +#: model:ir.model.fields,help:api_log_mail.field_api_log__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" diff --git a/api_log_mail/models/__init__.py b/api_log_mail/models/__init__.py new file mode 100644 index 000000000..13ae7379a --- /dev/null +++ b/api_log_mail/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import api_log_collection +from . import api_log diff --git a/api_log_mail/models/api_log.py b/api_log_mail/models/api_log.py new file mode 100644 index 000000000..36ca3ec90 --- /dev/null +++ b/api_log_mail/models/api_log.py @@ -0,0 +1,41 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class APILog(models.Model): + _name = "api.log" + _inherit = [ + "api.log", + "mail.activity.mixin", + # mail.thread is needed + # because message_subscribe is called + # during activity creation + "mail.thread", + ] + _mail_post_access = "read" # Access required to open an activity + + @api.model + def log_request(self, request, override_log_values=None): + return super( + APILog, + self.with_context(tracking_disable=True), + ).log_request(request, override_log_values=override_log_values) + + def _notify_api_log_exception(self): + if collection := self.collection_ref: + activity_type = collection.api_log_mail_exception_activity_type_id + if activity_type: + self.sudo().activity_schedule( + activity_type_id=activity_type.id, + ) + + mail_template = collection.api_log_mail_exception_template_id + if mail_template: + mail_template.sudo().send_mail(self.id) + + def log_exception(self, exception): + res = super().log_exception(exception) + self._notify_api_log_exception() + return res diff --git a/api_log_mail/models/api_log_collection.py b/api_log_mail/models/api_log_collection.py new file mode 100644 index 000000000..cf7a87336 --- /dev/null +++ b/api_log_mail/models/api_log_collection.py @@ -0,0 +1,21 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class APILogCollection(models.AbstractModel): + _inherit = "api.log_collection.mixin" + + api_log_mail_exception_template_id = fields.Many2one( + comodel_name="mail.template", + domain=[("model_id.model", "=", "api.log")], + string="Error E-mail Template", + help="An email based on this template will be sent when an error is logged.", + ) + api_log_mail_exception_activity_type_id = fields.Many2one( + comodel_name="mail.activity.type", + domain=[("res_model", "=", "api.log")], + string="Error Activity type", + help="An activity of this type will be created when an error is logged.", + ) diff --git a/api_log_mail/readme/CONFIGURE.md b/api_log_mail/readme/CONFIGURE.md new file mode 100644 index 000000000..8c16db49d --- /dev/null +++ b/api_log_mail/readme/CONFIGURE.md @@ -0,0 +1 @@ +In any log collection that has logging enabled, insert an activity type in "Error Activity type". diff --git a/api_log_mail/readme/CONTRIBUTORS.md b/api_log_mail/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..6e720b67d --- /dev/null +++ b/api_log_mail/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/api_log_mail/readme/DESCRIPTION.md b/api_log_mail/readme/DESCRIPTION.md new file mode 100644 index 000000000..f1207db61 --- /dev/null +++ b/api_log_mail/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to create an activity when an exception is logged in an API logs collection. diff --git a/api_log_mail/static/description/icon.png b/api_log_mail/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

API Log notification

+ +

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This module allows to create an activity when an exception is logged in +an API logs collection.

+

Table of contents

+ +
+

Configuration

+

In any log collection that has logging enabled, insert an activity type +in “Error Activity type”.

+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • PyTech
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

SirPyTech

+

This module is part of the OCA/rest-framework project on GitHub.

+

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

+
+
+
+
+ + From 99b05668a45ca1d11f2f1f91a088ce349e7c879d Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:44:57 +0100 Subject: [PATCH 10/24] [IMP] api_log_mail: pre-commit execution --- api_log_mail/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 api_log_mail/pyproject.toml diff --git a/api_log_mail/pyproject.toml b/api_log_mail/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/api_log_mail/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" From ee45ea0940b7f75ddeaaa65b01d572b7886e879e Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:45:04 +0100 Subject: [PATCH 11/24] [MIG] api_log_mail: Migration to 17.0 --- api_log_mail/README.rst | 16 +++++------- api_log_mail/__manifest__.py | 2 +- api_log_mail/static/description/index.html | 30 +++++++++------------- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/api_log_mail/README.rst b/api_log_mail/README.rst index 9c6aca9f9..0823a5515 100644 --- a/api_log_mail/README.rst +++ b/api_log_mail/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ==================== API Log notification ==================== @@ -17,17 +13,17 @@ API Log notification .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/16.0/api_log_mail + :target: https://github.com/OCA/rest-framework/tree/17.0/api_log_mail :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-api_log_mail + :target: https://translation.odoo-community.org/projects/rest-framework-17-0/rest-framework-17-0-api_log_mail :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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -52,7 +48,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -92,6 +88,6 @@ Current `maintainer `__: |maintainer-SirPyTech| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/api_log_mail/__manifest__.py b/api_log_mail/__manifest__.py index d334b7f9c..3a5de78b3 100644 --- a/api_log_mail/__manifest__.py +++ b/api_log_mail/__manifest__.py @@ -3,7 +3,7 @@ { "name": "API Log notification", - "version": "16.0.1.0.0", + "version": "17.0.1.0.0", "license": "AGPL-3", "author": "PyTech, Odoo Community Association (OCA)", "maintainers": [ diff --git a/api_log_mail/static/description/index.html b/api_log_mail/static/description/index.html index ec1a7e7b8..f5f83e50b 100644 --- a/api_log_mail/static/description/index.html +++ b/api_log_mail/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +API Log notification -
+
+

API Log notification

- - -Odoo Community Association - -
-

API Log notification

-

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This module allows to create an activity when an exception is logged in an API logs collection.

Table of contents

@@ -391,28 +386,28 @@

API Log notification

-

Configuration

+

Configuration

In any log collection that has logging enabled, insert an activity type in “Error Activity type”.

-

Bug Tracker

+

Bug Tracker

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

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • PyTech
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -431,11 +426,10 @@

Maintainers

promote its widespread use.

Current maintainer:

SirPyTech

-

This module is part of the OCA/rest-framework project on GitHub.

+

This module is part of the OCA/rest-framework project on GitHub.

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

-
From 83041603028349b5f1feefab00fd888a4fb45206 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 15 Jul 2025 10:33:50 +0200 Subject: [PATCH 12/24] [ADD] fastapi_log --- fastapi_log/README.rst | 99 +++++ fastapi_log/__init__.py | 2 + fastapi_log/__manifest__.py | 23 + fastapi_log/fastapi_dispatcher.py | 76 ++++ fastapi_log/models/__init__.py | 2 + fastapi_log/models/fastapi_endpoint.py | 35 ++ fastapi_log/models/fastapi_log.py | 227 ++++++++++ fastapi_log/readme/CONTRIBUTORS.md | 1 + fastapi_log/readme/DESCRIPTION.md | 3 + fastapi_log/readme/USAGE.md | 6 + fastapi_log/security/ir_model_access.xml | 17 + fastapi_log/security/res_groups.xml | 17 + fastapi_log/static/description/index.html | 438 +++++++++++++++++++ fastapi_log/tests/__init__.py | 1 + fastapi_log/tests/test_fastapi_log.py | 161 +++++++ fastapi_log/views/fastapi_endpoint_views.xml | 42 ++ fastapi_log/views/fastapi_log_views.xml | 124 ++++++ 17 files changed, 1274 insertions(+) create mode 100644 fastapi_log/README.rst create mode 100644 fastapi_log/__init__.py create mode 100644 fastapi_log/__manifest__.py create mode 100644 fastapi_log/fastapi_dispatcher.py create mode 100644 fastapi_log/models/__init__.py create mode 100644 fastapi_log/models/fastapi_endpoint.py create mode 100644 fastapi_log/models/fastapi_log.py create mode 100644 fastapi_log/readme/CONTRIBUTORS.md create mode 100644 fastapi_log/readme/DESCRIPTION.md create mode 100644 fastapi_log/readme/USAGE.md create mode 100644 fastapi_log/security/ir_model_access.xml create mode 100644 fastapi_log/security/res_groups.xml create mode 100644 fastapi_log/static/description/index.html create mode 100644 fastapi_log/tests/__init__.py create mode 100644 fastapi_log/tests/test_fastapi_log.py create mode 100644 fastapi_log/views/fastapi_endpoint_views.xml create mode 100644 fastapi_log/views/fastapi_log_views.xml diff --git a/fastapi_log/README.rst b/fastapi_log/README.rst new file mode 100644 index 000000000..6464683d2 --- /dev/null +++ b/fastapi_log/README.rst @@ -0,0 +1,99 @@ +=========== +Fastapi Log +=========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_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-fastapi_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| + +This module allows an endpoint to activate full request logging in a +database model. + +It is useful to debug production issues or to monitor the usage of a +specific endpoint. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To activate logging for an endpoint, you have to check the +``Log Requests`` checkbox in the endpoint's configuration. This will log +all requests and responses for that endpoint. + +A smart button will be displayed in the endpoint's form view to access +the endpoint logs. A global log view is also available in the +``FastAPI Logs`` menu. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com + +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-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_log/__init__.py b/fastapi_log/__init__.py new file mode 100644 index 000000000..d54296502 --- /dev/null +++ b/fastapi_log/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import fastapi_dispatcher diff --git a/fastapi_log/__manifest__.py b/fastapi_log/__manifest__.py new file mode 100644 index 000000000..c10eaf057 --- /dev/null +++ b/fastapi_log/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Fastapi Log", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "summary": "Log Fastapi requests in database", + "category": "Tools", + "depends": ["fastapi"], + "website": "https://github.com/OCA/rest-framework", + "data": [ + "security/res_groups.xml", + "security/ir_model_access.xml", + "views/fastapi_endpoint_views.xml", + "views/fastapi_log_views.xml", + ], + "maintainers": ["paradoxxxzero"], + "demo": [], + "installable": True, + "license": "AGPL-3", +} diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py new file mode 100644 index 000000000..3e0a68f26 --- /dev/null +++ b/fastapi_log/fastapi_dispatcher.py @@ -0,0 +1,76 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import registry, tools +from odoo.http import _dispatchers + +from odoo.addons.fastapi.fastapi_dispatcher import ( + FastApiDispatcher as BaseFastApiDispatcher, +) + +_logger = logging.getLogger(__name__) + + +# Inherit from last registered fastapi dispatcher +# This handles multiple overload of dispatchers +class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)): + routing_type = "fastapi" + + def dispatch(self, endpoint, args): + self.request.params = {} + environ = self._get_environ() + root_path = "/" + environ["PATH_INFO"].split("/")[1] + fastapi_endpoint = ( + self.request.env["fastapi.endpoint"] + .sudo() + .search([("root_path", "=", root_path)]) + ) + if fastapi_endpoint.log_requests: + log = None + try: + if tools.config["test_enable"]: + cr = getattr( + self.request.env.registry, "test_log_cr", self.request.env.cr + ) + else: + # Create an independent cursor + cr = registry(self.request.env.cr.dbname).cursor() + + env = self.request.env(cr=cr, su=True) + try: + # cf fastapi _get_environ + request = self.request.httprequest._HTTPRequest__wrapped + except AttributeError: + request = self.request.httprequest + + log = env["fastapi.log"].log_request( + request, environ, fastapi_endpoint.id + ) + except Exception as e: + _logger.warning("Failed to log request", exc_info=e) + + try: + response = super().dispatch(endpoint, args) + except Exception as e: + try: + log and log.log_exception(e) + except Exception as e: + _logger.warning("Failed to log exception", exc_info=e) + raise e + else: + try: + log and log.log_response(response) + except Exception as e: + _logger.warning("Failed to log response", exc_info=e) + finally: + if not tools.config["test_enable"]: + try: + cr.commit() # pylint: disable=E8102 + finally: + cr.close() + return response + + else: + return super().dispatch(endpoint, args) diff --git a/fastapi_log/models/__init__.py b/fastapi_log/models/__init__.py new file mode 100644 index 000000000..cddd4099d --- /dev/null +++ b/fastapi_log/models/__init__.py @@ -0,0 +1,2 @@ +from . import fastapi_endpoint +from . import fastapi_log diff --git a/fastapi_log/models/fastapi_endpoint.py b/fastapi_log/models/fastapi_endpoint.py new file mode 100644 index 000000000..e2649f812 --- /dev/null +++ b/fastapi_log/models/fastapi_endpoint.py @@ -0,0 +1,35 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + log_requests = fields.Boolean( + help="Log requests in database.", + ) + + fastapi_log_ids = fields.One2many( + "fastapi.log", + "endpoint_id", + string="Logs", + ) + + fastapi_log_count = fields.Integer( + compute="_compute_fastapi_log_count", + string="Logs Count", + ) + + @api.depends("fastapi_log_ids") + def _compute_fastapi_log_count(self): + data = self.env["fastapi.log"].read_group( + [("endpoint_id", "in", self.ids)], + ["endpoint_id"], + ["endpoint_id"], + ) + mapped_data = {m["endpoint_id"][0]: m["endpoint_id_count"] for m in data} + for record in self: + record.fastapi_log_count = mapped_data.get(record.id, 0) diff --git a/fastapi_log/models/fastapi_log.py b/fastapi_log/models/fastapi_log.py new file mode 100644 index 000000000..526d38213 --- /dev/null +++ b/fastapi_log/models/fastapi_log.py @@ -0,0 +1,227 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 +import json +import time +from traceback import format_exception + +from starlette.exceptions import HTTPException as StarletteHTTPException +from werkzeug.exceptions import HTTPException as WerkzeugHTTPException + +from odoo import api, fields, models + + +class FastapiLog(models.Model): + _name = "fastapi.log" + _description = "Fastapi Log" + _order = "id desc" + + endpoint_id = fields.Many2one( + "fastapi.endpoint", + string="Endpoint", + required=True, + ondelete="cascade", + index=True, + ) + + # Request + request_url = fields.Char() + request_method = fields.Char() + request_headers = fields.Json() + request_body = fields.Binary(attachment=False) + request_date = fields.Datetime() + request_time = fields.Float() + + # Response + response_status_code = fields.Integer() + response_headers = fields.Json() + response_body = fields.Binary(attachment=False) + response_date = fields.Datetime() + response_time = fields.Float() + + stack_trace = fields.Text() + + # Derived fields + name = fields.Char(compute="_compute_name", store=True) + time = fields.Float(compute="_compute_time", store=True) + request_preview = fields.Text(compute="_compute_request_preview") + response_preview = fields.Text(compute="_compute_response_preview") + request_b64 = fields.Binary( + string="Request Content", compute="_compute_request_b64" + ) + response_b64 = fields.Binary( + string="Response Content", compute="_compute_response_b64" + ) + request_headers_preview = fields.Text(compute="_compute_headers_preview") + response_headers_preview = fields.Text(compute="_compute_headers_preview") + request_content_type = fields.Char( + compute="_compute_request_headers_derived", store=True + ) + request_content_length = fields.Integer( + compute="_compute_request_headers_derived", store=True + ) + referrer = fields.Char(compute="_compute_request_headers_derived", store=True) + response_content_type = fields.Char( + compute="_compute_response_headers_derived", store=True + ) + response_content_length = fields.Integer( + compute="_compute_response_headers_derived", store=True + ) + + def _headers_to_dict(self, headers): + try: + return {key.lower(): value for key, value in headers.items()} + except AttributeError: + return {} + + def _current_time(self): + return time.time_ns() / 1e9 + + @api.model + def log_request(self, request, environ, endpoint_id): + body = None + # Be careful to not consume the request body if it hasn't been wrapped + stream = environ.get("wsgi.input") + if stream and stream.seekable(): + body = stream.read() + stream.seek(0) + + return self.create( + { + "endpoint_id": endpoint_id, + "request_url": request.url, + "request_method": request.method, + "request_headers": self._headers_to_dict(request.headers), + "request_body": body, + "request_date": fields.Datetime.now(), + "request_time": self._current_time(), + } + ) + + @api.model + def log_response(self, response): + return self.write( + { + "response_status_code": response.status_code, + "response_headers": self._headers_to_dict(response.headers), + "response_body": response.data, + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + + @api.model + def log_exception(self, exception): + self.write( + { + "stack_trace": "".join(format_exception(exception)), + } + ) + if isinstance(exception, StarletteHTTPException): + return self.write( + { + "response_status_code": exception.status_code, + "response_headers": self._headers_to_dict(exception.headers), + "response_body": exception.detail, + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + if isinstance(exception, WerkzeugHTTPException): + return self.write( + { + "response_status_code": exception.code, + "response_headers": self._headers_to_dict(exception.get_headers()), + "response_body": exception.get_body(), + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + try: + return self.log_response( + self.env.registry["ir.http"]._handle_error(exception) + ) + except Exception: + return self.write( + { + "response_status_code": 599, + "response_body": str(exception), + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + + @api.depends("request_url", "request_method", "request_date") + def _compute_name(self): + for record in self: + record.name = ( + f"{record.request_date.isoformat()} - " + f"[{record.request_method} {record.request_url}" + ) + + @api.depends("request_time", "response_time") + def _compute_time(self): + for record in self: + if record.request_time and record.response_time: + record.time = record.response_time - record.request_time + else: + record.time = 0 + + @api.depends("request_headers") + def _compute_request_headers_derived(self): + for record in self: + headers = record.request_headers or {} + record.request_content_type = headers.get("content-type", "") + record.request_content_length = headers.get("content-length", 0) + record.referrer = headers.get("referer", "") + + @api.depends("response_headers") + def _compute_response_headers_derived(self): + for record in self: + headers = record.response_headers or {} + record.response_content_type = headers.get("content-type", "") + record.response_content_length = headers.get("content-length", 0) + + @api.depends("request_body") + def _compute_request_preview(self): + for record in self.with_context(bin_size=False): + record.request_preview = record._body_preview(record.request_body) + + @api.depends("response_body") + def _compute_response_preview(self): + for record in self.with_context(bin_size=False): + record.response_preview = record._body_preview(record.response_body) + + def _body_preview(self, body): + # Display the first 1000 characters of the body if it's a text content + body_preview = False + if body: + try: + body_preview = body.decode("utf-8", errors="ignore") + if len(body_preview) > 1000: + body_preview = body_preview[:1000] + "...\n(...)" + except UnicodeDecodeError: + body_preview = False + return body_preview + + @api.depends("request_headers", "response_headers") + def _compute_headers_preview(self): + for record in self: + record.request_headers_preview = record._headers_preview( + record.request_headers + ) + record.response_headers_preview = record._headers_preview( + record.response_headers + ) + + def _headers_preview(self, headers): + return json.dumps(headers, sort_keys=True, indent=4) if headers else False + + @api.depends("request_body") + def _compute_request_b64(self): + self.request_b64 = base64.b64encode(self.request_body or b"") + + @api.depends("response_body") + def _compute_response_b64(self): + self.response_b64 = base64.b64encode(self.response_body or b"") diff --git a/fastapi_log/readme/CONTRIBUTORS.md b/fastapi_log/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..328a37da8 --- /dev/null +++ b/fastapi_log/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Florian Mounier diff --git a/fastapi_log/readme/DESCRIPTION.md b/fastapi_log/readme/DESCRIPTION.md new file mode 100644 index 000000000..60edac6e4 --- /dev/null +++ b/fastapi_log/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module allows an endpoint to activate full request logging in a database model. + +It is useful to debug production issues or to monitor the usage of a specific endpoint. diff --git a/fastapi_log/readme/USAGE.md b/fastapi_log/readme/USAGE.md new file mode 100644 index 000000000..420859a01 --- /dev/null +++ b/fastapi_log/readme/USAGE.md @@ -0,0 +1,6 @@ +To activate logging for an endpoint, you have to check the `Log Requests` checkbox in +the endpoint's configuration. This will log all requests and responses for that +endpoint. + +A smart button will be displayed in the endpoint's form view to access the endpoint +logs. A global log view is also available in the `FastAPI Logs` menu. diff --git a/fastapi_log/security/ir_model_access.xml b/fastapi_log/security/ir_model_access.xml new file mode 100644 index 000000000..ea4cd5edf --- /dev/null +++ b/fastapi_log/security/ir_model_access.xml @@ -0,0 +1,17 @@ + + + + + Fastapi Log: Read access + + + + + + + + diff --git a/fastapi_log/security/res_groups.xml b/fastapi_log/security/res_groups.xml new file mode 100644 index 000000000..3eec366d1 --- /dev/null +++ b/fastapi_log/security/res_groups.xml @@ -0,0 +1,17 @@ + + + + + + Fastapi Log Access + + + + diff --git a/fastapi_log/static/description/index.html b/fastapi_log/static/description/index.html new file mode 100644 index 000000000..0a76e9f5d --- /dev/null +++ b/fastapi_log/static/description/index.html @@ -0,0 +1,438 @@ + + + + + +Fastapi Log + + + +
+

Fastapi Log

+ + +

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This module allows an endpoint to activate full request logging in a +database model.

+

It is useful to debug production issues or to monitor the usage of a +specific endpoint.

+

Table of contents

+ +
+

Usage

+

To activate logging for an endpoint, you have to check the +Log Requests checkbox in the endpoint’s configuration. This will log +all requests and responses for that endpoint.

+

A smart button will be displayed in the endpoint’s form view to access +the endpoint logs. A global log view is also available in the +FastAPI Logs menu.

+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

paradoxxxzero

+

This module is part of the OCA/rest-framework project on GitHub.

+

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

+
+
+
+ + diff --git a/fastapi_log/tests/__init__.py b/fastapi_log/tests/__init__.py new file mode 100644 index 000000000..41a525a04 --- /dev/null +++ b/fastapi_log/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_log diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py new file mode 100644 index 000000000..77df06ee9 --- /dev/null +++ b/fastapi_log/tests/test_fastapi_log.py @@ -0,0 +1,161 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import os +import threading +import unittest +from contextlib import contextmanager + +from odoo.sql_db import TestCursor +from odoo.tests.common import HttpCase, RecordCapturer + +from odoo.addons.fastapi.schemas import DemoExceptionType + +from fastapi import status + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") +class FastAPIEncryptedErrorsCase(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.write({"log_requests": True}) + lang = ( + cls.env["res.lang"] + .with_context(active_test=False) + .search([("code", "=", "fr_BE")]) + ) + lang.active = True + + def setUp(self): + super().setUp() + # Use a side test cursor to be able to get exception logs + reg = self.env.registry + reg.test_log_lock = threading.RLock() + reg.test_log_cr = TestCursor(reg._db.cursor(), reg.test_log_lock) + + def tearDown(self): + reg = self.env.registry + reg.test_log_cr.rollback() + reg.test_log_cr.close() + reg.test_log_cr = None + reg.test_log_lock = None + super().tearDown() + + @contextmanager + def log_capturer(self): + with RecordCapturer( + self.env(cr=self.env.registry.test_log_cr)["fastapi.log"], + [("endpoint_id", "=", self.fastapi_demo_app.id)], + ) as capturer: + yield capturer + + def test_no_log_if_disabled(self): + self.fastapi_demo_app.write({"log_requests": False}) + + with self.log_capturer() as capturer: + response = self.url_open("/fastapi_demo/demo", timeout=200) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertFalse(capturer.records) + + def test_log_simple(self): + with self.log_capturer() as capturer: + response = self.url_open("/fastapi_demo/demo", timeout=200) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(len(capturer.records), 1) + log = capturer.records[0] + self.assertTrue(log.request_url.endswith("/fastapi_demo/demo")) + self.assertEqual(log.request_method, "GET") + self.assertEqual(log.response_status_code, 200) + self.assertTrue(log.time > 0) + + def test_log_exception(self): + with self.log_capturer() as capturer: + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=User Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.assertEqual(len(capturer.records), 1) + log = capturer.records[0] + self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertEqual(log.request_method, "GET") + self.assertEqual(log.response_status_code, 400) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b"User Error", log.response_body) + self.assertIn("odoo.exceptions.UserError: User Error\n", log.stack_trace) + + def test_log_bare_exception(self): + with self.log_capturer() as capturer: + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.bare_exception.value}" + "&error_message=Internal Server Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual( + response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + self.assertEqual(len(capturer.records), 1) + log = capturer.records[0] + self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertEqual(log.request_method, "GET") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b"Internal Server Error", log.response_body) + self.assertIn("NotImplementedError: Internal Server Error\n", log.stack_trace) + + def test_log_retrying_post(self): + with self.log_capturer() as capturer: + nbr_retries = 2 + route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}" + response = self.url_open( + route, timeout=20, files={"file": ("test.txt", b"test")} + ) + self.assertEqual(response.status_code, 200) + self.assertDictEqual( + response.json(), {"retries": nbr_retries, "file": "test"} + ) + + self.assertEqual(len(capturer.records), 3) + log = capturer.records[0] + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 200) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b'"retries":2', log.response_body) + self.assertIn(b'"file":"test"', log.response_body) + self.assertFalse(log.stack_trace) + log = capturer.records[1] + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) + self.assertIn( + "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", + log.stack_trace, + ) + log = capturer.records[2] + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) + self.assertIn( + "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", + log.stack_trace, + ) diff --git a/fastapi_log/views/fastapi_endpoint_views.xml b/fastapi_log/views/fastapi_endpoint_views.xml new file mode 100644 index 000000000..7997cd9c0 --- /dev/null +++ b/fastapi_log/views/fastapi_endpoint_views.xml @@ -0,0 +1,42 @@ + + + + + + Fastapi Log + fastapi.log + tree,form + [('endpoint_id', '=', active_id)] + {'default_endpoint_id': active_id} + + + + + fastapi.endpoint + + + +
+ +
+ + + + + + +
+ +
diff --git a/fastapi_log/views/fastapi_log_views.xml b/fastapi_log/views/fastapi_log_views.xml new file mode 100644 index 000000000..021442ffc --- /dev/null +++ b/fastapi_log/views/fastapi_log_views.xml @@ -0,0 +1,124 @@ + + + + + Fastapi Log + fastapi.log + tree,form + + + + fastapi.log.form + fastapi.log + + + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + fastapi.log.tree + fastapi.log + + + + + + + + + + + + + + + + fastapi.log.search + fastapi.log + + + + + + + + + + + + + + + + + + +
From 0015a1e472bea839f98ceb5541a54a46331a3e9e Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Tue, 15 Apr 2025 11:50:18 +0200 Subject: [PATCH 13/24] [REF] fastapi_log: Extract common features to `api_log` This way other APIs might use the new module `api_log` to store logs. --- fastapi_log/README.rst | 2 +- fastapi_log/__manifest__.py | 7 +- fastapi_log/fastapi_dispatcher.py | 38 ++-- fastapi_log/models/__init__.py | 2 +- fastapi_log/models/api_log.py | 58 +++++ fastapi_log/models/fastapi_endpoint.py | 20 +- fastapi_log/models/fastapi_log.py | 227 ------------------- fastapi_log/security/ir_model_access.xml | 17 -- fastapi_log/security/res_groups.xml | 17 -- fastapi_log/tests/test_fastapi_log.py | 43 ++-- fastapi_log/views/fastapi_endpoint_views.xml | 10 +- fastapi_log/views/fastapi_log_views.xml | 128 +++-------- 12 files changed, 146 insertions(+), 423 deletions(-) create mode 100644 fastapi_log/models/api_log.py delete mode 100644 fastapi_log/models/fastapi_log.py delete mode 100644 fastapi_log/security/ir_model_access.xml delete mode 100644 fastapi_log/security/res_groups.xml diff --git a/fastapi_log/README.rst b/fastapi_log/README.rst index 6464683d2..96624a991 100644 --- a/fastapi_log/README.rst +++ b/fastapi_log/README.rst @@ -71,7 +71,7 @@ Authors Contributors ------------ -- Florian Mounier florian.mounier@akretion.com +- Florian Mounier florian.mounier@akretion.com Maintainers ----------- diff --git a/fastapi_log/__manifest__.py b/fastapi_log/__manifest__.py index c10eaf057..8334dc5c2 100644 --- a/fastapi_log/__manifest__.py +++ b/fastapi_log/__manifest__.py @@ -8,11 +8,12 @@ "author": "Akretion, Odoo Community Association (OCA)", "summary": "Log Fastapi requests in database", "category": "Tools", - "depends": ["fastapi"], + "depends": [ + "api_log", + "fastapi", + ], "website": "https://github.com/OCA/rest-framework", "data": [ - "security/res_groups.xml", - "security/ir_model_access.xml", "views/fastapi_endpoint_views.xml", "views/fastapi_log_views.xml", ], diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py index 3e0a68f26..6a762cd51 100644 --- a/fastapi_log/fastapi_dispatcher.py +++ b/fastapi_log/fastapi_dispatcher.py @@ -28,37 +28,30 @@ def dispatch(self, endpoint, args): .search([("root_path", "=", root_path)]) ) if fastapi_endpoint.log_requests: - log = None - try: - if tools.config["test_enable"]: - cr = getattr( - self.request.env.registry, "test_log_cr", self.request.env.cr - ) - else: - # Create an independent cursor - cr = registry(self.request.env.cr.dbname).cursor() - - env = self.request.env(cr=cr, su=True) - try: - # cf fastapi _get_environ - request = self.request.httprequest._HTTPRequest__wrapped - except AttributeError: - request = self.request.httprequest - - log = env["fastapi.log"].log_request( - request, environ, fastapi_endpoint.id + if tools.config["test_enable"]: + cr = getattr( + self.request.env.registry, "test_log_cr", self.request.env.cr ) + else: + # Create an independent cursor + cr = registry(self.request.env.cr.dbname).cursor() + + env = self.request.env(cr=cr, su=True) + request = self.request + try: + log = env["api.log"].log_request(request) except Exception as e: _logger.warning("Failed to log request", exc_info=e) + log = None try: response = super().dispatch(endpoint, args) - except Exception as e: + except Exception as response_exc: try: - log and log.log_exception(e) + log and log.log_exception(response_exc) except Exception as e: _logger.warning("Failed to log exception", exc_info=e) - raise e + raise response_exc else: try: log and log.log_response(response) @@ -71,6 +64,5 @@ def dispatch(self, endpoint, args): finally: cr.close() return response - else: return super().dispatch(endpoint, args) diff --git a/fastapi_log/models/__init__.py b/fastapi_log/models/__init__.py index cddd4099d..23ac9cf0b 100644 --- a/fastapi_log/models/__init__.py +++ b/fastapi_log/models/__init__.py @@ -1,2 +1,2 @@ +from . import api_log from . import fastapi_endpoint -from . import fastapi_log diff --git a/fastapi_log/models/api_log.py b/fastapi_log/models/api_log.py new file mode 100644 index 000000000..ee28db599 --- /dev/null +++ b/fastapi_log/models/api_log.py @@ -0,0 +1,58 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from starlette.exceptions import HTTPException as StarletteHTTPException + +from odoo import api, fields, models + + +class FastapiLog(models.Model): + _inherit = "api.log" + + fastapi_endpoint_id = fields.Many2one( + comodel_name="fastapi.endpoint", + string="Endpoint", + ondelete="cascade", + index=True, + ) + + @api.model + def _get_request_body(self, request): + # Be careful to not consume the request body if it hasn't been wrapped + dispatcher = request.dispatcher + if dispatcher.routing_type == "fastapi": + environ = dispatcher._get_environ() + stream = environ.get("wsgi.input") + if stream and stream.seekable(): + request_body = stream.read() + stream.seek(0) + else: + request_body = super()._get_request_body(request) + return request_body + + @api.model + def _prepare_log_request(self, request): + log_request_values = super()._prepare_log_request(request) + dispatcher = request.dispatcher + if dispatcher.routing_type == "fastapi": + environ = dispatcher._get_environ() + endpoint = ( + request.env["fastapi.endpoint"] + .sudo() + ._get_endpoint(environ["PATH_INFO"]) + ) + log_request_values["fastapi_endpoint_id"] = endpoint.id + return log_request_values + + def _prepare_log_exception(self, exception): + values = super()._prepare_log_exception(exception) + if isinstance(exception, StarletteHTTPException): + values.update( + { + "response_status_code": exception.status_code, + "response_headers": self._headers_to_dict(exception.headers), + "response_body": exception.detail, + } + ) + return values diff --git a/fastapi_log/models/fastapi_endpoint.py b/fastapi_log/models/fastapi_endpoint.py index e2649f812..62789fc4c 100644 --- a/fastapi_log/models/fastapi_endpoint.py +++ b/fastapi_log/models/fastapi_endpoint.py @@ -13,8 +13,8 @@ class FastapiEndpoint(models.Model): ) fastapi_log_ids = fields.One2many( - "fastapi.log", - "endpoint_id", + comodel_name="api.log", + inverse_name="fastapi_endpoint_id", string="Logs", ) @@ -25,11 +25,13 @@ class FastapiEndpoint(models.Model): @api.depends("fastapi_log_ids") def _compute_fastapi_log_count(self): - data = self.env["fastapi.log"].read_group( - [("endpoint_id", "in", self.ids)], - ["endpoint_id"], - ["endpoint_id"], + groups = self.env["api.log"].read_group( + [("fastapi_endpoint_id", "in", self.ids)], + ["fastapi_endpoint_id"], + ["fastapi_endpoint_id"], ) - mapped_data = {m["endpoint_id"][0]: m["endpoint_id_count"] for m in data} - for record in self: - record.fastapi_log_count = mapped_data.get(record.id, 0) + mapped_data = { + g["fastapi_endpoint_id"][0]: g["fastapi_endpoint_id_count"] for g in groups + } + for endpoint in self: + endpoint.fastapi_log_count = mapped_data.get(endpoint.id, 0) diff --git a/fastapi_log/models/fastapi_log.py b/fastapi_log/models/fastapi_log.py deleted file mode 100644 index 526d38213..000000000 --- a/fastapi_log/models/fastapi_log.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2025 Akretion (http://www.akretion.com). -# @author Florian Mounier -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import base64 -import json -import time -from traceback import format_exception - -from starlette.exceptions import HTTPException as StarletteHTTPException -from werkzeug.exceptions import HTTPException as WerkzeugHTTPException - -from odoo import api, fields, models - - -class FastapiLog(models.Model): - _name = "fastapi.log" - _description = "Fastapi Log" - _order = "id desc" - - endpoint_id = fields.Many2one( - "fastapi.endpoint", - string="Endpoint", - required=True, - ondelete="cascade", - index=True, - ) - - # Request - request_url = fields.Char() - request_method = fields.Char() - request_headers = fields.Json() - request_body = fields.Binary(attachment=False) - request_date = fields.Datetime() - request_time = fields.Float() - - # Response - response_status_code = fields.Integer() - response_headers = fields.Json() - response_body = fields.Binary(attachment=False) - response_date = fields.Datetime() - response_time = fields.Float() - - stack_trace = fields.Text() - - # Derived fields - name = fields.Char(compute="_compute_name", store=True) - time = fields.Float(compute="_compute_time", store=True) - request_preview = fields.Text(compute="_compute_request_preview") - response_preview = fields.Text(compute="_compute_response_preview") - request_b64 = fields.Binary( - string="Request Content", compute="_compute_request_b64" - ) - response_b64 = fields.Binary( - string="Response Content", compute="_compute_response_b64" - ) - request_headers_preview = fields.Text(compute="_compute_headers_preview") - response_headers_preview = fields.Text(compute="_compute_headers_preview") - request_content_type = fields.Char( - compute="_compute_request_headers_derived", store=True - ) - request_content_length = fields.Integer( - compute="_compute_request_headers_derived", store=True - ) - referrer = fields.Char(compute="_compute_request_headers_derived", store=True) - response_content_type = fields.Char( - compute="_compute_response_headers_derived", store=True - ) - response_content_length = fields.Integer( - compute="_compute_response_headers_derived", store=True - ) - - def _headers_to_dict(self, headers): - try: - return {key.lower(): value for key, value in headers.items()} - except AttributeError: - return {} - - def _current_time(self): - return time.time_ns() / 1e9 - - @api.model - def log_request(self, request, environ, endpoint_id): - body = None - # Be careful to not consume the request body if it hasn't been wrapped - stream = environ.get("wsgi.input") - if stream and stream.seekable(): - body = stream.read() - stream.seek(0) - - return self.create( - { - "endpoint_id": endpoint_id, - "request_url": request.url, - "request_method": request.method, - "request_headers": self._headers_to_dict(request.headers), - "request_body": body, - "request_date": fields.Datetime.now(), - "request_time": self._current_time(), - } - ) - - @api.model - def log_response(self, response): - return self.write( - { - "response_status_code": response.status_code, - "response_headers": self._headers_to_dict(response.headers), - "response_body": response.data, - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - - @api.model - def log_exception(self, exception): - self.write( - { - "stack_trace": "".join(format_exception(exception)), - } - ) - if isinstance(exception, StarletteHTTPException): - return self.write( - { - "response_status_code": exception.status_code, - "response_headers": self._headers_to_dict(exception.headers), - "response_body": exception.detail, - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - if isinstance(exception, WerkzeugHTTPException): - return self.write( - { - "response_status_code": exception.code, - "response_headers": self._headers_to_dict(exception.get_headers()), - "response_body": exception.get_body(), - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - try: - return self.log_response( - self.env.registry["ir.http"]._handle_error(exception) - ) - except Exception: - return self.write( - { - "response_status_code": 599, - "response_body": str(exception), - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - - @api.depends("request_url", "request_method", "request_date") - def _compute_name(self): - for record in self: - record.name = ( - f"{record.request_date.isoformat()} - " - f"[{record.request_method} {record.request_url}" - ) - - @api.depends("request_time", "response_time") - def _compute_time(self): - for record in self: - if record.request_time and record.response_time: - record.time = record.response_time - record.request_time - else: - record.time = 0 - - @api.depends("request_headers") - def _compute_request_headers_derived(self): - for record in self: - headers = record.request_headers or {} - record.request_content_type = headers.get("content-type", "") - record.request_content_length = headers.get("content-length", 0) - record.referrer = headers.get("referer", "") - - @api.depends("response_headers") - def _compute_response_headers_derived(self): - for record in self: - headers = record.response_headers or {} - record.response_content_type = headers.get("content-type", "") - record.response_content_length = headers.get("content-length", 0) - - @api.depends("request_body") - def _compute_request_preview(self): - for record in self.with_context(bin_size=False): - record.request_preview = record._body_preview(record.request_body) - - @api.depends("response_body") - def _compute_response_preview(self): - for record in self.with_context(bin_size=False): - record.response_preview = record._body_preview(record.response_body) - - def _body_preview(self, body): - # Display the first 1000 characters of the body if it's a text content - body_preview = False - if body: - try: - body_preview = body.decode("utf-8", errors="ignore") - if len(body_preview) > 1000: - body_preview = body_preview[:1000] + "...\n(...)" - except UnicodeDecodeError: - body_preview = False - return body_preview - - @api.depends("request_headers", "response_headers") - def _compute_headers_preview(self): - for record in self: - record.request_headers_preview = record._headers_preview( - record.request_headers - ) - record.response_headers_preview = record._headers_preview( - record.response_headers - ) - - def _headers_preview(self, headers): - return json.dumps(headers, sort_keys=True, indent=4) if headers else False - - @api.depends("request_body") - def _compute_request_b64(self): - self.request_b64 = base64.b64encode(self.request_body or b"") - - @api.depends("response_body") - def _compute_response_b64(self): - self.response_b64 = base64.b64encode(self.response_body or b"") diff --git a/fastapi_log/security/ir_model_access.xml b/fastapi_log/security/ir_model_access.xml deleted file mode 100644 index ea4cd5edf..000000000 --- a/fastapi_log/security/ir_model_access.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Fastapi Log: Read access - - - - - - - - diff --git a/fastapi_log/security/res_groups.xml b/fastapi_log/security/res_groups.xml deleted file mode 100644 index 3eec366d1..000000000 --- a/fastapi_log/security/res_groups.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Fastapi Log Access - - - - diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index 77df06ee9..c862669cd 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -7,15 +7,16 @@ from contextlib import contextmanager from odoo.sql_db import TestCursor -from odoo.tests.common import HttpCase, RecordCapturer +from odoo.tests.common import RecordCapturer +from odoo.addons.api_log.tests.common import CommonAPILog from odoo.addons.fastapi.schemas import DemoExceptionType from fastapi import status @unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") -class FastAPIEncryptedErrorsCase(HttpCase): +class FastAPIEncryptedErrorsCase(CommonAPILog): @classmethod def setUpClass(cls): super().setUpClass() @@ -47,8 +48,8 @@ def tearDown(self): @contextmanager def log_capturer(self): with RecordCapturer( - self.env(cr=self.env.registry.test_log_cr)["fastapi.log"], - [("endpoint_id", "=", self.fastapi_demo_app.id)], + self.env(cr=self.env.registry.test_log_cr)[self.log_model._name], + [("fastapi_endpoint_id", "=", self.fastapi_demo_app.id)], ) as capturer: yield capturer @@ -128,6 +129,18 @@ def test_log_retrying_post(self): ) self.assertEqual(len(capturer.records), 3) + for log in capturer.records[1:]: + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b"fake error", log.response_body) + self.assertIn( + "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", + log.stack_trace, + ) + log = capturer.records[0] self.assertIn("/fastapi_demo/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") @@ -137,25 +150,3 @@ def test_log_retrying_post(self): self.assertIn(b'"retries":2', log.response_body) self.assertIn(b'"file":"test"', log.response_body) self.assertFalse(log.stack_trace) - log = capturer.records[1] - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) - self.assertEqual(log.request_method, "POST") - self.assertEqual(log.response_status_code, 500) - self.assertTrue(log.time > 0) - self.assertTrue(log.response_body) - self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) - self.assertIn( - "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", - log.stack_trace, - ) - log = capturer.records[2] - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) - self.assertEqual(log.request_method, "POST") - self.assertEqual(log.response_status_code, 500) - self.assertTrue(log.time > 0) - self.assertTrue(log.response_body) - self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) - self.assertIn( - "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", - log.stack_trace, - ) diff --git a/fastapi_log/views/fastapi_endpoint_views.xml b/fastapi_log/views/fastapi_endpoint_views.xml index 7997cd9c0..bf90e7ea1 100644 --- a/fastapi_log/views/fastapi_endpoint_views.xml +++ b/fastapi_log/views/fastapi_endpoint_views.xml @@ -8,10 +8,10 @@ Fastapi Log - fastapi.log + api.log tree,form - [('endpoint_id', '=', active_id)] - {'default_endpoint_id': active_id} + [('fastapi_endpoint_id', '=', active_id)] + {'default_fastapi_endpoint_id': active_id} @@ -25,7 +25,7 @@ type="action" name="%(fastapi_log.fastapi_log_action_from_endpoint)s" icon="fa-book" - groups="fastapi_log.group_fastapi_log" + groups="api_log.group_api_log" attrs="{'invisible': [('fastapi_log_count', '=', 0)]}" > @@ -34,7 +34,7 @@ - + diff --git a/fastapi_log/views/fastapi_log_views.xml b/fastapi_log/views/fastapi_log_views.xml index 021442ffc..1f33e422f 100644 --- a/fastapi_log/views/fastapi_log_views.xml +++ b/fastapi_log/views/fastapi_log_views.xml @@ -5,115 +5,55 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> - - Fastapi Log - fastapi.log - tree,form - - - - fastapi.log.form - fastapi.log + + Add Fastapi fields to API log form view + api.log + -
- -
-

- -

-
- - - - - - - - - - - - - - - - - - - - - -
-
+ + +
- - fastapi.log.tree - fastapi.log + + Add Fastapi fields to API log tree view + api.log + - - - - - - - - - + + - - fastapi.log.search - fastapi.log + + Add Fastapi fields to API log search view + api.log + - - - - - + + + + - - - - - - - +
+ + Fastapi Logs + api.log + [ + ("fastapi_endpoint_id", "!=", False) + ] + tree,form + + Date: Mon, 28 Apr 2025 12:00:02 +0200 Subject: [PATCH 14/24] [FIX] fastapi_log: Manage multi-slash endpoints --- fastapi_log/fastapi_dispatcher.py | 3 +-- fastapi_log/tests/test_fastapi_log.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py index 6a762cd51..a870ed407 100644 --- a/fastapi_log/fastapi_dispatcher.py +++ b/fastapi_log/fastapi_dispatcher.py @@ -21,11 +21,10 @@ class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)): def dispatch(self, endpoint, args): self.request.params = {} environ = self._get_environ() - root_path = "/" + environ["PATH_INFO"].split("/")[1] fastapi_endpoint = ( self.request.env["fastapi.endpoint"] .sudo() - .search([("root_path", "=", root_path)]) + ._get_endpoint(environ["PATH_INFO"]) ) if fastapi_endpoint.log_requests: if tools.config["test_enable"]: diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index c862669cd..e75333dc5 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -21,6 +21,7 @@ class FastAPIEncryptedErrorsCase(CommonAPILog): def setUpClass(cls): super().setUpClass() cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app.root_path += "/test" cls.fastapi_demo_app._handle_registry_sync() cls.fastapi_demo_app.write({"log_requests": True}) lang = ( @@ -57,19 +58,19 @@ def test_no_log_if_disabled(self): self.fastapi_demo_app.write({"log_requests": False}) with self.log_capturer() as capturer: - response = self.url_open("/fastapi_demo/demo", timeout=200) + response = self.url_open("/fastapi_demo/test/demo", timeout=200) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertFalse(capturer.records) def test_log_simple(self): with self.log_capturer() as capturer: - response = self.url_open("/fastapi_demo/demo", timeout=200) + response = self.url_open("/fastapi_demo/test/demo", timeout=200) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(capturer.records), 1) log = capturer.records[0] - self.assertTrue(log.request_url.endswith("/fastapi_demo/demo")) + self.assertTrue(log.request_url.endswith("/fastapi_demo/test/demo")) self.assertEqual(log.request_method, "GET") self.assertEqual(log.response_status_code, 200) self.assertTrue(log.time > 0) @@ -77,7 +78,7 @@ def test_log_simple(self): def test_log_exception(self): with self.log_capturer() as capturer: route = ( - "/fastapi_demo/demo/exception?" + "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.user_error.value}" "&error_message=User Error" ) @@ -86,7 +87,7 @@ def test_log_exception(self): self.assertEqual(len(capturer.records), 1) log = capturer.records[0] - self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertIn("/fastapi_demo/test/demo/exception", log.request_url) self.assertEqual(log.request_method, "GET") self.assertEqual(log.response_status_code, 400) self.assertTrue(log.time > 0) @@ -97,7 +98,7 @@ def test_log_exception(self): def test_log_bare_exception(self): with self.log_capturer() as capturer: route = ( - "/fastapi_demo/demo/exception?" + "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.bare_exception.value}" "&error_message=Internal Server Error" ) @@ -108,7 +109,7 @@ def test_log_bare_exception(self): self.assertEqual(len(capturer.records), 1) log = capturer.records[0] - self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertIn("/fastapi_demo/test/demo/exception", log.request_url) self.assertEqual(log.request_method, "GET") self.assertEqual(log.response_status_code, 500) self.assertTrue(log.time > 0) @@ -119,7 +120,7 @@ def test_log_bare_exception(self): def test_log_retrying_post(self): with self.log_capturer() as capturer: nbr_retries = 2 - route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}" + route = f"/fastapi_demo/test/demo/retrying?nbr_retries={nbr_retries}" response = self.url_open( route, timeout=20, files={"file": ("test.txt", b"test")} ) @@ -130,7 +131,7 @@ def test_log_retrying_post(self): self.assertEqual(len(capturer.records), 3) for log in capturer.records[1:]: - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertIn("/fastapi_demo/test/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") self.assertEqual(log.response_status_code, 500) self.assertTrue(log.time > 0) @@ -142,7 +143,7 @@ def test_log_retrying_post(self): ) log = capturer.records[0] - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertIn("/fastapi_demo/test/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") self.assertEqual(log.response_status_code, 200) self.assertTrue(log.time > 0) From 79d2eabd8d08ee7c6e60fe865f23af10d1c56fc6 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 25 Jun 2025 11:42:48 +0200 Subject: [PATCH 15/24] [IMP] fastapi_log: Adapt to log collection --- fastapi_log/README.rst | 3 + fastapi_log/__manifest__.py | 2 +- fastapi_log/fastapi_dispatcher.py | 24 +++---- .../migrations/16.0.1.1.0/post-migration.py | 32 +++++++++ .../migrations/16.0.1.1.0/pre-migration.py | 20 ++++++ fastapi_log/models/api_log.py | 23 ++++--- fastapi_log/models/fastapi_endpoint.py | 37 ++--------- fastapi_log/readme/CONTRIBUTORS.md | 2 + fastapi_log/static/description/index.html | 4 ++ fastapi_log/tests/common.py | 35 ++++++++++ fastapi_log/tests/test_fastapi_log.py | 66 ++++++------------- fastapi_log/views/fastapi_endpoint_views.xml | 25 ++++--- fastapi_log/views/fastapi_log_views.xml | 43 +----------- 13 files changed, 162 insertions(+), 154 deletions(-) create mode 100644 fastapi_log/migrations/16.0.1.1.0/post-migration.py create mode 100644 fastapi_log/migrations/16.0.1.1.0/pre-migration.py create mode 100644 fastapi_log/tests/common.py diff --git a/fastapi_log/README.rst b/fastapi_log/README.rst index 96624a991..fdb54e937 100644 --- a/fastapi_log/README.rst +++ b/fastapi_log/README.rst @@ -72,6 +72,9 @@ Contributors ------------ - Florian Mounier florian.mounier@akretion.com +- `PyTech `__: + + - Simone Rubino Maintainers ----------- diff --git a/fastapi_log/__manifest__.py b/fastapi_log/__manifest__.py index 8334dc5c2..27938d00e 100644 --- a/fastapi_log/__manifest__.py +++ b/fastapi_log/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Fastapi Log", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "author": "Akretion, Odoo Community Association (OCA)", "summary": "Log Fastapi requests in database", "category": "Tools", diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py index a870ed407..ba5071083 100644 --- a/fastapi_log/fastapi_dispatcher.py +++ b/fastapi_log/fastapi_dispatcher.py @@ -1,9 +1,10 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + import logging -from odoo import registry, tools from odoo.http import _dispatchers from odoo.addons.fastapi.fastapi_dispatcher import ( @@ -27,16 +28,8 @@ def dispatch(self, endpoint, args): ._get_endpoint(environ["PATH_INFO"]) ) if fastapi_endpoint.log_requests: - if tools.config["test_enable"]: - cr = getattr( - self.request.env.registry, "test_log_cr", self.request.env.cr - ) - else: - # Create an independent cursor - cr = registry(self.request.env.cr.dbname).cursor() - - env = self.request.env(cr=cr, su=True) request = self.request + env = request.env(su=True) try: log = env["api.log"].log_request(request) except Exception as e: @@ -50,18 +43,17 @@ def dispatch(self, endpoint, args): log and log.log_exception(response_exc) except Exception as e: _logger.warning("Failed to log exception", exc_info=e) + else: + # Be sure to commit/save the exception's log + env.cr.commit() + raise response_exc else: try: log and log.log_response(response) except Exception as e: _logger.warning("Failed to log response", exc_info=e) - finally: - if not tools.config["test_enable"]: - try: - cr.commit() # pylint: disable=E8102 - finally: - cr.close() + return response else: return super().dispatch(endpoint, args) diff --git a/fastapi_log/migrations/16.0.1.1.0/post-migration.py b/fastapi_log/migrations/16.0.1.1.0/post-migration.py new file mode 100644 index 000000000..b95831de3 --- /dev/null +++ b/fastapi_log/migrations/16.0.1.1.0/post-migration.py @@ -0,0 +1,32 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + endpoint_id_column = openupgrade.get_legacy_name("fastapi_endpoint_id") + openupgrade.logged_query( + env.cr, + """ + UPDATE api_log SET + collection_id=%(endpoint_id_column)s, + collection_model='fastapi.endpoint', + collection_ref='fastapi.endpoint,' || %(endpoint_id_column)s + WHERE %(endpoint_id_column)s IS NOT NULL + """ + % { + "endpoint_id_column": endpoint_id_column, + }, + ) + openupgrade.logged_query( + env.cr, + """ + ALTER TABLE api_log + DROP COLUMN %(endpoint_id_column)s + """ + % { + "endpoint_id_column": endpoint_id_column, + }, + ) diff --git a/fastapi_log/migrations/16.0.1.1.0/pre-migration.py b/fastapi_log/migrations/16.0.1.1.0/pre-migration.py new file mode 100644 index 000000000..9cdaa9143 --- /dev/null +++ b/fastapi_log/migrations/16.0.1.1.0/pre-migration.py @@ -0,0 +1,20 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.copy_columns( + env.cr, + { + "api_log": [ + ( + "fastapi_endpoint_id", + None, + None, + ), + ], + }, + ) diff --git a/fastapi_log/models/api_log.py b/fastapi_log/models/api_log.py index ee28db599..086e656b4 100644 --- a/fastapi_log/models/api_log.py +++ b/fastapi_log/models/api_log.py @@ -1,21 +1,24 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from starlette.exceptions import HTTPException as StarletteHTTPException -from odoo import api, fields, models +from odoo import api, models class FastapiLog(models.Model): _inherit = "api.log" - fastapi_endpoint_id = fields.Many2one( - comodel_name="fastapi.endpoint", - string="Endpoint", - ondelete="cascade", - index=True, - ) + @api.model + def _selection_collection_ref(self): + collections = super()._selection_collection_ref() + fastapi_endpoint_model = self.env["fastapi.endpoint"] + collections.append( + (fastapi_endpoint_model._name, fastapi_endpoint_model._description) + ) + return collections @api.model def _get_request_body(self, request): @@ -42,7 +45,11 @@ def _prepare_log_request(self, request): .sudo() ._get_endpoint(environ["PATH_INFO"]) ) - log_request_values["fastapi_endpoint_id"] = endpoint.id + log_request_values["collection_ref"] = "%s,%s" % ( + endpoint._name, + endpoint.id, + ) + return log_request_values def _prepare_log_exception(self, exception): diff --git a/fastapi_log/models/fastapi_endpoint.py b/fastapi_log/models/fastapi_endpoint.py index 62789fc4c..4770dcf24 100644 --- a/fastapi_log/models/fastapi_endpoint.py +++ b/fastapi_log/models/fastapi_endpoint.py @@ -1,37 +1,14 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import models class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" - - log_requests = fields.Boolean( - help="Log requests in database.", - ) - - fastapi_log_ids = fields.One2many( - comodel_name="api.log", - inverse_name="fastapi_endpoint_id", - string="Logs", - ) - - fastapi_log_count = fields.Integer( - compute="_compute_fastapi_log_count", - string="Logs Count", - ) - - @api.depends("fastapi_log_ids") - def _compute_fastapi_log_count(self): - groups = self.env["api.log"].read_group( - [("fastapi_endpoint_id", "in", self.ids)], - ["fastapi_endpoint_id"], - ["fastapi_endpoint_id"], - ) - mapped_data = { - g["fastapi_endpoint_id"][0]: g["fastapi_endpoint_id_count"] for g in groups - } - for endpoint in self: - endpoint.fastapi_log_count = mapped_data.get(endpoint.id, 0) + _name = "fastapi.endpoint" + _inherit = [ + "api.log_collection.mixin", + "fastapi.endpoint", + ] diff --git a/fastapi_log/readme/CONTRIBUTORS.md b/fastapi_log/readme/CONTRIBUTORS.md index 328a37da8..1e935bfb5 100644 --- a/fastapi_log/readme/CONTRIBUTORS.md +++ b/fastapi_log/readme/CONTRIBUTORS.md @@ -1 +1,3 @@ - Florian Mounier +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/fastapi_log/static/description/index.html b/fastapi_log/static/description/index.html index 0a76e9f5d..b0b206a30 100644 --- a/fastapi_log/static/description/index.html +++ b/fastapi_log/static/description/index.html @@ -416,6 +416,10 @@

Authors

Contributors

diff --git a/fastapi_log/tests/common.py b/fastapi_log/tests/common.py new file mode 100644 index 000000000..186c8542e --- /dev/null +++ b/fastapi_log/tests/common.py @@ -0,0 +1,35 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager + +from odoo.tests.common import RecordCapturer + +from odoo.addons.api_log.tests.common import Common as CommonAPILog + + +class Common(CommonAPILog): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app.root_path += "/test" + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.write({"log_requests": True}) + lang = ( + cls.env["res.lang"] + .with_context(active_test=False) + .search([("code", "=", "fr_BE")]) + ) + lang.active = True + + @contextmanager + def log_capturer(self): + app = self.fastapi_demo_app + with RecordCapturer( + self.env[self.log_model._name], + [("collection_ref", "=", "%s,%s" % (app._name, app.id))], + ) as capturer: + yield capturer diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index e75333dc5..bcf7feb46 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -1,59 +1,19 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + import os -import threading import unittest -from contextlib import contextmanager - -from odoo.sql_db import TestCursor -from odoo.tests.common import RecordCapturer -from odoo.addons.api_log.tests.common import CommonAPILog from odoo.addons.fastapi.schemas import DemoExceptionType +from odoo.addons.fastapi_log.tests.common import Common from fastapi import status -@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") -class FastAPIEncryptedErrorsCase(CommonAPILog): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") - cls.fastapi_demo_app.root_path += "/test" - cls.fastapi_demo_app._handle_registry_sync() - cls.fastapi_demo_app.write({"log_requests": True}) - lang = ( - cls.env["res.lang"] - .with_context(active_test=False) - .search([("code", "=", "fr_BE")]) - ) - lang.active = True - - def setUp(self): - super().setUp() - # Use a side test cursor to be able to get exception logs - reg = self.env.registry - reg.test_log_lock = threading.RLock() - reg.test_log_cr = TestCursor(reg._db.cursor(), reg.test_log_lock) - - def tearDown(self): - reg = self.env.registry - reg.test_log_cr.rollback() - reg.test_log_cr.close() - reg.test_log_cr = None - reg.test_log_lock = None - super().tearDown() - - @contextmanager - def log_capturer(self): - with RecordCapturer( - self.env(cr=self.env.registry.test_log_cr)[self.log_model._name], - [("fastapi_endpoint_id", "=", self.fastapi_demo_app.id)], - ) as capturer: - yield capturer - +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "TestFastapiLog skipped") +class TestFastapiLog(Common): def test_no_log_if_disabled(self): self.fastapi_demo_app.write({"log_requests": False}) @@ -151,3 +111,19 @@ def test_log_retrying_post(self): self.assertIn(b'"retries":2', log.response_body) self.assertIn(b'"file":"test"', log.response_body) self.assertFalse(log.stack_trace) + + def test_collection_ref(self): + """The created log holds a reference to its endpoint and viceversa.""" + # Arrange + endpoint = self.fastapi_demo_app + # pre-condition + self.assertFalse(endpoint.log_ids) + + # Act + with self.log_capturer() as capturer: + self.url_open("/fastapi_demo/test/demo", timeout=200) + + # Assert + log = capturer.records[-1] + self.assertEqual(log.collection_ref, endpoint) + self.assertIn(log, endpoint.log_ids) diff --git a/fastapi_log/views/fastapi_endpoint_views.xml b/fastapi_log/views/fastapi_endpoint_views.xml index bf90e7ea1..ac50560e8 100644 --- a/fastapi_log/views/fastapi_endpoint_views.xml +++ b/fastapi_log/views/fastapi_endpoint_views.xml @@ -2,33 +2,32 @@ - - Fastapi Log - api.log - tree,form - [('fastapi_endpoint_id', '=', active_id)] - {'default_fastapi_endpoint_id': active_id} - - - fastapi.endpoint
+
diff --git a/fastapi_log/views/fastapi_log_views.xml b/fastapi_log/views/fastapi_log_views.xml index 1f33e422f..3c19ba737 100644 --- a/fastapi_log/views/fastapi_log_views.xml +++ b/fastapi_log/views/fastapi_log_views.xml @@ -2,54 +2,15 @@ - - Add Fastapi fields to API log form view - api.log - - - - - - - - - - Add Fastapi fields to API log tree view - api.log - - - - - - - - - - Add Fastapi fields to API log search view - api.log - - - - - - - - - - - Fastapi Logs api.log [ - ("fastapi_endpoint_id", "!=", False) + ("collection_model", "=", "fastapi.endpoint"), ] tree,form From 32cfbf9e6244a4385b1d19fdbf69721aea05b681 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Tue, 9 Sep 2025 12:06:24 +0200 Subject: [PATCH 16/24] [IMP] fastapi_log: Use dedicated cursor for logs Co-authored-by: Florian Mounier --- fastapi_log/fastapi_dispatcher.py | 64 ++++++++++++++++++--------- fastapi_log/tests/common.py | 27 ++++++++++- fastapi_log/tests/test_fastapi_log.py | 2 +- 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py index ba5071083..a6978bc57 100644 --- a/fastapi_log/fastapi_dispatcher.py +++ b/fastapi_log/fastapi_dispatcher.py @@ -4,7 +4,9 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging +from contextlib import contextmanager +from odoo import registry from odoo.http import _dispatchers from odoo.addons.fastapi.fastapi_dispatcher import ( @@ -19,6 +21,28 @@ class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)): routing_type = "fastapi" + @contextmanager + def _create_log_env(self, request_env): + request_registry = request_env.registry + if request_registry.in_test_mode(): + # During tests, use the dedicated test's cursor + cr = request_registry.test_log_cr + else: + # Create an independent cursor + # so the logs are committed despite any endpoint's exceptions + cr = registry(request_registry.db_name).cursor() + + try: + yield request_env(cr=cr, su=True) + finally: + # While executing tests, + # the cursor is already managed in the tests + if not request_registry.in_test_mode(): + try: + cr.commit() # pylint: disable=invalid-commit + finally: + cr.close() + def dispatch(self, endpoint, args): self.request.params = {} environ = self._get_environ() @@ -28,32 +52,28 @@ def dispatch(self, endpoint, args): ._get_endpoint(environ["PATH_INFO"]) ) if fastapi_endpoint.log_requests: - request = self.request - env = request.env(su=True) - try: - log = env["api.log"].log_request(request) - except Exception as e: - _logger.warning("Failed to log request", exc_info=e) - log = None - - try: - response = super().dispatch(endpoint, args) - except Exception as response_exc: + with self._create_log_env(self.request.env) as log_env: try: - log and log.log_exception(response_exc) + log = log_env["api.log"].log_request(self.request) except Exception as e: - _logger.warning("Failed to log exception", exc_info=e) - else: - # Be sure to commit/save the exception's log - env.cr.commit() + _logger.warning("Failed to log request", exc_info=e) + log = None - raise response_exc - else: try: - log and log.log_response(response) - except Exception as e: - _logger.warning("Failed to log response", exc_info=e) + response = super().dispatch(endpoint, args) + except Exception as response_exc: + try: + log and log.log_exception(response_exc) + except Exception as e: + _logger.warning("Failed to log exception", exc_info=e) + + raise response_exc + else: + try: + log and log.log_response(response) + except Exception as e: + _logger.warning("Failed to log response", exc_info=e) - return response + return response else: return super().dispatch(endpoint, args) diff --git a/fastapi_log/tests/common.py b/fastapi_log/tests/common.py index 186c8542e..013dba216 100644 --- a/fastapi_log/tests/common.py +++ b/fastapi_log/tests/common.py @@ -3,8 +3,10 @@ # Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import threading from contextlib import contextmanager +from odoo.sql_db import TestCursor from odoo.tests.common import RecordCapturer from odoo.addons.api_log.tests.common import Common as CommonAPILog @@ -25,11 +27,34 @@ def setUpClass(cls): ) lang.active = True + def setUp(self): + super().setUp() + # Use a side test cursor to be able to get exception logs + reg = self.env.registry + reg.test_log_lock = threading.RLock() + reg.test_log_cr = TestCursor(reg._db.cursor(), reg.test_log_lock) + + def tearDown(self): + reg = self.env.registry + reg.test_log_cr.rollback() + reg.test_log_cr.close() + reg.test_log_cr = None + reg.test_log_lock = None + super().tearDown() + + def _get_log_env(self): + return self.env(cr=self.env.registry.test_log_cr) + + def _get_log_env_records(self, records): + log_env = self._get_log_env() + return log_env[records._name].browse(records.ids) + @contextmanager def log_capturer(self): app = self.fastapi_demo_app + log_env = self._get_log_env() with RecordCapturer( - self.env[self.log_model._name], + log_env[self.log_model._name], [("collection_ref", "=", "%s,%s" % (app._name, app.id))], ) as capturer: yield capturer diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index bcf7feb46..5c6fcae1b 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -126,4 +126,4 @@ def test_collection_ref(self): # Assert log = capturer.records[-1] self.assertEqual(log.collection_ref, endpoint) - self.assertIn(log, endpoint.log_ids) + self.assertIn(log, log.collection_ref.log_ids) From 1d43e18d1770cba2466f6f189786bca02d90836c Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 20 Oct 2025 12:56:19 +0200 Subject: [PATCH 17/24] [IMP] fastapi_log: In tests, check Log identifier in response --- fastapi_log/README.rst | 8 +++- fastapi_log/i18n/fastapi_log.pot | 50 ++++++++++++++++++++++ fastapi_log/static/description/icon.png | Bin 0 -> 10254 bytes fastapi_log/static/description/index.html | 28 +++++++----- fastapi_log/tests/test_fastapi_log.py | 2 + 5 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 fastapi_log/i18n/fastapi_log.pot create mode 100644 fastapi_log/static/description/icon.png diff --git a/fastapi_log/README.rst b/fastapi_log/README.rst index fdb54e937..acb364f00 100644 --- a/fastapi_log/README.rst +++ b/fastapi_log/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + =========== Fastapi Log =========== @@ -7,13 +11,13 @@ Fastapi Log !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !! source digest: sha256:0a3d7815e8fb3a49d33bd30d2dc3da061167d4b1ff5e98b3995507d1040f2157 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github diff --git a/fastapi_log/i18n/fastapi_log.pot b/fastapi_log/i18n/fastapi_log.pot new file mode 100644 index 000000000..a36fae56e --- /dev/null +++ b/fastapi_log/i18n/fastapi_log.pot @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi_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: fastapi_log +#: model_terms:ir.ui.view,arch_db:fastapi_log.fastapi_endpoint_form_view +msgid "Logs" +msgstr "" + +#. module: fastapi_log +#: model:ir.model,name:fastapi_log.model_fastapi_endpoint +msgid "FastAPI Endpoint" +msgstr "" + +#. module: fastapi_log +#: model:ir.actions.act_window,name:fastapi_log.fastapi_log_action +#: model:ir.ui.menu,name:fastapi_log.menu_fastapi_log +msgid "Fastapi Logs" +msgstr "" + +#. module: fastapi_log +#: model:ir.model.fields,field_description:fastapi_log.field_fastapi_endpoint__log_requests +msgid "Log Requests" +msgstr "" + +#. module: fastapi_log +#: model:ir.model,name:fastapi_log.model_api_log +msgid "Log for API" +msgstr "" + +#. module: fastapi_log +#: model:ir.model.fields,help:fastapi_log.field_fastapi_endpoint__log_requests +msgid "Log requests in database." +msgstr "" + +#. module: fastapi_log +#: model:ir.model.fields,field_description:fastapi_log.field_fastapi_endpoint__log_ids +msgid "Logs" +msgstr "" diff --git a/fastapi_log/static/description/icon.png b/fastapi_log/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 zcmbt)WmufcvhH9Zc!C8B?l8#UE&&o;gF7=g3=D(IAOS+K1lK^25Zv7%L4sRw_uvvF z*qyAk?>c**=lnR&y+1yw{;I3Hy6Ua2{<d0kcR+VvBo; zA_X`>;1;xAPL9rQqFxd#f5{a^zW*uaW+r3+U{|fRunu`GZhy$X z8_|Zi{zd#vIokczl8Xh*4Wi@i0+C?Rg1AB5VOEg8B>buLFCi~r5DPd2ED7QP2>^LO zKpr7+?*I1bPaFSLLEa0l2$tj*;u8Qtc=&(RUc*VK@ zjIN{I--GfO@vl+&r^eqy_BZ3dndN_PDzMc*W^!?dIsWAWU@LBjBg6^f4F6*!-hUYh zY$Xb}gF8b0%S1Ac@c%Rs()UCiEu3v6SiFE>h_!{gBb-H2{e=wB5o!YkT0>#LKZFw$ z?CuD0Gvfsb(|XbVxx0AL0%`gG2X+6|f;jiTHU9shtjoW-{2!| zMN*WuOj6elhD4zqgjNpX>F#JP{)hAbenX<+FPr>7jXM&q{|x+pbj8cU<=>Ej zWE1_%qoFVzDAZB%g@v<+1ud%<#2E~ML11jOV5pUZoXktGmzB38%te^i-3o9i$lge>z>tBcK|P2K0H9w{l#|i%$~egM)Ys{q>p<9yaE*%v2cy1wXE{AXqG1_b znfyg@Fq*e@yC)^(@$R*j^E;skyEM6pmL$1ctg*mWiWM&q1{nj>E^)Odw$RPr zhjesSk}k}@-e_%uZTy0t_*TJD&6%*HV0KH>xE@oBex6CL@`Ty3nH_2OF#M?6j(j|9 znRKGSfp3Q2i+|>}w?>8g$>r`|OcvG5r;p)z8DO8+O>EvYQ=_~`p}9!ReUEjUnNL@6 z+C*aoo67(sd|7QgW54@V9Y8PnBW$Q+7ZsRFA}Vj*viA!yWUfb!s*yJi6JKsXZCH4j z*B%nJpad-DDvJ8d>xrxkkh6A}i7V3nULqHCiG~|)YY6{NE3M}c^s#PQhzhsJUf^QW zR+F;up-dN*!)M1ZYl@d0HoqfVD2PNiQcPdzq4NDKO!8mUl{!t*ntBg_+-+lRlI0~Lr>5v!PiQj|hD7B-YFIs~6hIY*R6USZA zlb}=UxqxpSzIsL3pPmiuixCN|3LFBd?0Ih8Y6GWQ;U>dkdXtQaQ&8H|TGAQbuHY=F z_R83&B{1_hP7L#$^eAe?GPB_83y#HZKTwD>e-@E2P>Gk$BBb9|Ivfmdp za~s>3=aj(;xmz8n)sI}uFO$|C>0CZbcTY$Bq6~L-Bc9=vl@X#0S~Q@j8iKzuPeQE_ zQSI)wNz~CvJ>!%QszoCfUm9}h^DL!WYAN|FtMO#kpDXq74sYC87(uvv*jiCjV?Ta& zgO1D0OP3TEN3YnBpD6GnmsEolzEbGM{&VlTz_)J(o{nl0+TmNt{xL%L6G&UR$^aYC zQOA#W7R%9JsC5oTZJE>_?!Ci}mNH{0ObyUd%Q!k%5J8Z`8sR!m`~|Taje`(bLD7=a z-{-=d7w;k@DIrgU{I@K}eN`>S**Lg<@ChAf$M(&kV9TLUixqFQ>YoYHrI!K#R6`S> z%?d5hQ@&;Gje<|uRQZb%Hhibocl9(buI?=0aZW{JYXx?ZS@Lr%G8L<d+riEi2~+{HfHK{K^VrGYNi{2-WJOiC>Pz?f*)cxKCl>1H1=$jb!^ zpmYw>eoiM0Hy7$xbbX_e5o*+{7T2&-t%-h4i7MMo;k|tSqQAeNkwHS9hWY#EV7r3| zTmOmN{;b9OUZpp`LP(I9Wo%R#$b6YdH7GD4*p6>a2N2A04pQ*n;INQMh%+mj;x7>S z_(H?uJ^n!r1)kJH1*s+%$al#?C^Cw{H@RA^QGB=Dubyc)XUaY>f`(VKTlIO-YNCp{1n zOl*>jT?Dtf5fD$DY-j&B*Xmn|2-u2OB zBL@-lFs5lhcQKXBR*cIXmi%~EJcc^5#Xpg!E^A6sXf1#$qJGRpmU~A zcdj-cvBfx(fIRAMU(1obztJR%I7v3R-%$#~r!0sS^I(iC*5i6296*88A7I=_JhU3p zya!aCti0R5*RFT%LW0R|;u&oJ6=P-c$le4J0bi}u!!@;xzao|l6fJ{;Mld9hGhrJg zr_B)=4yktp)yPB@tCC_L9h1>GzXD6DA!W7xt{1)8!07~gONkEWC8@y%lciB{9ojy) zWm$drJ_9uVJ>Q$-`@q%OM7_S>(K=__CGYB~@@mE^Z=eT|x0Rv?Z-N)LLWR zod*Zy3v)iMX@usPX-OKBDgC8yq?fMhqf8H)A&C)Hi29YFn!NVf5!J0-F{wC&L5-3`#id=4?=2>Zp6Pdu4N6#bG&atu7 z8IET&ciXy_Tp4YjMx3yIAbw#_e2#jgGJ~ogkv-|M7|%Gio%2@mnS89NKUOM#Bzg4_ z9e9oN;^m>G*#?)AawODi6YckRPmkSKD_4b4WFpj|@|eS!B0WN@?QscYzTH`~6e%iz z!z1>ps)CG37%(E=kZ_>re)@ODv^0^=rWU^*m;6M&gD10EYImO98JVabRe5{#wrogYUKPB@_(#e7Ej9_x;n1oHDj5GawU)A&1hWj|HzJB(q{vMTX>jOW;Jz zBsW&SqTaR7!NXXg_A}$XnFpg_n)Zi;{e9eb*k|b(y$a}12boJ7rqQXQpVhU8HxHTl zt8Ln!KLFyfq!%}hdMXle^qajw2g6S{z&7tQ6J(w9 z3+!HTO{_TqM{9o$RR~lKFf4b4(xLUP?QG;McNFQc_Yd_mig9Ejy9%q~Ye>rIn3};U z)w&1@QCK;cC(;x0G&YuSad+>{c@ZsFJcUdcs@PP-x{mrO)|6_#CjMlXsMJx;Cr?FF zVFrlt@$Z-Ll^*7d0#`5Uez@bb{Xn(BQLhScBhF!6+aIso0=l{PP7P(6-ru>nVy%AP z+|eZpY(ooMU7rtG$l#14v=Z?@ebOjm(A2)5k_${|wAA$oq+;42wiS78ezjgWWnTrF z`1!i2h{fM91aD8uxz?tZpE(PsL37e3$*I6%un5Bzzpn10p`j72R;3=Oaug_|Z(y)@ z9$SJN@-5d1tNIy0=7|d&_HAnDx!yDd-u#qmfuDh)0a_CVje{hvQz9rDFHJTpQ0Dg@ zGQ3t*gZlcFSXfx%OG@Cds&NDROxd^osY_)abmo^dKMUY!R~kGH%*;rutPF@Mx$zrv z6Q1soKnYYRW#;Bi-!H)>Br0<`y+Wy~p7_<>{ljuG`Dpje=v1x}-ND<)bWBr|<}v6B zkDTUZ^@VsH>CyR}ml4j2rB{}0q8eGwX>ExkI9yZN0)(P}$N(yi$AxmBY#Xj`(7zs{ zJbn2&jE`-*0lww_r;|fNaWm_xp;c9JHIv|RExZGKP%18qjgYa);`N-^VqXNVz{~)~ z?^&D;ouy!pKPy?%@xH`A zSR z7x%N3@o&{YEjfa|1;*eW_4TU{ zt;qCcY3Hj(<0DJuny*QL!y!StcG{>bhpUP%eVMq=1xcR>yZT8X9)1;rXOmQjPcANs zr>&Qb{rr66;s|4v3iGmQlMjr9j;G6pqNs%;TsyVNd3{i~hpDX8ugdcnd&UQJzj)rH zh>S6#n`cCJ9CwHv<2Ht$o`R5(h#r||VB?%J?s5W48;^o)b`Pi1^~}5{Y19lg{&W@LfHt*gc1`w$RfLrK{~H?A1$5 z;5v?AIhpN%gQsR6+Act9-3y z8>jCTMnWQq-^s3#Lb|WalgB$k3F>}lyCxs<2&A;LS0}s#<|hPx9kM#B+Lu2DiD_3P zelg;N!80(j@HNc2pXs}re%sHi+{aqBt~qUOy86?zN>7)yiCEJqy@2Gh#gzJE6j6Rx zBQK{77zW?gLWtQ20Dzntu16k9^N>DQ@Nmbx*mOg=F=k)8VJfM%y(Xu41;8YCz+@K| z9u7vhlT`BOnk_oMTeC;u@OhhoTeA`^34^iMihCLM_uVD>rI-9@4l7ocZl@DJ8FWZU zB0lRBIqkHj4#pE&mD(X!e!~;G$`7f47k* zOznM2@`&KM(|f5}sz)z%2}yJ5YmMj5Zwzr-W?v3R&@KuJ+l0zo==N@)nsbMHqHV}w z7#_ntMGCNM21RuH^SYG+RH0sHUsF2z7ams57@2xbPj0y5)8h+caqv@P^q!do+}>+X zzUBx|mikTawzXWYzJ4(AqAJpBF4ObmD_@gyg->oFGB6`k(8+?rFRV5P1yDkFM=8(c z%RI)iG(rKtq-^V%B_(R9;tk6WIzA?x@cESTXg zWYDBxkoNB5v6J8BP&n@HVtBNb@r+XYpjgub zR4oE*$ffXJuh2g8TCaLnpNoSxJ~Jx@ayx9z5Osa)=AI#bg^5eQb<6gpR%c+Qs#N*e z@XE4pAmjdI#0%pV7sIN>mNa^jTkd=<==2_#t-}9Ju&Z^|Lp$%B92@eN%=MRc)LK$% z@!XAg;dQ8bt=@ZNey7+a(dy^o;QKGP@Rb5NJYQRrGEC{J=FB(Irw-MAfoP(9RK;)&jlxSCT=W;ODCf($WqRFhqN#LR^qVhK zWhEp4`{Nnk;n0FHj}eNCZpRM`Y-@MIM&pvr7zQOZ3Ik5;CmZbR99b&22(!-07YNF) z$o0MKej-jnvQV39{TH4r2R5univa1{ASc|VOTi4c@`t2FId|xkh5typ-rdU;1j){adk@*+( zkHj{5B~eSy&HrPOOvl_FJ98)0V;^d`0-u0FTslgiLBQVGSTiSyu zgMGAu&R}SbNa-DgKJb?;fe3Qys$?=;5?V`eRiq*Kj$I`}Z*x4rC~eNM=DsOq(=nUW>(+7o@O8K-_U(X? zTyg032nXKax5W~SF5|eBj%r8Fa>i!ejC72*sd}zJ)t7Xy!gFvM`c4@*Iw>z$u)j_l zR-Uqxymg}>Ti>i%9j*4kwfC33i~kyIQ``n)r(L z!|H2*)Mwj4dk%e*L0tgFdW185>j4<7YwLXwcOsed`%6mS{+=&d@d!B}GkbDV*0 zNIWzW^|trz!&;qeI&mPiVDOUL70xpqVv0fpN9tjpu)@1LD9D<9}9{57j9!W$`zC6&i zl9lKkmPh`x)5+h>>JtiRNNBW5$_)%-)#+SVSGsjX2T=+SRX05>yJZd`1hyk<@{%1+ zDu^k>J$d*Qz6BZMwHx!@O**^Tx&fsHDw%$@J0nfj^je^Ihy*aIx{B(hkBvSvh46Z9 zRO)BjjXL_IHXKo~$4es=8Wxk;Y+&nVBCXA;=MVuLgVn8Mk(*y^+kP3f?Pr~4^A}hXj9UHS}qeI%XKD3KhHnkrNH0(Y20BWl&!Kfm`EVh2;i5C zpirU^K0nc2-I{cqvjZKVx z=&hH#-d=gDWjVE}cMNAPJf;#NYdQ=h`twjX6yquXuCNgGx1~uk{YHAmFpQF`ZLGC=~ukEyj?cFDI zH=@XvV#AY1EY4qb`y*;Ki>KuFB|2|toL7__Cr0S1Dl{s#y0=~7HSq~&7lpBc*VLua zvv3r&-LM*{hq%IYP7<@)dG-G$kMrZaqs(MYoZ zugEeJ@u(ip9rMoVtoFe;dF`^Br5x7v!rr5`hb5mJ#ocGqXHnm9m`yILjd0>UQSMv) z^v}l5^bM6RZ6M%{mkI) zHOoSp&dX)*xUt+kXscna#a`XxI;Ul2Sxa^i5sZc=(Q)oA^2-_;!pfYHAul+oA@Ilelm;rw@FYR+SIaWS?;_ zUdw<|qqaYq(nqu>rG48E9dYAoT6GH;QRuBYK1}W#C_Z_?7~k*pJ3?MzVt&rhZTsBy zw?nN$_Z>kimtwWcy`0?G#!)&7GjOcxCQps@p&ml8>~z(t=sjhR$6aFh!Vw5GA(lTh z5GM)jCwloa6a}7mdfqNYE7oi`Jv$m5>5qR%9eZ=)=a z+K4j5NpcDHHdepCS+P*{@o=yNp&TE(Sd4b0Notqso-Kt_mhDk1<-fa>T4KdY2N`U) zxu41vD%T&k$Gl?CW81%7r#-o1TZ0&PCcy}L4TPiV;sz`|S!&w8-s$rLdM zF&)>@`7=)65PWn#oi|8tXNb|((2ojf9d0fNZ^l7xY~dX~%*Xf-v2W-2n$i~s!4?H; z2qbQscFN21tqB{|x1+(^G~xQSrvX&Y;V-%?b1}zjBQX{GOFcVYTcwm>>}>6^HA=$x zn+z^Biv_5}0!#@7z1~YXJFCT2?D^jm+kH7jAqBo?M@ZdMl|2|66oLnSJXUOJtVLxe z0vH)N^t*qrjq=eFRMV>BFEfS)-2RzKlt973;d3D}4edwIE>kGc5-o=JV56ird)RlS z{Jg@0t-b#Ife80%!E~(7`qkZ8O~Q-8_{j7G&tqwX&&>^tm-#*{v7j-f1n0}mCR#7P z-4FkajD2$9?4Fc7-C_|0Z_G^bxIs%tWk|aFgSQ(qkM+5PRh=g&ZeAZg35$-kn~}_;~&fP-dCNCzg>{gyW!~LZpn?aZ~Va3~H0Ta)z z<4XPVk@;#%1S@fq<(2#8T04#8$mz>vM;(jek0>Qh!K%t5*4tU(fVYwD3Ri~=D!AmI zV$Dt#TEDX7{lpW%tF&DOlTO)vZodn_%wYu~)ZQ}Qo^cBbDHd{YajkzNxttQW>ST<^ z2~^xhB_y1sjIF5;xchvCn{QVugIE2eYZDZ!-Y-4lJdb34*k({@M zJ5!9Di^||~(IZ4iOoAbtggao+CaYvJynmB^;4r-tY2gS_*P!?U?hlEX;l+^*{%B2n z)|1j9wOHQQ^5Xha>{Cu8_w^8=#6;Dz7kU~RgTqn;ynDm6{xdlkf2vk0UK^oS3yVy4 zE+v&qnlYtPHBk#X&2}r7`@K`J@^e~Qm?iRJ*tbAaZDZTmB&mWMkZp7Kj7^kth#_uX z5z>gC(8Xz|Ie(+#&wiF3;Aey|Db(R*-U)!6;l_5@u?-$>j0SgEl5+c}Lfe-$p-dFH zB_$bC<)x6#A_2Uuo8=^l1@}vK!gvbF#b&MoH8ac3xMxUz$LFb8KU(x$YhtHanM_sw zYOFMBX2iNNSe&a}!;G9nv(tsW4@%3iQcqczOCF*JOBQ@4Orw=o?_vc(9$hfO`>U6& zyY_CUa9pASiJpmv`@oR!k;&$`h8!)$uS=}d-fPddfIdMDUW@%3y1LI(1Q=e$)sz(QC*E;Nfl99YTgk+|@jl`+iF?<_D?4YqV0Zl)lO8YWC@1ZWW^mi{5ePQN<~FQ2NMG$|K{py5akJa zkezmqhN)>MGMp$7=sOo2(7ppv``dCIwf&MaQQis7S596kkiw8Do(jO?EY4iJ4Hec6 z4Hymzu`w)cI9Pbq6GPtTP)x&Lmk;FT=ZCB4>(5}c0?;2l`p&?>&<;2(P8a3lOTNP# zdEzF5qDpkRR&PZC&cS{7xD@qV;(g5X%xI?m$9Q -Fastapi Log +README.rst -
-

Fastapi Log

+
+ + +Odoo Community Association + +
+

Fastapi Log

-

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This module allows an endpoint to activate full request logging in a database model.

It is useful to debug production issues or to monitor the usage of a @@ -388,7 +393,7 @@

Fastapi Log

-

Usage

+

Usage

To activate logging for an endpoint, you have to check the Log Requests checkbox in the endpoint’s configuration. This will log all requests and responses for that endpoint.

@@ -397,7 +402,7 @@

Usage

FastAPI Logs menu.

-

Bug Tracker

+

Bug Tracker

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

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Akretion
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -438,5 +443,6 @@

Maintainers

+
diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index 5c6fcae1b..c5d7bf169 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -47,6 +47,8 @@ def test_log_exception(self): self.assertEqual(len(capturer.records), 1) log = capturer.records[0] + self.assertEqual(response.headers["API-Log-Entry-ID"], str(log.id)) + self.assertIn("/fastapi_demo/test/demo/exception", log.request_url) self.assertEqual(log.request_method, "GET") self.assertEqual(log.response_status_code, 400) From f9f041bea7ac0544d2f47518bced2d57d666f84d Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:45:35 +0100 Subject: [PATCH 18/24] [IMP] fastapi_log: pre-commit execution --- fastapi_log/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastapi_log/pyproject.toml diff --git a/fastapi_log/pyproject.toml b/fastapi_log/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/fastapi_log/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" From 93422fce980f2bda627cf19bc8daac67408c3156 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:45:42 +0100 Subject: [PATCH 19/24] [MIG] fastapi_log: Migration to 17.0 --- fastapi_log/README.rst | 16 ++++------ fastapi_log/__manifest__.py | 2 +- .../migrations/16.0.1.1.0/post-migration.py | 32 ------------------- .../migrations/16.0.1.1.0/pre-migration.py | 20 ------------ fastapi_log/models/api_log.py | 5 +-- fastapi_log/static/description/index.html | 30 +++++++---------- fastapi_log/tests/common.py | 3 +- fastapi_log/tests/test_fastapi_log.py | 16 ++++++---- fastapi_log/views/fastapi_endpoint_views.xml | 6 +--- 9 files changed, 33 insertions(+), 97 deletions(-) delete mode 100644 fastapi_log/migrations/16.0.1.1.0/post-migration.py delete mode 100644 fastapi_log/migrations/16.0.1.1.0/pre-migration.py diff --git a/fastapi_log/README.rst b/fastapi_log/README.rst index acb364f00..f08a3f2f1 100644 --- a/fastapi_log/README.rst +++ b/fastapi_log/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - =========== Fastapi Log =========== @@ -17,17 +13,17 @@ Fastapi Log .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_log + :target: https://github.com/OCA/rest-framework/tree/17.0/fastapi_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-fastapi_log + :target: https://translation.odoo-community.org/projects/rest-framework-17-0/rest-framework-17-0-fastapi_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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -60,7 +56,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -101,6 +97,6 @@ Current `maintainer `__: |maintainer-paradoxxxzero| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_log/__manifest__.py b/fastapi_log/__manifest__.py index 27938d00e..196cdab98 100644 --- a/fastapi_log/__manifest__.py +++ b/fastapi_log/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Fastapi Log", - "version": "16.0.1.1.0", + "version": "17.0.1.0.0", "author": "Akretion, Odoo Community Association (OCA)", "summary": "Log Fastapi requests in database", "category": "Tools", diff --git a/fastapi_log/migrations/16.0.1.1.0/post-migration.py b/fastapi_log/migrations/16.0.1.1.0/post-migration.py deleted file mode 100644 index b95831de3..000000000 --- a/fastapi_log/migrations/16.0.1.1.0/post-migration.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - endpoint_id_column = openupgrade.get_legacy_name("fastapi_endpoint_id") - openupgrade.logged_query( - env.cr, - """ - UPDATE api_log SET - collection_id=%(endpoint_id_column)s, - collection_model='fastapi.endpoint', - collection_ref='fastapi.endpoint,' || %(endpoint_id_column)s - WHERE %(endpoint_id_column)s IS NOT NULL - """ - % { - "endpoint_id_column": endpoint_id_column, - }, - ) - openupgrade.logged_query( - env.cr, - """ - ALTER TABLE api_log - DROP COLUMN %(endpoint_id_column)s - """ - % { - "endpoint_id_column": endpoint_id_column, - }, - ) diff --git a/fastapi_log/migrations/16.0.1.1.0/pre-migration.py b/fastapi_log/migrations/16.0.1.1.0/pre-migration.py deleted file mode 100644 index 9cdaa9143..000000000 --- a/fastapi_log/migrations/16.0.1.1.0/pre-migration.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - openupgrade.copy_columns( - env.cr, - { - "api_log": [ - ( - "fastapi_endpoint_id", - None, - None, - ), - ], - }, - ) diff --git a/fastapi_log/models/api_log.py b/fastapi_log/models/api_log.py index 086e656b4..af7d16c4f 100644 --- a/fastapi_log/models/api_log.py +++ b/fastapi_log/models/api_log.py @@ -45,10 +45,7 @@ def _prepare_log_request(self, request): .sudo() ._get_endpoint(environ["PATH_INFO"]) ) - log_request_values["collection_ref"] = "%s,%s" % ( - endpoint._name, - endpoint.id, - ) + log_request_values["collection_ref"] = f"{endpoint._name},{endpoint.id}" return log_request_values diff --git a/fastapi_log/static/description/index.html b/fastapi_log/static/description/index.html index 95e25fdd1..8feb911df 100644 --- a/fastapi_log/static/description/index.html +++ b/fastapi_log/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Fastapi Log -
+
+

Fastapi Log

- - -Odoo Community Association - -
-

Fastapi Log

-

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This module allows an endpoint to activate full request logging in a database model.

It is useful to debug production issues or to monitor the usage of a @@ -393,7 +388,7 @@

Fastapi Log

-

Usage

+

Usage

To activate logging for an endpoint, you have to check the Log Requests checkbox in the endpoint’s configuration. This will log all requests and responses for that endpoint.

@@ -402,23 +397,23 @@

Usage

FastAPI Logs menu.

-

Bug Tracker

+

Bug Tracker

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

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Akretion
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -438,11 +433,10 @@

Maintainers

promote its widespread use.

Current maintainer:

paradoxxxzero

-

This module is part of the OCA/rest-framework project on GitHub.

+

This module is part of the OCA/rest-framework project on GitHub.

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

-
diff --git a/fastapi_log/tests/common.py b/fastapi_log/tests/common.py index 013dba216..072b9a725 100644 --- a/fastapi_log/tests/common.py +++ b/fastapi_log/tests/common.py @@ -38,6 +38,7 @@ def tearDown(self): reg = self.env.registry reg.test_log_cr.rollback() reg.test_log_cr.close() + reg.test_log_cr._cursor.close() reg.test_log_cr = None reg.test_log_lock = None super().tearDown() @@ -55,6 +56,6 @@ def log_capturer(self): log_env = self._get_log_env() with RecordCapturer( log_env[self.log_model._name], - [("collection_ref", "=", "%s,%s" % (app._name, app.id))], + [("collection_ref", "=", f"{app._name},{app.id}")], ) as capturer: yield capturer diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index c5d7bf169..4e1b584fc 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -6,6 +6,8 @@ import os import unittest +from odoo.tools import mute_logger + from odoo.addons.fastapi.schemas import DemoExceptionType from odoo.addons.fastapi_log.tests.common import Common @@ -36,7 +38,7 @@ def test_log_simple(self): self.assertTrue(log.time > 0) def test_log_exception(self): - with self.log_capturer() as capturer: + with self.log_capturer() as capturer, mute_logger("odoo.http"): route = ( "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.user_error.value}" @@ -58,7 +60,7 @@ def test_log_exception(self): self.assertIn("odoo.exceptions.UserError: User Error\n", log.stack_trace) def test_log_bare_exception(self): - with self.log_capturer() as capturer: + with self.log_capturer() as capturer, mute_logger("odoo.http"): route = ( "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.bare_exception.value}" @@ -91,8 +93,9 @@ def test_log_retrying_post(self): response.json(), {"retries": nbr_retries, "file": "test"} ) - self.assertEqual(len(capturer.records), 3) - for log in capturer.records[1:]: + logs = capturer.records.sorted() + self.assertEqual(len(logs), 3) + for log in logs[1:]: self.assertIn("/fastapi_demo/test/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") self.assertEqual(log.response_status_code, 500) @@ -100,11 +103,12 @@ def test_log_retrying_post(self): self.assertTrue(log.response_body) self.assertIn(b"fake error", log.response_body) self.assertIn( - "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", + "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: " + "fake error", log.stack_trace, ) - log = capturer.records[0] + log = logs[0] self.assertIn("/fastapi_demo/test/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") self.assertEqual(log.response_status_code, 200) diff --git a/fastapi_log/views/fastapi_endpoint_views.xml b/fastapi_log/views/fastapi_endpoint_views.xml index ac50560e8..fac4428cc 100644 --- a/fastapi_log/views/fastapi_endpoint_views.xml +++ b/fastapi_log/views/fastapi_endpoint_views.xml @@ -18,12 +18,8 @@ type="object" name="action_logs" icon="fa-book" - attrs="{ - 'invisible': [ - ('log_ids', '=', []), - ], - }" groups="api_log.group_api_log" + invisible="not log_ids" >
Logs From 2d23d9b445aaf956e03207b45d10b50e439ab77d Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Tue, 22 Apr 2025 13:16:56 +0200 Subject: [PATCH 20/24] [ADD] fastapi_log_mail --- fastapi_log_mail/README.rst | 93 ++++ fastapi_log_mail/__init__.py | 1 + fastapi_log_mail/__manifest__.py | 22 + fastapi_log_mail/models/__init__.py | 4 + fastapi_log_mail/models/api_log.py | 16 + fastapi_log_mail/models/fastapi_endpoint.py | 15 + fastapi_log_mail/readme/CONFIGURE.md | 1 + fastapi_log_mail/readme/CONTRIBUTORS.md | 2 + fastapi_log_mail/readme/DESCRIPTION.md | 1 + .../static/description/index.html | 435 ++++++++++++++++++ fastapi_log_mail/tests/__init__.py | 1 + .../tests/test_fastapi_log_mail.py | 50 ++ .../views/fastapi_endpoint_views.xml | 24 + 13 files changed, 665 insertions(+) create mode 100644 fastapi_log_mail/README.rst create mode 100644 fastapi_log_mail/__init__.py create mode 100644 fastapi_log_mail/__manifest__.py create mode 100644 fastapi_log_mail/models/__init__.py create mode 100644 fastapi_log_mail/models/api_log.py create mode 100644 fastapi_log_mail/models/fastapi_endpoint.py create mode 100644 fastapi_log_mail/readme/CONFIGURE.md create mode 100644 fastapi_log_mail/readme/CONTRIBUTORS.md create mode 100644 fastapi_log_mail/readme/DESCRIPTION.md create mode 100644 fastapi_log_mail/static/description/index.html create mode 100644 fastapi_log_mail/tests/__init__.py create mode 100644 fastapi_log_mail/tests/test_fastapi_log_mail.py create mode 100644 fastapi_log_mail/views/fastapi_endpoint_views.xml diff --git a/fastapi_log_mail/README.rst b/fastapi_log_mail/README.rst new file mode 100644 index 000000000..2f3303c13 --- /dev/null +++ b/fastapi_log_mail/README.rst @@ -0,0 +1,93 @@ +=========== +Fastapi Log +=========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_log_mail + :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-fastapi_log_mail + :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| + +This module allows to send an email when an exception occurs in an +endpoint. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In any endpoint that has logging enabled, insert an email template in +"Error E-mail Template". + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* PyTech + +Contributors +------------ + +- `PyTech `__: + + - Simone Rubino + +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-SirPyTech| image:: https://github.com/SirPyTech.png?size=40px + :target: https://github.com/SirPyTech + :alt: SirPyTech + +Current `maintainer `__: + +|maintainer-SirPyTech| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_log_mail/__init__.py b/fastapi_log_mail/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/fastapi_log_mail/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fastapi_log_mail/__manifest__.py b/fastapi_log_mail/__manifest__.py new file mode 100644 index 000000000..a402dce47 --- /dev/null +++ b/fastapi_log_mail/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fastapi Log", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "PyTech, Odoo Community Association (OCA)", + "maintainers": [ + "SirPyTech", + ], + "website": "https://github.com/OCA/rest-framework", + "summary": "Email exceptions of Endpoints.", + "category": "Tools", + "depends": [ + "fastapi_log", + "mail", + ], + "data": [ + "views/fastapi_endpoint_views.xml", + ], +} diff --git a/fastapi_log_mail/models/__init__.py b/fastapi_log_mail/models/__init__.py new file mode 100644 index 000000000..89f5ea517 --- /dev/null +++ b/fastapi_log_mail/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import api_log +from . import fastapi_endpoint diff --git a/fastapi_log_mail/models/api_log.py b/fastapi_log_mail/models/api_log.py new file mode 100644 index 000000000..257bd642f --- /dev/null +++ b/fastapi_log_mail/models/api_log.py @@ -0,0 +1,16 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import models + + +class FastapiLog(models.Model): + _inherit = "api.log" + + def log_exception(self, exception): + res = super().log_exception(exception) + mail_template = self.fastapi_endpoint_id.fastapi_log_mail_template_id + if mail_template: + mail_template.sudo().send_mail(self.id) + return res diff --git a/fastapi_log_mail/models/fastapi_endpoint.py b/fastapi_log_mail/models/fastapi_endpoint.py new file mode 100644 index 000000000..0aef2c454 --- /dev/null +++ b/fastapi_log_mail/models/fastapi_endpoint.py @@ -0,0 +1,15 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + fastapi_log_mail_template_id = fields.Many2one( + comodel_name="mail.template", + domain=[("model_id.model", "=", "api.log")], + string="Error E-mail Template", + help="Select the email template that will be sent when an error is logged.", + ) diff --git a/fastapi_log_mail/readme/CONFIGURE.md b/fastapi_log_mail/readme/CONFIGURE.md new file mode 100644 index 000000000..fd221d770 --- /dev/null +++ b/fastapi_log_mail/readme/CONFIGURE.md @@ -0,0 +1 @@ +In any endpoint that has logging enabled, insert an email template in "Error E-mail Template". \ No newline at end of file diff --git a/fastapi_log_mail/readme/CONTRIBUTORS.md b/fastapi_log_mail/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..6e720b67d --- /dev/null +++ b/fastapi_log_mail/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/fastapi_log_mail/readme/DESCRIPTION.md b/fastapi_log_mail/readme/DESCRIPTION.md new file mode 100644 index 000000000..8eccf80b8 --- /dev/null +++ b/fastapi_log_mail/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to send an email when an exception occurs in an endpoint. diff --git a/fastapi_log_mail/static/description/index.html b/fastapi_log_mail/static/description/index.html new file mode 100644 index 000000000..1ec0ff159 --- /dev/null +++ b/fastapi_log_mail/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +Fastapi Log + + + +
+

Fastapi Log

+ + +

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This module allows to send an email when an exception occurs in an +endpoint.

+

Table of contents

+ +
+

Configuration

+

In any endpoint that has logging enabled, insert an email template in +“Error E-mail Template”.

+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • PyTech
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

SirPyTech

+

This module is part of the OCA/rest-framework project on GitHub.

+

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

+
+
+
+ + diff --git a/fastapi_log_mail/tests/__init__.py b/fastapi_log_mail/tests/__init__.py new file mode 100644 index 000000000..0d3e465bc --- /dev/null +++ b/fastapi_log_mail/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_log_mail diff --git a/fastapi_log_mail/tests/test_fastapi_log_mail.py b/fastapi_log_mail/tests/test_fastapi_log_mail.py new file mode 100644 index 000000000..74cad8ba8 --- /dev/null +++ b/fastapi_log_mail/tests/test_fastapi_log_mail.py @@ -0,0 +1,50 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import os +import unittest + +from odoo.addons.api_log.tests.common import CommonAPILog +from odoo.addons.fastapi.schemas import DemoExceptionType +from odoo.addons.mail.tests.common import MailCase + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") +class FastAPIEncryptedErrorsCase(CommonAPILog, MailCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.log_requests = True + cls.fastapi_demo_app.fastapi_log_mail_template_id = cls.env[ + "mail.template" + ].create( + { + "name": "Test exception email template", + "model_id": cls.env.ref("api_log.model_api_log").id, + } + ) + + def test_endpoint_exception_send_email(self): + """If an endpoint has an email template, + when an exception occurs an email is sent using the configured template. + """ + # Arrange + mail_template = self.fastapi_demo_app.fastapi_log_mail_template_id + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=User Error" + ) + # pre-condition + self.assertTrue(mail_template) + + # Act + with self.mock_mail_gateway(): + self.url_open(route, timeout=200) + + # Assert + sent_email = self._filter_mail() + self.assertTrue(sent_email) diff --git a/fastapi_log_mail/views/fastapi_endpoint_views.xml b/fastapi_log_mail/views/fastapi_endpoint_views.xml new file mode 100644 index 000000000..fdd6deaaa --- /dev/null +++ b/fastapi_log_mail/views/fastapi_endpoint_views.xml @@ -0,0 +1,24 @@ + + + + + fastapi.endpoint + + + + + + + + From d6704115e141ffb97fd871336e5c330d79a5d67a Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 25 Jun 2025 11:46:38 +0200 Subject: [PATCH 21/24] [IMP] fastapi_log_mail: Adapt to log collection --- fastapi_log_mail/README.rst | 13 ++--- fastapi_log_mail/__init__.py | 1 - fastapi_log_mail/__manifest__.py | 8 +-- .../migrations/16.0.1.1.0/post-migration.py | 32 +++++++++++ .../migrations/16.0.1.1.0/pre-migration.py | 20 +++++++ fastapi_log_mail/models/__init__.py | 4 -- fastapi_log_mail/models/api_log.py | 16 ------ fastapi_log_mail/models/fastapi_endpoint.py | 15 ----- fastapi_log_mail/readme/CONFIGURE.md | 2 +- fastapi_log_mail/readme/DESCRIPTION.md | 2 +- .../static/description/index.html | 13 ++--- .../tests/test_fastapi_log_mail.py | 56 +++++++++++++++---- .../views/fastapi_endpoint_views.xml | 12 +++- 13 files changed, 126 insertions(+), 68 deletions(-) create mode 100644 fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py create mode 100644 fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py delete mode 100644 fastapi_log_mail/models/__init__.py delete mode 100644 fastapi_log_mail/models/api_log.py delete mode 100644 fastapi_log_mail/models/fastapi_endpoint.py diff --git a/fastapi_log_mail/README.rst b/fastapi_log_mail/README.rst index 2f3303c13..af99233ca 100644 --- a/fastapi_log_mail/README.rst +++ b/fastapi_log_mail/README.rst @@ -1,6 +1,6 @@ -=========== -Fastapi Log -=========== +======================== +FastAPI Log notification +======================== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -28,8 +28,8 @@ Fastapi Log |badge1| |badge2| |badge3| |badge4| |badge5| -This module allows to send an email when an exception occurs in an -endpoint. +This module allows to create an activity when an exception is logged in +a fastapi endpoint. **Table of contents** @@ -39,8 +39,7 @@ endpoint. Configuration ============= -In any endpoint that has logging enabled, insert an email template in -"Error E-mail Template". +Configure a fastapi endpoint as explained in ``api_log_mail``. Bug Tracker =========== diff --git a/fastapi_log_mail/__init__.py b/fastapi_log_mail/__init__.py index 0650744f6..e69de29bb 100644 --- a/fastapi_log_mail/__init__.py +++ b/fastapi_log_mail/__init__.py @@ -1 +0,0 @@ -from . import models diff --git a/fastapi_log_mail/__manifest__.py b/fastapi_log_mail/__manifest__.py index a402dce47..8bcbc0621 100644 --- a/fastapi_log_mail/__manifest__.py +++ b/fastapi_log_mail/__manifest__.py @@ -2,19 +2,19 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { - "name": "Fastapi Log", - "version": "16.0.1.0.0", + "name": "FastAPI Log notification", + "version": "16.0.1.1.0", "license": "AGPL-3", "author": "PyTech, Odoo Community Association (OCA)", "maintainers": [ "SirPyTech", ], "website": "https://github.com/OCA/rest-framework", - "summary": "Email exceptions of Endpoints.", + "summary": "Notify logged exceptions.", "category": "Tools", "depends": [ "fastapi_log", - "mail", + "api_log_mail", ], "data": [ "views/fastapi_endpoint_views.xml", diff --git a/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py b/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py new file mode 100644 index 000000000..1903d6e19 --- /dev/null +++ b/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py @@ -0,0 +1,32 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + template_id_column = openupgrade.get_legacy_name( + "fastapi_log_mail_template_id", + ) + openupgrade.logged_query( + env.cr, + """ + UPDATE fastapi_endpoint SET + api_log_mail_exception_template_id=%(template_id_column)s + WHERE %(template_id_column)s IS NOT NULL + """ + % { + "template_id_column": template_id_column, + }, + ) + openupgrade.logged_query( + env.cr, + """ + ALTER TABLE fastapi_endpoint + DROP COLUMN %(template_id_column)s + """ + % { + "template_id_column": template_id_column, + }, + ) diff --git a/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py b/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py new file mode 100644 index 000000000..71f919a98 --- /dev/null +++ b/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py @@ -0,0 +1,20 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.copy_columns( + env.cr, + { + "fastapi_endpoint": [ + ( + "fastapi_log_mail_template_id", + None, + None, + ), + ], + }, + ) diff --git a/fastapi_log_mail/models/__init__.py b/fastapi_log_mail/models/__init__.py deleted file mode 100644 index 89f5ea517..000000000 --- a/fastapi_log_mail/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from . import api_log -from . import fastapi_endpoint diff --git a/fastapi_log_mail/models/api_log.py b/fastapi_log_mail/models/api_log.py deleted file mode 100644 index 257bd642f..000000000 --- a/fastapi_log_mail/models/api_log.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - - -from odoo import models - - -class FastapiLog(models.Model): - _inherit = "api.log" - - def log_exception(self, exception): - res = super().log_exception(exception) - mail_template = self.fastapi_endpoint_id.fastapi_log_mail_template_id - if mail_template: - mail_template.sudo().send_mail(self.id) - return res diff --git a/fastapi_log_mail/models/fastapi_endpoint.py b/fastapi_log_mail/models/fastapi_endpoint.py deleted file mode 100644 index 0aef2c454..000000000 --- a/fastapi_log_mail/models/fastapi_endpoint.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" - - fastapi_log_mail_template_id = fields.Many2one( - comodel_name="mail.template", - domain=[("model_id.model", "=", "api.log")], - string="Error E-mail Template", - help="Select the email template that will be sent when an error is logged.", - ) diff --git a/fastapi_log_mail/readme/CONFIGURE.md b/fastapi_log_mail/readme/CONFIGURE.md index fd221d770..ca1622a8b 100644 --- a/fastapi_log_mail/readme/CONFIGURE.md +++ b/fastapi_log_mail/readme/CONFIGURE.md @@ -1 +1 @@ -In any endpoint that has logging enabled, insert an email template in "Error E-mail Template". \ No newline at end of file +Configure a fastapi endpoint as explained in `api_log_mail`. diff --git a/fastapi_log_mail/readme/DESCRIPTION.md b/fastapi_log_mail/readme/DESCRIPTION.md index 8eccf80b8..e92d7f261 100644 --- a/fastapi_log_mail/readme/DESCRIPTION.md +++ b/fastapi_log_mail/readme/DESCRIPTION.md @@ -1 +1 @@ -This module allows to send an email when an exception occurs in an endpoint. +This module allows to create an activity when an exception is logged in a fastapi endpoint. diff --git a/fastapi_log_mail/static/description/index.html b/fastapi_log_mail/static/description/index.html index 1ec0ff159..026bfe3b8 100644 --- a/fastapi_log_mail/static/description/index.html +++ b/fastapi_log_mail/static/description/index.html @@ -3,7 +3,7 @@ -Fastapi Log +FastAPI Log notification -
-

Fastapi Log

+
+

FastAPI Log notification

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

-

This module allows to send an email when an exception occurs in an -endpoint.

+

This module allows to create an activity when an exception is logged in +a fastapi endpoint.

Table of contents

    @@ -387,8 +387,7 @@

    Fastapi Log

Configuration

-

In any endpoint that has logging enabled, insert an email template in -“Error E-mail Template”.

+

Configure a fastapi endpoint as explained in api_log_mail.

Bug Tracker

diff --git a/fastapi_log_mail/tests/test_fastapi_log_mail.py b/fastapi_log_mail/tests/test_fastapi_log_mail.py index 74cad8ba8..e4bde480a 100644 --- a/fastapi_log_mail/tests/test_fastapi_log_mail.py +++ b/fastapi_log_mail/tests/test_fastapi_log_mail.py @@ -1,24 +1,32 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + import os import unittest -from odoo.addons.api_log.tests.common import CommonAPILog from odoo.addons.fastapi.schemas import DemoExceptionType +from odoo.addons.fastapi_log.tests.common import Common from odoo.addons.mail.tests.common import MailCase +from fastapi import status + -@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") -class FastAPIEncryptedErrorsCase(CommonAPILog, MailCase): +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "TestFastapiLogMail skipped") +class TestFastapiLogMail(Common, MailCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") - - cls.fastapi_demo_app._handle_registry_sync() - cls.fastapi_demo_app.log_requests = True - cls.fastapi_demo_app.fastapi_log_mail_template_id = cls.env[ + cls.fastapi_demo_app.api_log_mail_exception_activity_type_id = cls.env[ + "mail.activity.type" + ].create( + { + "name": "Test exception activity type", + "res_model": "api.log", + } + ) + cls.fastapi_demo_app.api_log_mail_exception_template_id = cls.env[ "mail.template" ].create( { @@ -27,16 +35,42 @@ def setUpClass(cls): } ) + def test_endpoint_exception_create_activity(self): + """If an endpoint has an activity type, + when an exception occurs an activity of the configured type is created. + """ + # Arrange + app = self.fastapi_demo_app + activity_type = app.api_log_mail_exception_activity_type_id + route = ( + "/fastapi_demo/test/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=An error happened" + ) + # pre-condition + self.assertTrue(activity_type) + + # Act + with self.log_capturer() as capturer: + response = self.url_open(route, timeout=200) + + # Assert + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + log = capturer.records + self.assertEqual(len(log), 1) + self.assertTrue(log.activity_ids) + def test_endpoint_exception_send_email(self): """If an endpoint has an email template, when an exception occurs an email is sent using the configured template. """ # Arrange - mail_template = self.fastapi_demo_app.fastapi_log_mail_template_id + app = self.fastapi_demo_app + mail_template = app.api_log_mail_exception_template_id route = ( - "/fastapi_demo/demo/exception?" + "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.user_error.value}" - "&error_message=User Error" + "&error_message=An error happened" ) # pre-condition self.assertTrue(mail_template) diff --git a/fastapi_log_mail/views/fastapi_endpoint_views.xml b/fastapi_log_mail/views/fastapi_endpoint_views.xml index fdd6deaaa..6e1d27886 100644 --- a/fastapi_log_mail/views/fastapi_endpoint_views.xml +++ b/fastapi_log_mail/views/fastapi_endpoint_views.xml @@ -5,12 +5,22 @@ --> + Add log mail fields to endpoint form view fastapi.endpoint + c**=lnR&y+1yw{;I3Hy6Ua2{<d0kcR+VvBo; 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 -FastAPI Log notification +README.rst -
-

FastAPI Log notification

+
+ + +Odoo Community Association + +
+

FastAPI Log notification

-

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This module allows to create an activity when an exception is logged in a fastapi endpoint.

Table of contents

@@ -386,11 +391,11 @@

FastAPI Log notification

-

Configuration

+

Configuration

Configure a fastapi endpoint as explained in api_log_mail.

-

Bug Tracker

+

Bug Tracker

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

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • PyTech
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -430,5 +435,6 @@

Maintainers

+
diff --git a/fastapi_log_mail/tests/test_fastapi_log_mail.py b/fastapi_log_mail/tests/test_fastapi_log_mail.py index e4bde480a..c4e83cb02 100644 --- a/fastapi_log_mail/tests/test_fastapi_log_mail.py +++ b/fastapi_log_mail/tests/test_fastapi_log_mail.py @@ -15,10 +15,8 @@ @unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "TestFastapiLogMail skipped") class TestFastapiLogMail(Common, MailCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.fastapi_demo_app.api_log_mail_exception_activity_type_id = cls.env[ + def _set_mail_exception_activity_type(self, app): + app.api_log_mail_exception_activity_type_id = app.env[ "mail.activity.type" ].create( { @@ -26,12 +24,12 @@ def setUpClass(cls): "res_model": "api.log", } ) - cls.fastapi_demo_app.api_log_mail_exception_template_id = cls.env[ - "mail.template" - ].create( + + def _set_mail_exception_template(self, app): + app.api_log_mail_exception_template_id = app.env["mail.template"].create( { "name": "Test exception email template", - "model_id": cls.env.ref("api_log.model_api_log").id, + "model_id": app.env.ref("api_log.model_api_log").id, } ) @@ -40,7 +38,8 @@ def test_endpoint_exception_create_activity(self): when an exception occurs an activity of the configured type is created. """ # Arrange - app = self.fastapi_demo_app + app = self._get_log_env_records(self.fastapi_demo_app) + self._set_mail_exception_activity_type(app) activity_type = app.api_log_mail_exception_activity_type_id route = ( "/fastapi_demo/test/demo/exception?" @@ -65,7 +64,8 @@ def test_endpoint_exception_send_email(self): when an exception occurs an email is sent using the configured template. """ # Arrange - app = self.fastapi_demo_app + app = self._get_log_env_records(self.fastapi_demo_app) + self._set_mail_exception_template(app) mail_template = app.api_log_mail_exception_template_id route = ( "/fastapi_demo/test/demo/exception?" From 16ec3bdf6d9dbdd2dee861588605c69d256491ca Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:46:03 +0100 Subject: [PATCH 23/24] [IMP] fastapi_log_mail: pre-commit execution --- fastapi_log_mail/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastapi_log_mail/pyproject.toml diff --git a/fastapi_log_mail/pyproject.toml b/fastapi_log_mail/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/fastapi_log_mail/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" From c85b244e1a9cd9a52fe8e3e66c8f129f7d6a6893 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 26 Mar 2026 12:46:09 +0100 Subject: [PATCH 24/24] [MIG] fastapi_log_mail: Migration to 17.0 --- fastapi_log_mail/README.rst | 16 ++++------ fastapi_log_mail/__manifest__.py | 2 +- .../migrations/16.0.1.1.0/post-migration.py | 32 ------------------- .../migrations/16.0.1.1.0/pre-migration.py | 20 ------------ .../static/description/index.html | 30 +++++++---------- .../tests/test_fastapi_log_mail.py | 6 ++-- .../views/fastapi_endpoint_views.xml | 12 ++----- 7 files changed, 25 insertions(+), 93 deletions(-) delete mode 100644 fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py delete mode 100644 fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py diff --git a/fastapi_log_mail/README.rst b/fastapi_log_mail/README.rst index c53d52f14..dc66e6326 100644 --- a/fastapi_log_mail/README.rst +++ b/fastapi_log_mail/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ======================== FastAPI Log notification ======================== @@ -17,17 +13,17 @@ FastAPI Log notification .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_log_mail + :target: https://github.com/OCA/rest-framework/tree/17.0/fastapi_log_mail :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-fastapi_log_mail + :target: https://translation.odoo-community.org/projects/rest-framework-17-0/rest-framework-17-0-fastapi_log_mail :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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -51,7 +47,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -91,6 +87,6 @@ Current `maintainer `__: |maintainer-SirPyTech| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_log_mail/__manifest__.py b/fastapi_log_mail/__manifest__.py index 8bcbc0621..e137a0235 100644 --- a/fastapi_log_mail/__manifest__.py +++ b/fastapi_log_mail/__manifest__.py @@ -3,7 +3,7 @@ { "name": "FastAPI Log notification", - "version": "16.0.1.1.0", + "version": "17.0.1.0.0", "license": "AGPL-3", "author": "PyTech, Odoo Community Association (OCA)", "maintainers": [ diff --git a/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py b/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py deleted file mode 100644 index 1903d6e19..000000000 --- a/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - template_id_column = openupgrade.get_legacy_name( - "fastapi_log_mail_template_id", - ) - openupgrade.logged_query( - env.cr, - """ - UPDATE fastapi_endpoint SET - api_log_mail_exception_template_id=%(template_id_column)s - WHERE %(template_id_column)s IS NOT NULL - """ - % { - "template_id_column": template_id_column, - }, - ) - openupgrade.logged_query( - env.cr, - """ - ALTER TABLE fastapi_endpoint - DROP COLUMN %(template_id_column)s - """ - % { - "template_id_column": template_id_column, - }, - ) diff --git a/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py b/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py deleted file mode 100644 index 71f919a98..000000000 --- a/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - openupgrade.copy_columns( - env.cr, - { - "fastapi_endpoint": [ - ( - "fastapi_log_mail_template_id", - None, - None, - ), - ], - }, - ) diff --git a/fastapi_log_mail/static/description/index.html b/fastapi_log_mail/static/description/index.html index ca12278b1..0776c72c8 100644 --- a/fastapi_log_mail/static/description/index.html +++ b/fastapi_log_mail/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +FastAPI Log notification -
+
+

FastAPI Log notification

- - -Odoo Community Association - -
-

FastAPI Log notification

-

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This module allows to create an activity when an exception is logged in a fastapi endpoint.

Table of contents

@@ -391,27 +386,27 @@

FastAPI Log notification

-

Configuration

+

Configuration

Configure a fastapi endpoint as explained in api_log_mail.

-

Bug Tracker

+

Bug Tracker

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

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • PyTech
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -430,11 +425,10 @@

Maintainers

promote its widespread use.

Current maintainer:

SirPyTech

-

This module is part of the OCA/rest-framework project on GitHub.

+

This module is part of the OCA/rest-framework project on GitHub.

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

-
diff --git a/fastapi_log_mail/tests/test_fastapi_log_mail.py b/fastapi_log_mail/tests/test_fastapi_log_mail.py index c4e83cb02..b0fa864cd 100644 --- a/fastapi_log_mail/tests/test_fastapi_log_mail.py +++ b/fastapi_log_mail/tests/test_fastapi_log_mail.py @@ -6,6 +6,8 @@ import os import unittest +from odoo.tools import mute_logger + from odoo.addons.fastapi.schemas import DemoExceptionType from odoo.addons.fastapi_log.tests.common import Common from odoo.addons.mail.tests.common import MailCase @@ -50,7 +52,7 @@ def test_endpoint_exception_create_activity(self): self.assertTrue(activity_type) # Act - with self.log_capturer() as capturer: + with self.log_capturer() as capturer, mute_logger("odoo.http"): response = self.url_open(route, timeout=200) # Assert @@ -76,7 +78,7 @@ def test_endpoint_exception_send_email(self): self.assertTrue(mail_template) # Act - with self.mock_mail_gateway(): + with self.mock_mail_gateway(), mute_logger("odoo.http"): self.url_open(route, timeout=200) # Assert diff --git a/fastapi_log_mail/views/fastapi_endpoint_views.xml b/fastapi_log_mail/views/fastapi_endpoint_views.xml index 6e1d27886..1250b3d98 100644 --- a/fastapi_log_mail/views/fastapi_endpoint_views.xml +++ b/fastapi_log_mail/views/fastapi_endpoint_views.xml @@ -13,20 +13,12 @@