Skip to content

Conversation

@SirPyTech
Copy link

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.

@SirPyTech SirPyTech marked this pull request as ready for review August 4, 2025 10:13
@SirPyTech
Copy link
Author

@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,
Copy link
Contributor

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

Copy link
Author

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(
Copy link
Contributor

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?

Copy link
Author

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

Copy link
Contributor

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.

Copy link
Author

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):
Copy link
Contributor

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

Copy link
Author

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?

try:
headers_dict = {key: value for key, value in headers.items()}
return self._sanitize_headers_dict(headers_dict)
except AttributeError:
Copy link
Contributor

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?

Copy link
Author

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 😄

Comment on lines 152 to 153
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)

Copy link
Author

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.

def _prepare_log_response(self, response):
return {
"response_status_code": response.status_code,
"response_headers": self._headers_to_dict(response.headers),
Copy link
Contributor

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)

Copy link
Author

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.

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))]
Copy link
Contributor

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

from odoo import api, models


class FastapiLog(models.Model):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class FastapiLog(models.Model):
class ApiLog(models.Model):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! Thanks

@SirPyTech SirPyTech force-pushed the 16.0-add-fastapi_log branch from 7674bd7 to d1f3eec Compare August 21, 2025 10:21
@SirPyTech
Copy link
Author

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

Stack
2025-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)

Copy link
Author

@SirPyTech SirPyTech left a 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,
Copy link
Author

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(
Copy link
Author

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):
Copy link
Author

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?

try:
headers_dict = {key: value for key, value in headers.items()}
return self._sanitize_headers_dict(headers_dict)
except AttributeError:
Copy link
Author

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 😄

Comment on lines 152 to 153
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)
Copy link
Author

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.

def _prepare_log_response(self, response):
return {
"response_status_code": response.status_code,
"response_headers": self._headers_to_dict(response.headers),
Copy link
Author

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.

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))]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

from odoo import api, models


class FastapiLog(models.Model):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! Thanks

@SirPyTech SirPyTech requested a review from simahawk August 21, 2025 10:28
@sebastienbeau sebastienbeau added this to the 16.0 milestone Sep 1, 2025
Copy link
Contributor

@paradoxxxzero paradoxxxzero left a 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.


def _prepare_log_response(self, response):
headers_dict = self._headers_to_dict(response.headers)
headers_dict["API_LOG_ENTRY_ID"] = self.id
Copy link
Contributor

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.

Copy link
Author

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"
Copy link
Contributor

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.

Copy link
Author

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).

_logger.warning("Failed to log exception", exc_info=e)
else:
# Be sure to commit/save the exception's log
env.cr.commit()
Copy link
Contributor

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.

Copy link
Author

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?

@SirPyTech SirPyTech force-pushed the 16.0-add-fastapi_log branch from d1f3eec to 4e1d2b8 Compare September 9, 2025 11:01
Copy link
Author

@SirPyTech SirPyTech left a 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


def _prepare_log_response(self, response):
headers_dict = self._headers_to_dict(response.headers)
headers_dict["API_LOG_ENTRY_ID"] = self.id
Copy link
Author

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

_logger.warning("Failed to log exception", exc_info=e)
else:
# Be sure to commit/save the exception's log
env.cr.commit()
Copy link
Author

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(
Copy link
Author

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"
Copy link
Author

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).

@SirPyTech SirPyTech force-pushed the 16.0-add-fastapi_log branch from 4e1d2b8 to d7bf717 Compare September 9, 2025 11:05
Copy link
Contributor

@paradoxxxzero paradoxxxzero left a 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

return self.sudo().create(log_request_values)

def _prepare_log_response(self, response):
response.headers["API_LOG_ENTRY_ID"] = self.id
Copy link
Contributor

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

Copy link
Author

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 = {
Copy link
Contributor

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

Copy link
Author

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.

Comment on lines 175 to 177
values = {
"stack_trace": "".join(format_exception(exception)),
"response_headers": self._inject_log_entry(dict()),
Copy link
Contributor

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

Suggested change
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),
}

Copy link
Author

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.

Copy link
Contributor

@paradoxxxzero paradoxxxzero left a 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

@SirPyTech SirPyTech changed the title Log FastAPI requests [16.0][ADD] fastapi_log Oct 20, 2025
@SirPyTech SirPyTech force-pushed the 16.0-add-fastapi_log branch from 67660fd to 0fae517 Compare November 26, 2025 08:54
@SirPyTech
Copy link
Author

Rebased to check if this is affected by fastapi/fastapi@51ad909.
It still looks good 😎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants