-
-
Notifications
You must be signed in to change notification settings - Fork 354
[16.0][ADD] fastapi_log #554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 16.0
Are you sure you want to change the base?
Conversation
|
@simahawk here I addressed your latest comments akretion#8 (comment) and akretion#8 (comment), please have a look |
| collection_model = fields.Char( | ||
| compute="_compute_collection", | ||
| store=True, | ||
| index=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we need all these indexes. Looks like you search only using collection_ref.
IMO you can drop the collection_ref index and search via model+id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the reason for these b64 fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought they would be useful to store the binary value of the request/response, but now I see request/response body is already Binary.
Removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They were used to dowload the payload directly from the log form in case of a file upload/download.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They were used to dowload the payload directly from the log form in case of a file upload/download.
Thanks for letting us know, I added them back.
| @api.depends( | ||
| "collection_ref", | ||
| ) | ||
| def _compute_collection(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't be the other way around? You compute collection_ref based on model + id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, you can compute collection_ref based on model + id; or you can compute model + id from collection_ref.
I see no big difference between the two approaches, is there any specific reason to do it in the way you suggest?
api_log/models/api_log.py
Outdated
| try: | ||
| headers_dict = {key: value for key, value in headers.items()} | ||
| return self._sanitize_headers_dict(headers_dict) | ||
| except AttributeError: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when do you expect to have this kind of error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm suspicious about the headers argument, if it does not have the items method then an AttributeError would be raised.
But you're right, let's be optimistic and remove the exception handling 😄
| def log_request(self, request, override_log_values=None): | ||
| log_request_values = self._prepare_log_request(request) | ||
| if override_log_values: | ||
| log_request_values.update(override_log_values) | ||
| return self.sudo().create(log_request_values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| def log_request(self, request, override_log_values=None): | |
| log_request_values = self._prepare_log_request(request) | |
| if override_log_values: | |
| log_request_values.update(override_log_values) | |
| return self.sudo().create(log_request_values) | |
| def log_request(self, request, **kw): | |
| log_request_values = self._prepare_log_request(request) | |
| log_request_values.update(kw) | |
| return self.sudo().create(log_request_values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to keep the log values in their own variable instead of using kwargs because this way more parameters can be added to the method to tweak its behavior.
I removed the if as suggested though.
api_log/models/api_log.py
Outdated
| def _prepare_log_response(self, response): | ||
| return { | ||
| "response_status_code": response.status_code, | ||
| "response_headers": self._headers_to_dict(response.headers), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the things done in rest_log was to inject a reference to the log entry in the response.
There we assumed the response was always JSON data and added a key.
Here we could inject a response header (eg: API_LOG_ENTRY_URL)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added the identifier (api.log.id) of the log record, that is enough to find the log record to whoever has access to them.
Please keep in mind that I agree with the author of #501 that wrote:
This is not a migration of rest_log, this module aims to be simpler, at least in the first iterations.
A more elaborated feature (like building the full URL of the log record) can be added later, after the first simple module is merged.
api_log/models/api_log_collection.py
Outdated
| def _compute_log_ids(self): | ||
| for collection in self: | ||
| collection.log_ids = self.env["api.log"].search( | ||
| [("collection_ref", "=", "%s,%s" % (collection._name, collection.id))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see my comment above on how to search for logs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
api_log_mail/models/api_log.py
Outdated
| from odoo import api, models | ||
|
|
||
|
|
||
| class FastapiLog(models.Model): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| class FastapiLog(models.Model): | |
| class ApiLog(models.Model): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! Thanks
7674bd7 to
d1f3eec
Compare
|
The error in the tests (https://github.com/OCA/rest-framework/actions/runs/17124124402/job/48571725570?pr=554#step:8:120) does not seem to be related to these changes Stack2025-08-21 10:24:04,335 340 INFO odoo odoo.modules.loading: Loading module graphql_base (12/53)
2025-08-21 10:24:04,475 340 CRITICAL odoo odoo.modules.module: Couldn't load module graphql_base
2025-08-21 10:24:04,475 340 CRITICAL odoo odoo.modules.module: cannot import name 'HttpQueryError' from 'graphql_server' (/opt/odoo-venv/lib/python3.10/site-packages/graphql_server/__init__.py)
2025-08-21 10:24:04,477 340 WARNING odoo odoo.modules.loading: Transient module states were reset
2025-08-21 10:24:04,477 340 ERROR odoo odoo.modules.registry: Failed to load registry
Traceback (most recent call last):
File "/opt/odoo/odoo/modules/registry.py", line 87, in new
odoo.modules.load_modules(registry, force_demo, status, update_module)
File "/opt/odoo/odoo/modules/loading.py", line 493, in load_modules
processed_modules += load_marked_modules(cr, graph,
File "/opt/odoo/odoo/modules/loading.py", line 374, in load_marked_modules
loaded, processed = load_module_graph(
File "/opt/odoo/odoo/modules/loading.py", line 190, in load_module_graph
load_openerp_module(package.name)
File "/opt/odoo/odoo/modules/module.py", line 471, in load_openerp_module
__import__('odoo.addons.' + module_name)
File "/__w/rest-framework/rest-framework/graphql_base/__init__.py", line 4, in
from .controllers import GraphQLControllerMixin
File "/__w/rest-framework/rest-framework/graphql_base/controllers/__init__.py", line 1, in
from .main import GraphQLControllerMixin
File "/__w/rest-framework/rest-framework/graphql_base/controllers/main.py", line 6, in
from graphql_server import (
ImportError: cannot import name 'HttpQueryError' from 'graphql_server' (/opt/odoo-venv/lib/python3.10/site-packages/graphql_server/__init__.py)
2025-08-21 10:24:04,478 340 CRITICAL odoo odoo.service.server: Failed to initialize database `odoo`.
Traceback (most recent call last):
File "/opt/odoo/odoo/service/server.py", line 1333, in preload_registries
registry = Registry.new(dbname, update_module=update_module)
File "", line 2, in new
File "/opt/odoo/odoo/tools/func.py", line 87, in locked
return func(inst, *args, **kwargs)
File "/opt/odoo/odoo/modules/registry.py", line 87, in new
odoo.modules.load_modules(registry, force_demo, status, update_module)
File "/opt/odoo/odoo/modules/loading.py", line 493, in load_modules
processed_modules += load_marked_modules(cr, graph,
File "/opt/odoo/odoo/modules/loading.py", line 374, in load_marked_modules
loaded, processed = load_module_graph(
File "/opt/odoo/odoo/modules/loading.py", line 190, in load_module_graph
load_openerp_module(package.name)
File "/opt/odoo/odoo/modules/module.py", line 471, in load_openerp_module
__import__('odoo.addons.' + module_name)
File "/__w/rest-framework/rest-framework/graphql_base/__init__.py", line 4, in
from .controllers import GraphQLControllerMixin
File "/__w/rest-framework/rest-framework/graphql_base/controllers/__init__.py", line 1, in
from .main import GraphQLControllerMixin
File "/__w/rest-framework/rest-framework/graphql_base/controllers/main.py", line 6, in
from graphql_server import (
ImportError: cannot import name 'HttpQueryError' from 'graphql_server' (/opt/odoo-venv/lib/python3.10/site-packages/graphql_server/__init__.py)
|
SirPyTech
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review!
Please check the latest changes
| collection_model = fields.Char( | ||
| compute="_compute_collection", | ||
| store=True, | ||
| index=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought they would be useful to store the binary value of the request/response, but now I see request/response body is already Binary.
Removed
| @api.depends( | ||
| "collection_ref", | ||
| ) | ||
| def _compute_collection(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, you can compute collection_ref based on model + id; or you can compute model + id from collection_ref.
I see no big difference between the two approaches, is there any specific reason to do it in the way you suggest?
api_log/models/api_log.py
Outdated
| try: | ||
| headers_dict = {key: value for key, value in headers.items()} | ||
| return self._sanitize_headers_dict(headers_dict) | ||
| except AttributeError: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm suspicious about the headers argument, if it does not have the items method then an AttributeError would be raised.
But you're right, let's be optimistic and remove the exception handling 😄
| def log_request(self, request, override_log_values=None): | ||
| log_request_values = self._prepare_log_request(request) | ||
| if override_log_values: | ||
| log_request_values.update(override_log_values) | ||
| return self.sudo().create(log_request_values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to keep the log values in their own variable instead of using kwargs because this way more parameters can be added to the method to tweak its behavior.
I removed the if as suggested though.
api_log/models/api_log.py
Outdated
| def _prepare_log_response(self, response): | ||
| return { | ||
| "response_status_code": response.status_code, | ||
| "response_headers": self._headers_to_dict(response.headers), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added the identifier (api.log.id) of the log record, that is enough to find the log record to whoever has access to them.
Please keep in mind that I agree with the author of #501 that wrote:
This is not a migration of rest_log, this module aims to be simpler, at least in the first iterations.
A more elaborated feature (like building the full URL of the log record) can be added later, after the first simple module is merged.
api_log/models/api_log_collection.py
Outdated
| def _compute_log_ids(self): | ||
| for collection in self: | ||
| collection.log_ids = self.env["api.log"].search( | ||
| [("collection_ref", "=", "%s,%s" % (collection._name, collection.id))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
api_log_mail/models/api_log.py
Outdated
| from odoo import api, models | ||
|
|
||
|
|
||
| class FastapiLog(models.Model): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! Thanks
paradoxxxzero
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work thanks. Nothing blocking except the exception commit problem. Once it’s fixed I’ll close my old PR.
api_log/models/api_log.py
Outdated
|
|
||
| def _prepare_log_response(self, response): | ||
| headers_dict = self._headers_to_dict(response.headers) | ||
| headers_dict["API_LOG_ENTRY_ID"] = self.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not add the id to the headers of the response, only in the logs. Set it in the response instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, thanks for catching that!
I have added a quick assert in the tests too
| <field name="response_status_code" /> | ||
| <field name="response_headers_preview" /> | ||
| <field | ||
| name="response_preview" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no way to access the full response in the view anymore since the removal of the b64 field.
I think it should be kept one way or another.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Answered to the same query in #554 (comment).
fastapi_log/fastapi_dispatcher.py
Outdated
| _logger.warning("Failed to log exception", exc_info=e) | ||
| else: | ||
| # Be sure to commit/save the exception's log | ||
| env.cr.commit() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very wrong. You are committing a transaction of a failed request.
Consider this simple case :
@router.post("/create_partner")
def create_partner(env: Annotated[api.Environment, Depends(optionally_authenticated_partner_env)]):
partner = env["res.partner"].create({...})
if is_something_wrong_with_new_partner(partner):
raise UserError("Something is wrong with new partner")
return "Ok"If logging is enabled the wrong partner will be stored in database.
Please restore the separate cursor (https://github.com/OCA/rest-framework/pull/501/files#diff-67e6f9ad0c4d04c8de3dcd507c53fa5bbebf76b169c660d976f36c84c236ea68R38) or find another way to commit the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I verified this and you are correct.
I added the separate cursor in a commit with you as a co-author to fix this, could you please check?
d1f3eec to
4e1d2b8
Compare
SirPyTech
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review @paradoxxxzero, please check the latest changes
api_log/models/api_log.py
Outdated
|
|
||
| def _prepare_log_response(self, response): | ||
| headers_dict = self._headers_to_dict(response.headers) | ||
| headers_dict["API_LOG_ENTRY_ID"] = self.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, thanks for catching that!
I have added a quick assert in the tests too
fastapi_log/fastapi_dispatcher.py
Outdated
| _logger.warning("Failed to log exception", exc_info=e) | ||
| else: | ||
| # Be sure to commit/save the exception's log | ||
| env.cr.commit() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I verified this and you are correct.
I added the separate cursor in a commit with you as a co-author to fix this, could you please check?
| 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They were used to dowload the payload directly from the log form in case of a file upload/download.
Thanks for letting us know, I added them back.
| <field name="response_status_code" /> | ||
| <field name="response_headers_preview" /> | ||
| <field | ||
| name="response_preview" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Answered to the same query in #554 (comment).
4e1d2b8 to
d7bf717
Compare
paradoxxxzero
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except for missing header in exception, LGTM
api_log/models/api_log.py
Outdated
| return self.sudo().create(log_request_values) | ||
|
|
||
| def _prepare_log_response(self, response): | ||
| response.headers["API_LOG_ENTRY_ID"] = self.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be better to use kebab-case here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it is the common convention for headers keys but I didn't know that, thanks!
| return self.sudo().write(log_response_values) | ||
|
|
||
| def _prepare_log_exception(self, exception): | ||
| values = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API_LOG_ENTRY_ID header is not set in case of exception
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I added it, please check.
Note that it won't be returned in the response to the client but only stored in the log record, because in case of an exception the dispatcher simply propagates the exception raised by the endpoint.
d7bf717 to
fb9fad2
Compare
api_log/models/api_log.py
Outdated
| values = { | ||
| "stack_trace": "".join(format_exception(exception)), | ||
| "response_headers": self._inject_log_entry(dict()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is especially important to return the header in case of exception, luckily we can do this by setting exception headers attribute, see: https://github.com/OCA/rest-framework/blob/18.0/fastapi/fastapi_dispatcher.py#L46
| values = { | |
| "stack_trace": "".join(format_exception(exception)), | |
| "response_headers": self._inject_log_entry(dict()), | |
| exception.headers = getattr(exception, "headers", {}) | |
| values = { | |
| "stack_trace": "".join(format_exception(exception)), | |
| "response_headers": self._inject_log_entry(exception.headers), | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, that's why I mentioned it specifically, but I didn't know a way to inject it; the solution you proposed is awesome and works great!
I included it, please check the new changes.
fb9fad2 to
67660fd
Compare
paradoxxxzero
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Also please rename this PR to something like: [16.0][ADD] fastapi_log
This way other APIs might use the new module `api_log` to store logs.
Co-authored-by: Florian Mounier <paradoxxx.zero@gmail.com>
67660fd to
0fae517
Compare
|
Rebased to check if this is affected by fastapi/fastapi@51ad909. |
In the past months we have been proposing many improvements to #501 with akretion#8.
From more than a month ago, the branch is outdated (akretion#8 (comment)) and it is causing conflicts in akretion#8.
Here we are proposing all those improvements to OCA directly.