Enable Dark Mode!
how-to-configure-structured-json-logging-in-odoo-19.jpg
By: Sidharth P

How to Configure Structured JSON Logging in Odoo 19

Odoo 19 Technical Odoo Enterprises Odoo Community

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:

  1. Restart Odoo to enable the logging patch.
  2. Create multiple Draft Quotations.
  3. Go to a Sale Order and click "Test JSON Log".
  4. 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.


Frequently Asked Questions

Why should JSON logging be used in Odoo 19?

With JSON logging, logs become structured and easy to search and integrate with monitoring services.

Will JSON logging remove Odoo default logs?

No. It just changes the format of logs without affecting any other features of the logging system.

Is it possible to add custom data to JSON logs?

Yes, you can add custom data using the extra={} parameter.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



Recent Posts

WhatsApp