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.
- odoo/addons/api_log/README.rst +97 -0
- odoo/addons/api_log/__init__.py +1 -0
- odoo/addons/api_log/__manifest__.py +20 -0
- odoo/addons/api_log/i18n/api_log.pot +246 -0
- odoo/addons/api_log/models/__init__.py +2 -0
- odoo/addons/api_log/models/api_log.py +277 -0
- odoo/addons/api_log/models/api_log_collection.py +55 -0
- odoo/addons/api_log/readme/CONTRIBUTORS.md +5 -0
- odoo/addons/api_log/readme/DESCRIPTION.md +4 -0
- odoo/addons/api_log/security/ir_model_access.xml +17 -0
- odoo/addons/api_log/security/res_groups.xml +17 -0
- odoo/addons/api_log/static/description/icon.png +0 -0
- odoo/addons/api_log/static/description/index.html +440 -0
- odoo/addons/api_log/tests/__init__.py +1 -0
- odoo/addons/api_log/tests/common.py +23 -0
- odoo/addons/api_log/tests/test_api_log.py +86 -0
- odoo/addons/api_log/views/api_log_views.xml +118 -0
- odoo_addon_api_log-16.0.1.0.0.7.dist-info/METADATA +112 -0
- odoo_addon_api_log-16.0.1.0.0.7.dist-info/RECORD +21 -0
- odoo_addon_api_log-16.0.1.0.0.7.dist-info/WHEEL +5 -0
- odoo_addon_api_log-16.0.1.0.0.7.dist-info/top_level.txt +1 -0
|
@@ -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,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
|
+
}
|