odoo-addon-api-log 16.0.1.0.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ .. image:: https://odoo-community.org/readme-banner-image
2
+ :target: https://odoo-community.org/get-involved?utm_source=readme
3
+ :alt: Odoo Community Association
4
+
5
+ =======
6
+ API Log
7
+ =======
8
+
9
+ ..
10
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11
+ !! This file is generated by oca-gen-addon-readme !!
12
+ !! changes will be overwritten. !!
13
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14
+ !! source digest: sha256:a1948d1ff8b66338522f1c1a2988d5ab8b0a7ff97fcbe9589879bd67b30719ce
15
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16
+
17
+ .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
18
+ :target: https://odoo-community.org/page/development-status
19
+ :alt: Beta
20
+ .. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
21
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
22
+ :alt: License: AGPL-3
23
+ .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
24
+ :target: https://github.com/OCA/rest-framework/tree/16.0/api_log
25
+ :alt: OCA/rest-framework
26
+ .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27
+ :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-api_log
28
+ :alt: Translate me on Weblate
29
+ .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
30
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0
31
+ :alt: Try me on Runboat
32
+
33
+ |badge1| |badge2| |badge3| |badge4| |badge5|
34
+
35
+ This module allows to store request and response logs for any API.
36
+
37
+ When a response is logged, the header ``API_LOG_ENTRY_ID`` is injected
38
+ in the response header. This header stores the identifier of the log
39
+ record produced from the response.
40
+
41
+ **Table of contents**
42
+
43
+ .. contents::
44
+ :local:
45
+
46
+ Bug Tracker
47
+ ===========
48
+
49
+ Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.
50
+ In case of trouble, please check there if your issue has already been reported.
51
+ If you spotted it first, help us to smash it by providing a detailed and welcomed
52
+ `feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20api_log%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
53
+
54
+ Do not contact contributors directly about support or help with technical issues.
55
+
56
+ Credits
57
+ =======
58
+
59
+ Authors
60
+ -------
61
+
62
+ * Akretion
63
+
64
+ Contributors
65
+ ------------
66
+
67
+ - Florian Mounier florian.mounier@akretion.com
68
+ - Guewen Baconnier guewen.baconnier@camptocamp.com
69
+ - Simone Orsi simahawk@gmail.com
70
+ - `PyTech <https://www.pytech.it>`__:
71
+
72
+ - Simone Rubino <simone.rubino@pytech.it>
73
+
74
+ Maintainers
75
+ -----------
76
+
77
+ This module is maintained by the OCA.
78
+
79
+ .. image:: https://odoo-community.org/logo.png
80
+ :alt: Odoo Community Association
81
+ :target: https://odoo-community.org
82
+
83
+ OCA, or the Odoo Community Association, is a nonprofit organization whose
84
+ mission is to support the collaborative development of Odoo features and
85
+ promote its widespread use.
86
+
87
+ .. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px
88
+ :target: https://github.com/paradoxxxzero
89
+ :alt: paradoxxxzero
90
+
91
+ Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
92
+
93
+ |maintainer-paradoxxxzero|
94
+
95
+ This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/api_log>`_ project on GitHub.
96
+
97
+ You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
@@ -0,0 +1 @@
1
+ from . import models
@@ -0,0 +1,20 @@
1
+ # Copyright 2025 Akretion (http://www.akretion.com).
2
+ # @author Florian Mounier <florian.mounier@akretion.com>
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4
+
5
+ {
6
+ "name": "API Log",
7
+ "version": "16.0.1.0.0",
8
+ "author": "Akretion, Odoo Community Association (OCA)",
9
+ "license": "AGPL-3",
10
+ "summary": "Log API requests in database",
11
+ "category": "Tools",
12
+ "depends": ["web"],
13
+ "website": "https://github.com/OCA/rest-framework",
14
+ "data": [
15
+ "security/res_groups.xml",
16
+ "security/ir_model_access.xml",
17
+ "views/api_log_views.xml",
18
+ ],
19
+ "maintainers": ["paradoxxxzero"],
20
+ }
@@ -0,0 +1,246 @@
1
+ # Translation of Odoo Server.
2
+ # This file contains the translation of the following modules:
3
+ # * api_log
4
+ #
5
+ msgid ""
6
+ msgstr ""
7
+ "Project-Id-Version: Odoo Server 16.0\n"
8
+ "Report-Msgid-Bugs-To: \n"
9
+ "Last-Translator: \n"
10
+ "Language-Team: \n"
11
+ "MIME-Version: 1.0\n"
12
+ "Content-Type: text/plain; charset=UTF-8\n"
13
+ "Content-Transfer-Encoding: \n"
14
+ "Plural-Forms: \n"
15
+
16
+ #. module: api_log
17
+ #: model:ir.actions.act_window,name:api_log.api_log_action
18
+ #: model:ir.ui.menu,name:api_log.menu_api_log
19
+ msgid "API Log"
20
+ msgstr ""
21
+
22
+ #. module: api_log
23
+ #: model:res.groups,name:api_log.group_api_log
24
+ msgid "API Log Access"
25
+ msgstr ""
26
+
27
+ #. module: api_log
28
+ #: model:ir.model.fields,field_description:api_log.field_api_log__collection_id
29
+ #: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search
30
+ msgid "Collection"
31
+ msgstr ""
32
+
33
+ #. module: api_log
34
+ #: model:ir.model.fields,field_description:api_log.field_api_log__collection_model
35
+ msgid "Collection Model"
36
+ msgstr ""
37
+
38
+ #. module: api_log
39
+ #: model:ir.model.fields,field_description:api_log.field_api_log__collection_ref
40
+ msgid "Collection Ref"
41
+ msgstr ""
42
+
43
+ #. module: api_log
44
+ #: model:ir.model,name:api_log.model_api_log_collection_mixin
45
+ msgid "Collection of API logs"
46
+ msgstr ""
47
+
48
+ #. module: api_log
49
+ #: model:ir.model.fields,field_description:api_log.field_api_log__create_uid
50
+ msgid "Created by"
51
+ msgstr ""
52
+
53
+ #. module: api_log
54
+ #: model:ir.model.fields,field_description:api_log.field_api_log__create_date
55
+ msgid "Created on"
56
+ msgstr ""
57
+
58
+ #. module: api_log
59
+ #: model:ir.model.fields,field_description:api_log.field_api_log__display_name
60
+ msgid "Display Name"
61
+ msgstr ""
62
+
63
+ #. module: api_log
64
+ #: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search
65
+ msgid "Error"
66
+ msgstr ""
67
+
68
+ #. module: api_log
69
+ #: model:ir.model.fields,field_description:api_log.field_api_log__id
70
+ msgid "ID"
71
+ msgstr ""
72
+
73
+ #. module: api_log
74
+ #: model:ir.model.fields,field_description:api_log.field_api_log____last_update
75
+ msgid "Last Modified on"
76
+ msgstr ""
77
+
78
+ #. module: api_log
79
+ #: model:ir.model.fields,field_description:api_log.field_api_log__write_uid
80
+ msgid "Last Updated by"
81
+ msgstr ""
82
+
83
+ #. module: api_log
84
+ #: model:ir.model.fields,field_description:api_log.field_api_log__write_date
85
+ msgid "Last Updated on"
86
+ msgstr ""
87
+
88
+ #. module: api_log
89
+ #: model:ir.model.fields,field_description:api_log.field_api_log_collection_mixin__log_requests
90
+ msgid "Log Requests"
91
+ msgstr ""
92
+
93
+ #. module: api_log
94
+ #: model:ir.model,name:api_log.model_api_log
95
+ msgid "Log for API"
96
+ msgstr ""
97
+
98
+ #. module: api_log
99
+ #: model:ir.model.fields,help:api_log.field_api_log_collection_mixin__log_requests
100
+ msgid "Log requests in database."
101
+ msgstr ""
102
+
103
+ #. module: api_log
104
+ #: model:ir.model.fields,field_description:api_log.field_api_log_collection_mixin__log_ids
105
+ msgid "Logs"
106
+ msgstr ""
107
+
108
+ #. module: api_log
109
+ #: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search
110
+ msgid "Method"
111
+ msgstr ""
112
+
113
+ #. module: api_log
114
+ #: model:ir.model.fields,field_description:api_log.field_api_log__name
115
+ msgid "Name"
116
+ msgstr ""
117
+
118
+ #. module: api_log
119
+ #: model:ir.model.fields,field_description:api_log.field_api_log__referrer
120
+ msgid "Referrer"
121
+ msgstr ""
122
+
123
+ #. module: api_log
124
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_body
125
+ msgid "Request Body"
126
+ msgstr ""
127
+
128
+ #. module: api_log
129
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_b64
130
+ msgid "Request Content"
131
+ msgstr ""
132
+
133
+ #. module: api_log
134
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_content_length
135
+ msgid "Request Content Length"
136
+ msgstr ""
137
+
138
+ #. module: api_log
139
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_content_type
140
+ msgid "Request Content Type"
141
+ msgstr ""
142
+
143
+ #. module: api_log
144
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_date
145
+ msgid "Request Date"
146
+ msgstr ""
147
+
148
+ #. module: api_log
149
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_headers
150
+ msgid "Request Headers"
151
+ msgstr ""
152
+
153
+ #. module: api_log
154
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_headers_preview
155
+ msgid "Request Headers Preview"
156
+ msgstr ""
157
+
158
+ #. module: api_log
159
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_method
160
+ msgid "Request Method"
161
+ msgstr ""
162
+
163
+ #. module: api_log
164
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_preview
165
+ msgid "Request Preview"
166
+ msgstr ""
167
+
168
+ #. module: api_log
169
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_time
170
+ msgid "Request Time"
171
+ msgstr ""
172
+
173
+ #. module: api_log
174
+ #: model:ir.model.fields,field_description:api_log.field_api_log__request_url
175
+ msgid "Request Url"
176
+ msgstr ""
177
+
178
+ #. module: api_log
179
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_body
180
+ msgid "Response Body"
181
+ msgstr ""
182
+
183
+ #. module: api_log
184
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_b64
185
+ msgid "Response Content"
186
+ msgstr ""
187
+
188
+ #. module: api_log
189
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_content_length
190
+ msgid "Response Content Length"
191
+ msgstr ""
192
+
193
+ #. module: api_log
194
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_content_type
195
+ msgid "Response Content Type"
196
+ msgstr ""
197
+
198
+ #. module: api_log
199
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_date
200
+ msgid "Response Date"
201
+ msgstr ""
202
+
203
+ #. module: api_log
204
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_headers
205
+ msgid "Response Headers"
206
+ msgstr ""
207
+
208
+ #. module: api_log
209
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_headers_preview
210
+ msgid "Response Headers Preview"
211
+ msgstr ""
212
+
213
+ #. module: api_log
214
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_preview
215
+ msgid "Response Preview"
216
+ msgstr ""
217
+
218
+ #. module: api_log
219
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_status_code
220
+ msgid "Response Status Code"
221
+ msgstr ""
222
+
223
+ #. module: api_log
224
+ #: model:ir.model.fields,field_description:api_log.field_api_log__response_time
225
+ msgid "Response Time"
226
+ msgstr ""
227
+
228
+ #. module: api_log
229
+ #: model:ir.model.fields,field_description:api_log.field_api_log__stack_trace
230
+ msgid "Stack Trace"
231
+ msgstr ""
232
+
233
+ #. module: api_log
234
+ #: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search
235
+ msgid "Success"
236
+ msgstr ""
237
+
238
+ #. module: api_log
239
+ #: model:ir.model.fields,field_description:api_log.field_api_log__time
240
+ msgid "Time"
241
+ msgstr ""
242
+
243
+ #. module: api_log
244
+ #: model_terms:ir.ui.view,arch_db:api_log.api_log_view_search
245
+ msgid "Url"
246
+ msgstr ""
@@ -0,0 +1,2 @@
1
+ from . import api_log_collection
2
+ from . import api_log
@@ -0,0 +1,277 @@
1
+ # Copyright 2025 Akretion (http://www.akretion.com).
2
+ # @author Florian Mounier <florian.mounier@akretion.com>
3
+ # Copyright 2025 Simone Rubino - PyTech
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
5
+
6
+ import base64
7
+ import json
8
+ import time
9
+ from datetime import date
10
+ from traceback import format_exception
11
+
12
+ from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
13
+
14
+ from odoo import api, fields, models
15
+
16
+
17
+ class APILog(models.Model):
18
+ _name = "api.log"
19
+ _description = "Log for API"
20
+ _order = "id desc"
21
+
22
+ collection_ref = fields.Reference(
23
+ selection="_selection_collection_ref",
24
+ )
25
+ collection_model = fields.Char(
26
+ compute="_compute_collection",
27
+ store=True,
28
+ index=True,
29
+ )
30
+ collection_id = fields.Integer(
31
+ compute="_compute_collection",
32
+ store=True,
33
+ index=True,
34
+ )
35
+
36
+ # Request
37
+ request_url = fields.Char()
38
+ request_method = fields.Char()
39
+ request_headers = fields.Json()
40
+ request_body = fields.Binary(attachment=False)
41
+ request_date = fields.Datetime()
42
+ request_time = fields.Float()
43
+
44
+ # Response
45
+ response_status_code = fields.Integer()
46
+ response_headers = fields.Json()
47
+ response_body = fields.Binary(attachment=False)
48
+ response_date = fields.Datetime()
49
+ response_time = fields.Float()
50
+
51
+ stack_trace = fields.Text()
52
+
53
+ # Derived fields
54
+ name = fields.Char(compute="_compute_name", store=True)
55
+ time = fields.Float(compute="_compute_time", store=True)
56
+ request_preview = fields.Text(compute="_compute_request_preview")
57
+ response_preview = fields.Text(compute="_compute_response_preview")
58
+ # Binary fields are useful to download the payload in case of file download/upload
59
+ request_b64 = fields.Binary(
60
+ string="Request Content", compute="_compute_request_b64"
61
+ )
62
+ response_b64 = fields.Binary(
63
+ string="Response Content", compute="_compute_response_b64"
64
+ )
65
+ request_headers_preview = fields.Text(compute="_compute_headers_preview")
66
+ response_headers_preview = fields.Text(compute="_compute_headers_preview")
67
+ request_content_type = fields.Char(
68
+ compute="_compute_request_headers_derived", store=True
69
+ )
70
+ request_content_length = fields.Integer(
71
+ compute="_compute_request_headers_derived", store=True
72
+ )
73
+ referrer = fields.Char(compute="_compute_request_headers_derived", store=True)
74
+ response_content_type = fields.Char(
75
+ compute="_compute_response_headers_derived", store=True
76
+ )
77
+ response_content_length = fields.Integer(
78
+ compute="_compute_response_headers_derived", store=True
79
+ )
80
+
81
+ @api.model
82
+ def _selection_collection_ref(self):
83
+ return []
84
+
85
+ @api.depends(
86
+ "collection_ref",
87
+ )
88
+ def _compute_collection(self):
89
+ for log in self:
90
+ collection = log.collection_ref
91
+ if collection:
92
+ collection_model = collection._name
93
+ collection_id = collection.id
94
+ else:
95
+ collection_model = False
96
+ collection_id = False
97
+ log.collection_model = collection_model
98
+ log.collection_id = collection_id
99
+
100
+ @api.model
101
+ def _headers_hidden_keys(self):
102
+ """Header keys that should not be logged.
103
+
104
+ They might contains sensitive data.
105
+ """
106
+ return (
107
+ "Api-Key",
108
+ "Cookie",
109
+ )
110
+
111
+ @api.model
112
+ def _sanitize_headers_dict(self, headers_dict):
113
+ keys_to_hide = self._headers_hidden_keys()
114
+ for key in headers_dict:
115
+ if key in keys_to_hide:
116
+ headers_dict[key] = "<redacted>"
117
+ return headers_dict
118
+
119
+ @api.model
120
+ def _headers_to_dict(self, headers):
121
+ headers_dict = {key: value for key, value in headers.items()}
122
+ return self._sanitize_headers_dict(headers_dict)
123
+
124
+ def _current_time(self):
125
+ return time.time_ns() / 1e9
126
+
127
+ @api.model
128
+ def _get_http_request(self, request):
129
+ return request.httprequest
130
+
131
+ @api.model
132
+ def _get_request_body(self, request):
133
+ """Take extra care with the request's body because it might get consumed."""
134
+ httprequest = self._get_http_request(request)
135
+ return httprequest.data
136
+
137
+ @api.model
138
+ def _prepare_log_request(self, request):
139
+ httprequest = self._get_http_request(request)
140
+ log_request_values = {
141
+ "request_url": httprequest.url,
142
+ "request_method": httprequest.method,
143
+ "request_headers": self._headers_to_dict(httprequest.headers),
144
+ "request_body": self._get_request_body(request),
145
+ "request_date": fields.Datetime.now(),
146
+ "request_time": self._current_time(),
147
+ }
148
+ return log_request_values
149
+
150
+ @api.model
151
+ def log_request(self, request, override_log_values=None):
152
+ log_request_values = self._prepare_log_request(request)
153
+ log_request_values.update(override_log_values or {})
154
+ return self.sudo().create(log_request_values)
155
+
156
+ def _inject_log_entry(self, values_dict):
157
+ values_dict["API-Log-Entry-ID"] = str(self.id)
158
+ return values_dict
159
+
160
+ def _prepare_log_response(self, response):
161
+ self._inject_log_entry(response.headers)
162
+ headers_dict = self._headers_to_dict(response.headers)
163
+ return {
164
+ "response_status_code": response.status_code,
165
+ "response_headers": headers_dict,
166
+ "response_body": response.data,
167
+ "response_date": fields.Datetime.now(),
168
+ "response_time": self._current_time(),
169
+ }
170
+
171
+ def log_response(self, response):
172
+ log_response_values = self._prepare_log_response(response)
173
+ return self.sudo().write(log_response_values)
174
+
175
+ def _prepare_log_exception(self, exception):
176
+ if not hasattr(exception, "headers"):
177
+ exception.headers = dict()
178
+ values = {
179
+ "stack_trace": "".join(format_exception(exception)),
180
+ "response_headers": self._inject_log_entry(exception.headers),
181
+ "response_body": str(exception),
182
+ "response_date": fields.Datetime.now(),
183
+ "response_time": self._current_time(),
184
+ }
185
+
186
+ if isinstance(exception, WerkzeugHTTPException):
187
+ values.update(
188
+ {
189
+ "response_status_code": exception.code,
190
+ "response_headers": self._headers_to_dict(exception.get_headers()),
191
+ "response_body": exception.get_body(),
192
+ }
193
+ )
194
+ return values
195
+
196
+ def log_exception(self, exception):
197
+ try:
198
+ exc_handling_response = self.env.registry["ir.http"]._handle_error(
199
+ exception
200
+ )
201
+ self.log_response(exc_handling_response)
202
+ except Exception as handling_exception:
203
+ exception = handling_exception
204
+ log_exception_values = self._prepare_log_exception(exception)
205
+ return self.sudo().write(log_exception_values)
206
+
207
+ @api.depends("request_url", "request_method", "request_date")
208
+ def _compute_name(self):
209
+ for log in self:
210
+ log.name = (
211
+ f"{(log.request_date or date.min).isoformat()} - "
212
+ f"[{log.request_method or ''}] {log.request_url or ''}"
213
+ )
214
+
215
+ @api.depends("request_time", "response_time")
216
+ def _compute_time(self):
217
+ for log in self:
218
+ if log.request_time and log.response_time:
219
+ log.time = log.response_time - log.request_time
220
+ else:
221
+ log.time = 0
222
+
223
+ @api.depends("request_headers")
224
+ def _compute_request_headers_derived(self):
225
+ for log in self:
226
+ headers = log.request_headers or {}
227
+ log.request_content_type = headers.get("content-type", "")
228
+ log.request_content_length = headers.get("content-length", 0)
229
+ log.referrer = headers.get("referer", "")
230
+
231
+ @api.depends("response_headers")
232
+ def _compute_response_headers_derived(self):
233
+ for log in self:
234
+ headers = log.response_headers or {}
235
+ log.response_content_type = headers.get("content-type", "")
236
+ log.response_content_length = headers.get("content-length", 0)
237
+
238
+ @api.depends("request_body")
239
+ def _compute_request_preview(self):
240
+ for log in self.with_context(bin_size=False):
241
+ log.request_preview = log._body_preview(log.request_body)
242
+
243
+ @api.depends("response_body")
244
+ def _compute_response_preview(self):
245
+ for log in self.with_context(bin_size=False):
246
+ log.response_preview = log._body_preview(log.response_body)
247
+
248
+ def _body_preview(self, body):
249
+ # Display the first 1000 characters of the body if it's a text content
250
+ body_preview = False
251
+ if body:
252
+ try:
253
+ body_preview = body.decode("utf-8", errors="ignore")
254
+ if len(body_preview) > 1000:
255
+ body_preview = body_preview[:1000] + "...\n(...)"
256
+ except UnicodeDecodeError:
257
+ body_preview = False
258
+ return body_preview
259
+
260
+ @api.depends("request_headers", "response_headers")
261
+ def _compute_headers_preview(self):
262
+ for log in self:
263
+ log.request_headers_preview = log._headers_preview(log.request_headers)
264
+ log.response_headers_preview = log._headers_preview(log.response_headers)
265
+
266
+ def _headers_preview(self, headers):
267
+ return json.dumps(headers, sort_keys=True, indent=4) if headers else False
268
+
269
+ @api.depends("request_body")
270
+ def _compute_request_b64(self):
271
+ for log in self:
272
+ log.request_b64 = base64.b64encode(log.request_body or b"")
273
+
274
+ @api.depends("response_body")
275
+ def _compute_response_b64(self):
276
+ for log in self:
277
+ log.response_b64 = base64.b64encode(log.response_body or b"")
@@ -0,0 +1,55 @@
1
+ # Copyright 2025 Simone Rubino - PyTech
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3
+
4
+ from odoo import fields, models
5
+
6
+
7
+ class APILogCollection(models.AbstractModel):
8
+ _name = "api.log_collection.mixin"
9
+ _description = "Collection of API logs"
10
+
11
+ log_requests = fields.Boolean(
12
+ help="Log requests in database.",
13
+ )
14
+
15
+ log_ids = fields.One2many(
16
+ comodel_name="api.log",
17
+ compute="_compute_log_ids",
18
+ string="Logs",
19
+ )
20
+
21
+ def _get_logs_domain(self):
22
+ """Domain to find the logs in `self`."""
23
+ return [
24
+ ("collection_model", "=", self._name),
25
+ ("collection_id", "in", self.ids),
26
+ ]
27
+
28
+ def _compute_log_ids(self):
29
+ all_logs = self.env["api.log"].search_read(
30
+ domain=self._get_logs_domain(),
31
+ fields=[
32
+ "collection_id",
33
+ ],
34
+ load=None,
35
+ )
36
+ log_ids_by_collection_id = {}
37
+ for log in all_logs:
38
+ log_ids_by_collection_id.setdefault(log["collection_id"], []).append(
39
+ log["id"]
40
+ )
41
+
42
+ for collection in self:
43
+ collection.log_ids = log_ids_by_collection_id.get(collection.id)
44
+
45
+ def action_logs(self):
46
+ return {
47
+ "type": "ir.actions.act_window",
48
+ "res_model": "api.log",
49
+ "name": "Logs",
50
+ "view_type": "form",
51
+ "view_mode": "tree,form",
52
+ "target": "current",
53
+ "domain": self._get_logs_domain(),
54
+ "context": dict(self.env.context),
55
+ }