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