Skip to content

Logging Standards

Standards for logging in the Odoo 15 system.

Overview

Proper logging helps with debugging, monitoring, auditing, and troubleshooting. This guide defines what, when, and how to log.


Log Levels

Level Hierarchy

Level Value Usage
DEBUG 10 Detailed diagnostic information
INFO 20 General operational information
WARNING 30 Something unexpected but handled
ERROR 40 Error that prevented operation
CRITICAL 50 System-wide failure

When to Use Each Level

import logging
_logger = logging.getLogger(__name__)

# DEBUG - Detailed diagnostic info (development only)
_logger.debug(f"Processing order {order.id} with {len(order.order_line)} lines")

# INFO - Normal operations worth noting
_logger.info(f"Order {order.name} confirmed successfully")

# WARNING - Unexpected but handled situation
_logger.warning(f"Retry attempt {attempt}/3 for SMS to {phone}")

# ERROR - Operation failed
_logger.error(f"Failed to send SMS to {phone}: {error}")

# CRITICAL - System failure
_logger.critical("Database connection lost - system cannot continue")

Logger Setup

Basic Setup

import logging

# Get logger for current module
_logger = logging.getLogger(__name__)

class MyModel(models.Model):
    _name = 'my.model'

    def my_method(self):
        _logger.info("Method called")

Module-Level Logger

# At top of each Python file
import logging
_logger = logging.getLogger(__name__)

What to Log

Do Log

Event Level Example
Method entry (complex) DEBUG "Starting batch process"
Successful operations INFO "Order confirmed"
External API calls INFO "Calling JustCall API"
Retry attempts WARNING "Retry 2/3 for API call"
Handled exceptions WARNING "Invalid input, using default"
Failed operations ERROR "Payment failed"
Unhandled exceptions ERROR "Unexpected error in process"
Security events INFO/WARNING "User login attempt"

Don't Log

Data Reason
Passwords Security risk
API keys/tokens Security risk
Credit card numbers PCI compliance
Personal data (excessive) Privacy
Every loop iteration Performance
Successful health checks Noise

Log Message Format

Structure

# Format: [Context] Action - Details
_logger.info(f"[Order {order.name}] Confirmed - Total: {order.amount_total}")
_logger.error(f"[Partner {partner.id}] Failed to sync - Error: {error}")

Good Examples

# Clear, contextual, actionable
_logger.info(f"SMS sent to {phone} for order {order.name}")
_logger.warning(f"API rate limit reached, waiting {wait_time}s")
_logger.error(f"Failed to upload signature for FSM order {fsm_order.id}: {str(e)}")

Bad Examples

# Too vague
_logger.info("Done")
_logger.error("Error occurred")

# Too verbose
_logger.debug(f"Now I am going to process the order which has id {order.id} and name {order.name} and...")

# Contains sensitive data
_logger.info(f"User {user.login} logged in with password {password}")

Logging Patterns

Method Entry/Exit

def complex_operation(self):
    _logger.debug(f"Starting complex_operation for {self.ids}")
    try:
        # ... operation ...
        _logger.info(f"complex_operation completed for {len(self)} records")
    except Exception as e:
        _logger.error(f"complex_operation failed: {e}")
        raise

External API Calls

def call_external_api(self, endpoint, payload):
    _logger.info(f"Calling API: {endpoint}")
    _logger.debug(f"Request payload: {payload}")

    try:
        response = requests.post(endpoint, json=payload, timeout=30)
        _logger.info(f"API response: {response.status_code}")
        _logger.debug(f"Response body: {response.text[:500]}")
        return response
    except requests.Timeout:
        _logger.error(f"API timeout after 30s: {endpoint}")
        raise
    except requests.RequestException as e:
        _logger.error(f"API error: {endpoint} - {e}")
        raise

Batch Processing

def process_batch(self, records):
    total = len(records)
    _logger.info(f"Starting batch processing of {total} records")

    success = 0
    failed = 0

    for i, record in enumerate(records):
        try:
            self._process_single(record)
            success += 1
        except Exception as e:
            failed += 1
            _logger.warning(f"Record {record.id} failed: {e}")

        # Progress logging for large batches
        if (i + 1) % 100 == 0:
            _logger.info(f"Progress: {i + 1}/{total} ({success} success, {failed} failed)")

    _logger.info(f"Batch complete: {success} success, {failed} failed out of {total}")

Exception Logging

