The new-age applications produce huge volumes of logs. Searching through plain text is difficult to use with monitoring tools. The solution to this problem is structured logging, which means that every log entry is emitted as a JSON object.
How Odoo 19 Handles Logging Internally
All of the logging functionality of Odoo is present in odoo/netsvc.py. Odoo uses a custom class that extends Python's standard LogRecord:
# odoo/netsvc.py
class LogRecord(logging.LogRecord):
def __init__(self, name, level, pathname, lineno,
msg, args, exc_info, func=None, sinfo=None, **kwargs):
super().__init__(...)
self.perf_info = ""
self.pid = os.getpid()
self.dbname = getattr(threading.current_thread(), 'dbname', '?')
Odoo's custom log records contain the process ID, the name of the current database, and perf_info (SQL queries count and execution time added by PerfFilter). The default log message format is:
%(asctime)s %(pid)s %(levelname)s %(dbname)s %(name)s: %(message)s %(perf_info)s
It looks nice and is easily readable, but not machine-readable. Odoo 19 already uses JSON inside.
PostgreSQLHandler uses json.dumps(metadata) to insert test-run data into the ir_logging table.
But there is no native JSON formatter for files and streams.
Building a JSONFormatter
Design a formatter that will serialize all enriched LogRecords into one line of JSON:
# your_module/logging_utils.py
import json, logging
from datetime import datetime, timezone
class OdooJSONFormatter(logging.Formatter):
def format(self, record):
record.message = record.getMessage()
payload = {
'timestamp': datetime.fromtimestamp(
record.created, tz=timezone.utc).isoformat(),
'level': record.levelname,
'logger': record.name,
'pid': getattr(record, 'pid', None),
'dbname': getattr(record, 'dbname', '?'),
'message': record.message,
'perf_info': getattr(record, 'perf_info', ''),
'pathname': record.pathname,
'lineno': record.lineno,
}
if record.exc_info:
payload['exception'] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=False)
Attach it to the root logger in your module's init.py
# your_module/__init__.py
from .logging_utils import OdooJSONFormatter
from odoo import netsvc as _netsvc
_orig = _netsvc.init_logger
def _patched():
_orig()
for h in __import__('logging').getLogger().handlers:
h.setFormatter(OdooJSONFormatter())
_netsvc.init_logger = _patched
Writing Structured Logs from Your Module
Use Python's built-in extra={} parameter to attach domain-specific fields to any log call:
# models/sale_order.py
import logging
from odoo import models, api
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_sync_orders(self):
""" Trigger sync for all draft orders """
for order in self.search([('state', '=', 'draft')]):
try:
order._sync_to_external_system()
_logger.info("Order synced", extra={
'order_id': order.id,
'order_name': order.name,
'amount': order.amount_total,
})
except ConnectionError as exc:
_logger.warning("Sync failed", extra={
'order_id': order.id,
'error': str(exc),
})
The output JSON log entry will look like:
{"timestamp": "2026-06-06T09:04:53.358669+00:00", "level": "INFO", "logger": "odoo.addons.test_structured_logging.models.sale_order", "pid": 173158, "dbname": "test_app_1", "message": "External sync pulse for S00031", "perf_info": "", "pathname": "/home/cybrosys/odoo_19/custom_app/test_structured_logging/models/sale_order.py", "lineno": 12, "funcName": "_sync_to_external_system"}Everything can be searched for – no regex required.
Testing the Solution
To do testing in the interface, add a button in the Sale Order form view:
<record id="view_order_form_inherit_logging" model="ir.ui.view">
<field name="name">sale.order.form.inherit.logging</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<header position="inside">
<button name="action_sync_orders" string="Test JSON Log" type="object"/>
</header>
</field>
</record>
Test Steps:
- Restart Odoo to enable the logging patch.
- Create multiple Draft Quotations.
- Go to a Sale Order and click "Test JSON Log".
- Look into your terminal; all synced drafts will be there as a JSON object.
The netsvc.py file of Odoo 19 itself provides information like pid, dbname, and SQL query performance, along with the log entries. What you have to do now is to use JSONFormatter and add the parameter extra={}. Your log entries will be converted into the structured JSON format, which can be easily ingested by any observability stack.
To read more about How to Use Logging in Odoo 19, refer to our blog How to Use Logging in Odoo 19.