def risky_operation(self):
    try:
        # ... operation ...
    except ValueError as e:
        # Expected error, log as warning
        _logger.warning(f"Invalid value encountered: {e}")
        return None
    except Exception as e:
        # Unexpected error, log as error with traceback
        _logger.exception(f"Unexpected error in risky_operation: {e}")
        raise

Odoo-Specific Logging

Cron Job Logging

def _cron_process_orders(self):
    _logger.info("Cron job started: process_orders")

    orders = self.search([('state', '=', 'draft')])
    _logger.info(f"Found {len(orders)} orders to process")

    for order in orders:
        try:
            order.action_confirm()
            _logger.debug(f"Order {order.name} confirmed")
        except Exception as e:
            _logger.error(f"Failed to confirm order {order.name}: {e}")

    _logger.info("Cron job completed: process_orders")

Computed Field Logging

@api.depends('order_line.price_total')
def _compute_amount_total(self):
    for order in self:
        order.amount_total = sum(order.order_line.mapped('price_total'))
        # Only log if debugging - computed fields run frequently
        _logger.debug(f"Computed total for {order.name}: {order.amount_total}")

Constraint Logging

@api.constrains('quantity')
def _check_quantity(self):
    for record in self:
        if record.quantity < 0:
            _logger.warning(f"Negative quantity attempted for record {record.id}")
            raise ValidationError("Quantity cannot be negative")

Configuration

Odoo Logging Configuration

# odoo.conf

# Log level (debug, info, warn, error, critical)
log_level = info

# Log to file
logfile = /var/log/odoo/odoo.log

# Rotate logs
logrotate = True

# Log database queries (debug only)
log_db = False

# Log specific modules at different levels
log_handler = :INFO,odoo.addons.my_module:DEBUG

Docker Compose Logging

# docker-compose.yml
services:
  odoo:
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "5"

View Logs

# All Odoo logs
docker compose logs odoo

# Follow logs
docker compose logs -f odoo

# Last 100 lines
docker compose logs --tail=100 odoo

# Filter errors
docker compose logs odoo | grep -i error

# Filter by module
docker compose logs odoo | grep "my_module"

Structured Logging

JSON Format (Advanced)

import json
import logging

class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_data = {
            'timestamp': self.formatTime(record),
            'level': record.levelname,
            'module': record.module,
            'message': record.getMessage(),
        }
        if hasattr(record, 'order_id'):
            log_data['order_id'] = record.order_id
        return json.dumps(log_data)

Adding Context

def process_order(self, order):
    # Add context to log record
    extra = {'order_id': order.id, 'customer': order.partner_id.name}
    _logger.info("Processing order", extra=extra)

Performance Considerations

Expensive Operations

# BAD - String formatting always happens
_logger.debug(f"Processing {expensive_computation()}")

# GOOD - Only compute if debug is enabled
if _logger.isEnabledFor(logging.DEBUG):
    _logger.debug(f"Processing {expensive_computation()}")

Avoid Logging in Loops

# BAD - Log every iteration
for record in records:
    _logger.info(f"Processing {record.id}")
    process(record)

# GOOD - Log summary
_logger.info(f"Processing {len(records)} records")
for record in records:
    process(record)
_logger.info("Processing complete")

Security Logging

Audit Trail

def write(self, vals):
    # Log sensitive field changes
    if 'amount' in vals:
        _logger.info(
            f"[AUDIT] User {self.env.user.login} changed amount "
            f"from {self.amount} to {vals['amount']} on record {self.id}"
        )
    return super().write(vals)

Authentication Events

def _check_credentials(self, password):
    result = super()._check_credentials(password)
    if result:
        _logger.info(f"[AUTH] Successful login: {self.login}")
    else:
        _logger.warning(f"[AUTH] Failed login attempt: {self.login}")
    return result

Quick Reference

Log Level Selection

Is it for debugging only?           → DEBUG
Is it normal operation?             → INFO
Is something wrong but handled?     → WARNING
Did an operation fail?              → ERROR
Is the system unusable?             → CRITICAL

Template

import logging
_logger = logging.getLogger(__name__)

class MyModel(models.Model):
    _name = 'my.model'

    def my_method(self):
        _logger.info(f"[{self._name}] Starting my_method for {self.ids}")
        try:
            # ... code ...
            _logger.info(f"[{self._name}] my_method completed successfully")
        except Exception as e:
            _logger.exception(f"[{self._name}] my_method failed: {e}")
            raise

Checklist

  • Logger defined at module level
  • Appropriate log level used
  • Context included in message
  • No sensitive data logged
  • Not logging in tight loops
  • Exceptions logged with traceback
  • External calls logged