Skip to Content
User Manual

Odexalabs Vigil Suite for Odoo

Everything you need to understand, configure, and get the most out of your complete Odoo health monitoring, backup, and performance platform.

Getting Started

How to install Odexalabs Vigil Suite for Odoo and open the dashboard for the first time.

Supported Environments

Vigil Suite runs anywhere you control the Odoo filesystem and processes. The following table shows where it can and cannot be deployed:

EnvironmentSupportedNotes
Self-hosted on bare metal or VPS (Linux) Yes The standard install flow below covers this case
Self-hosted in Docker Yes See Docker Deployment Notes below for the specific Docker requirements
Odoo Online (SaaS) No Odoo Online restricts the filesystem and process access required for backups, the Ghost File Scanner, and post-install index creation
Odoo.sh Planned Not supported in this release. Odoo.sh compatibility is on the roadmap and will be covered in a future release.
Filesystem access is required Several features (Ghost File Scanner, optional WARNING-level log file scan for Module Impact, the post-install index on ir_attachment, OAuth callback writes) need direct filesystem and process access. Any hosted Odoo platform that abstracts the filesystem away cannot run these features. Note: Module Impact ERROR attribution still works without a log file via the in-memory error buffer.

Installation

  1. Place the module — Copy the odexalabs_health_base folder into your Odoo addons directory (the same folder where your other custom modules live).
  2. Update Apps List — Log in to Odoo as an Administrator. Go to Apps in the top menu, click Update Apps List, then confirm with Update.
  3. Find and install — In the Apps search box, type Vigil Suite. Click Install on the matching card.
  4. Open the Dashboard — A new Vigil Suite application icon appears in your main Odoo home menu. Click it to open the Visual Dashboard.

Optional Python Packages

None of these packages are required to install or use Vigil Suite. The core dashboard, health score, snapshot cron, alert rules, cron history, session heatmap, error log, and most diagnostic features work out of the box with zero extra installs. The packages below are optional add-ons. Install only the ones whose features you actually need.

The general rule:

  • psutil is the only package recommended in most cases. It improves RAM monitoring accuracy. Without it, RAM readings still work but may be less precise on some environments.
  • All other packages are per-backup-provider. Install only the one(s) matching the cloud destination you have configured. If you back up to Amazon S3, install only boto3. If you back up to Google Drive, install only the Google packages. There is no need to install all of them.
  • If you do not use the Backup Manager at all, you can skip every package except psutil.
Package Optional Feature It Enables Install Command
psutil More accurate RAM usage, process count, and container memory limit detection pip install psutil
boto3 Amazon S3 and S3-compatible backups (DigitalOcean Spaces, MinIO, etc.) pip install boto3
paramiko SFTP backups (Hetzner Storage Box, Files.com, etc.) pip install paramiko
google-auth-oauthlib
google-api-python-client
google-auth-httplib2
Google Drive backups pip install google-auth-oauthlib google-api-python-client google-auth-httplib2
dropbox Dropbox backups pip install dropbox
msal Microsoft OneDrive backups pip install msal
After installing any package Restart Odoo so the new library is picked up by running processes. For Docker deployments, rebuild or restart the container (see the Docker Deployment Notes below).

Docker Deployment Notes

Vigil Suite works on Docker but needs a few one-time setup decisions to function fully. The points below cover the most common gotchas. Skip this section if you are not running Odoo in Docker.

1. Mounting the Module

The base Odoo Docker image typically reads custom addons from /mnt/extra-addons (adjust the path if your base image uses a different addons location). Two ways to get Vigil Suite into that path:

Option A — bind-mount your local addons directory. Quick to set up; the addons folder on your host stays editable.

docker-compose.yml
services:
odoo:
image:
# e.g. odoo:17, odoo:18, odoo:19, or your own internal image
volumes:
- ./custom-addons:/mnt/extra-addons
- odoo-data:/var/lib/odoo

# Drop the odexalabs_health_base folder into ./custom-addons on the host

Option B — bake the module into a custom image. Better for reproducible deploys, CI pipelines, and Kubernetes:

Dockerfile
FROM
COPY ./odexalabs_health_base /mnt/extra-addons/odexalabs_health_base

2. Installing Python Dependencies

Do not run pip install inside a running container. Changes vanish on the next container restart. Bake the packages into your image instead:

Dockerfile
FROM
RUN pip install --no-cache-dir \
psutil boto3 paramiko msal dropbox \
google-auth-oauthlib google-api-python-client google-auth-httplib2
COPY ./odexalabs_health_base /mnt/extra-addons/odexalabs_health_base

Only include the packages whose features you actually use (see the Optional Python Packages table above). psutil is the only package recommended in most cases; the rest are per-backup-provider.

If your base image runs as a non-root user and the RUN pip install command is rejected with a permissions error, you may need to temporarily switch users for the install step or use the same approach your base image's documentation recommends for adding packages.

3. Filestore Must Be a Persistent Volume

Critical for the Ghost File Scanner and attachments in general The Ghost File Scanner compares files on disk in /var/lib/odoo/filestore/ against the ir.attachment records in the database. If your Odoo data directory is not mounted as a persistent volume, every container restart wipes the filestore. The database still has the attachment records, so all your real files now look like "missing on disk" and the scanner cannot work correctly. Always mount /var/lib/odoo as a named volume or a host bind-mount.
docker-compose.yml (correct)
volumes:
- odoo-data:/var/lib/odoo
# or a bind-mount:
# - /opt/odoo/data:/var/lib/odoo

4. PostgreSQL pg_stat_statements in Docker

The standard postgres image does not enable pg_stat_statements by default. Override the command in your compose file so the extension is preloaded:

docker-compose.yml
services:
db:
image:
# e.g. postgres:15 or whichever version your Odoo deployment targets
command:
- "postgres"
- "-c"
- "shared_preload_libraries=pg_stat_statements"
- "-c"
- "pg_stat_statements.max=10000"
- "-c"
- "pg_stat_statements.track=all"

After restarting the database container, run CREATE EXTENSION pg_stat_statements; inside your Odoo database. The full setup steps are in Section 2.

5. Container Memory Limits Are Auto-Detected

If you set a memory limit on the Odoo container (--memory 4g or compose mem_limit: 4g), Vigil Suite reads the cgroup limit rather than the host's total RAM. The RAM percentage on the dashboard reflects the limit you configured, not the bare-metal total. A container with a 4 GB limit on a 32 GB host correctly shows "85% of 4 GB" when its workers approach saturation, rather than a misleading "10% of 32 GB" reading.

cgroup v2 is checked first, then cgroup v1, then a fallback to host RAM. No configuration is required.

6. Error Attribution Works Without a Log File

Module Impact captures ERROR-level records through an in-memory logging handler installed at server start, so error attribution works even when your Odoo container writes only to STDOUT (the Docker default). No --logfile is required for the ERROR signal.

The log file scan is used as a supplemental source for WARNING-level entries (the memory buffer only catches ERROR and above). If you want the extra WARNING coverage, configure Odoo to write logs to a file inside a mounted volume:

odoo.conf (mount at /etc/odoo/odoo.conf inside the container)
[options]
logfile = /var/lib/odoo/logs/odoo.log

Make sure the directory (/var/lib/odoo/logs/) exists and is writable by the odoo user. The path must be inside the persistent data volume so logs survive container restarts. This is optional — the panel is fully functional without it.

Quick sanity check after install Open the dashboard. If the RAM percentage looks plausible, the dashboard loads, and the Ghost Files KPI shows a numeric size (even if zero), the Docker setup is wired up correctly. If RAM shows 0 MB, psutil is not installed in the image. If the Ghost Files card reads "Not scanned", that's normal until you run the scanner once.

How It Works — The Snapshot Cycle

Vigil Suite runs a background snapshot every 15 minutes. Each snapshot collects all key metrics — RAM, database size, cron status, email queue, lock counts, and more — in a single coordinated pass. The dashboard always shows data from the most recent completed snapshot.

Each metric is collected independently. If one metric (e.g., module impact analysis) fails temporarily, the others still save correctly. A snapshot is marked Complete if all metrics succeeded, or Partial if any metric had an error. You will see a yellow warning banner on the dashboard when a snapshot is partial.

The Refresh Button

Clicking Refresh on the dashboard does two things:

  1. Takes a new system snapshot immediately (same as the 15-minute cron, but on-demand), updating all stored metrics
  2. Reloads the dashboard with the fresh snapshot data and redraws all charts

Note: The most volatile metrics — Worker Load, Lock Count, Long Transactions, and Live Slow Queries — are always read live from the database on every dashboard open (not from the 15-minute-old snapshot). Clicking Refresh forces a fresh snapshot so all the other metrics (RAM, database size, cron counts, etc.) also update immediately.

Use Refresh when you have just fixed a problem (restarted stuck crons, freed a lock) and want to see the current state immediately rather than waiting up to 15 minutes for the next cron tick.

First-time open If no snapshots exist yet (fresh install), the dashboard automatically takes the first snapshot when you open it. You do not need to wait for the 15-minute cron.

PostgreSQL Configuration

Optional but recommended — unlock the full Slow Query Monitor and Module Impact features.

pg_stat_statements Extension

pg_stat_statements is a built-in PostgreSQL extension that records execution statistics for every SQL query. Vigil Suite uses it to identify which queries are slow and which modules caused them.

What you get WITH pg_stat_statements:

  • The Captured Transactions log accumulates completed slow queries (kind=slow_query) with cumulative call counts and mean/max execution times — even queries that ran hours ago
  • The Module Impact analysis scores modules based on their slow-query footprint using real PostgreSQL data
  • The dashboard shows "hardware" coverage; without pg_stat_statements a small "partial coverage" badge appears warning that historical capture depends on the opt-in software monitor alone

What you miss WITHOUT pg_stat_statements:

  • The Captured Transactions log only receives entries from the opt-in software monitor (see Section 16). If the monitor is also disabled, no completed slow queries are ever persisted.
  • The Live Slow Queries wizard is unaffected — it always reads pg_stat_activity live, so real-time diagnosis still works without pg_stat_statements.
  • Module Impact scoring uses only the in-memory buffer — less data
  • Historical slow query trending is not available (only live snapshots)

How to Enable

You need to add the extension to your PostgreSQL configuration and restart the database server. This is a one-time operation:

Step 1 — Add to postgresql.conf
# Find your postgresql.conf file. Typical locations:
# Ubuntu/Debian: /etc/postgresql/15/main/postgresql.conf
# CentOS/RHEL: /var/lib/pgsql/15/data/postgresql.conf
# Docker: set via POSTGRES_SHARED_PRELOAD_LIBRARIES env var

# Add this line (or append to existing shared_preload_libraries):
shared_preload_libraries = 'pg_stat_statements'

# Optional: increase tracked query count (default 5000)
pg_stat_statements.max = 10000
pg_stat_statements.track = all
Step 2 — Restart PostgreSQL
# On Linux (systemd):
sudo systemctl restart postgresql

# On Docker, restart the postgres container:
docker restart your_postgres_container_name
Step 3 — Create the extension in your Odoo database
# Connect as a superuser and run:
psql -U postgres -d your_odoo_database_name
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
\q
Verify it works After setup, open Monitoring → Captured Transactions and filter by Kind = Slow Query. Within one cron cycle (up to 15 min) you should see rows populated from pg_stat_statements — each with a Call Count and a mean/max duration in the State field. If the list remains empty after a cron run and there are genuinely slow queries on the system, check that the extension is loaded via SHOW shared_preload_libraries; in psql.
Important: Both steps are required Adding to shared_preload_libraries AND running CREATE EXTENSION are both mandatory. Doing only one will not work. Vigil Suite detects this split state automatically and logs a one-time warning rather than spamming errors on every snapshot.

max_connections Setting

PostgreSQL's max_connections controls how many simultaneous database connections are allowed. Vigil Suite reads this value and shows it in the Connection Pool card on the dashboard.

For production Odoo with multi-worker mode, a typical setting is:

postgresql.conf
# For a server with 8 Odoo workers:
max_connections = 200

# General rule: (workers * 3) + 20 for overhead
# Set db_maxconn in odoo.conf to match:
db_maxconn = 200

If your DB Connections card regularly shows 80%+ utilization, you should either increase max_connections in PostgreSQL or reduce the number of concurrent Odoo workers.

Health Score

Your system's vital sign — a single number from 0 to 100 that summarises everything Vigil Suite knows about your Odoo instance right now.

The Health Score starts at 100 (perfect) and loses points as problems are detected. Think of it like a report card: higher is better, and each deduction tells you exactly what to fix.

Score RangeRatingWhat It Means
80 – 100GoodYour system is running smoothly. No immediate action needed.
50 – 79FairSome issues detected. Review and address the warnings shown.
0 – 49CriticalSignificant problems found. Immediate attention recommended.

How Points Are Deducted

ConditionPoints LostHow to Fix
Overdue scheduled actions (stuck crons > 0)
Extra −20 if more than 10 stuck
−20
(up to −40)
Open Settings → Technical → Scheduled Actions, find the affected cron, and click Run Manually or edit the nextcall date
Silently failed crons (crons that crashed with an error) −15 Check Cron History for the error message and fix the underlying problem
Failed outgoing emails > 0
Extra −20 if more than 50 failed
−20
(up to −40)
Check Settings → Technical → Outgoing Mail Servers; click Retry on the dashboard
Backup status is not "success" or "pending"
Note: "pending" (backup scheduled but not yet run) does not penalise
−10 Set up a Backup Job and make sure it runs successfully
Database bloat (dead rows) above 20%
Extra −10 if above 80%
−10
(up to −20)
Run the Table Bloat Analyzer and vacuum large tables
RAM at 70%+ of total system memory −5 Early warning — monitor for growth
RAM at 85%+ of total system memory −10 Scale server memory or reduce Odoo worker count
RAM at 95%+ of total system memory −20 Immediate action needed — risk of out-of-memory crashes
3 or more sessions waiting on a DB lock −5 Check Captured Transactions (filter by Kind = Lock Wait) and the Live Locks wizard; kill stuck transactions
10 or more sessions waiting on a DB lock −10 Investigate and terminate the blocking transaction immediately
1+ long-running transactions (over 30 seconds) −5 Check Captured Transactions (filter by Kind = Long Transaction) or Live Slow Queries wizard; consider killing if stuck
5+ long-running transactions −10 Multiple runaway queries — investigate immediately

Hard Floors — Score Caps

In addition to point deductions, some serious conditions cap the maximum score regardless of how healthy other signals look:

ConditionScore Capped AtWhy
Last backup status is "failed" (backup ran and failed)50A failed backup breaks your disaster recovery promise. This is incident-level regardless of other signals
Last backup status is "stale" (configured but hasn't run within its frequency + 1 hour grace)70Backup protection is degrading. The job is configured but no longer producing fresh recovery points
Last backup status is "none" (no backup job configured)75A production system with zero backup coverage cannot present as fully healthy regardless of other signals
Snapshot collection was partial (some metrics couldn't be collected)60Missing data means we can't honestly say the system is healthy
20 or more sessions waiting on DB locks40This level of lock contention means users are experiencing severe slowdowns
10 or more long-running transactions4010+ runaway queries usually means a stuck migration or serious runaway process
Score below 50 but everything looks fine? Check the backup status first — a backup job that ran and failed hard-caps the score at 50 even if every other metric is perfect. Also look for the partial-snapshot banner at the top of the dashboard.

Score consistently below 70?

A persistently low health score often points to deeper architectural issues — misconfigured workers, inefficient custom modules, or database growth problems that require expert analysis.

Get a Professional Audit →

KPI Cards

The four headline metrics at the top of your dashboard — your first stop when something seems wrong.

Health Score

Your overall system score (0–100). The border colour changes: green = Good (80+), yellow = Fair (50–79), red = Critical (below 50). A sparkline shows the trend over the last 24 hours of snapshots.

Database Size

Total size of your PostgreSQL database in MB. This is the database itself — not the filestore. Also shows the count of ghost files detected in the most recent scan.

RAM Usage

Memory consumed by all Odoo processes combined (master + workers). The number is the Proportional Set Size (PSS) — shared library pages are shared fairly between processes. A sparkline shows the trend. Requires psutil for accuracy.

Last Backup

Date and time of the last successful automated backup. Shows "Never" if no backup jobs are scheduled. Shows "Failed" if the last run failed. This card drives the −10 / cap-50 health score penalty.

Sparklines on Cards

The small chart lines under each metric value show the trend from the last 24 hours of snapshots (up to 96 data points at 15-minute intervals). A flat line means the metric is stable. A rising line means it is growing. Sparklines are drawn from historical snapshot data, not live readings.

RAM reading shows 0 MB? This means psutil is not installed. Without it, the module reads RAM from /proc/self/status on Linux (single process only) or falls back to the peak historical RSS from the OS — which can appear as 0 on some environments. Install psutil for reliable per-snapshot RAM readings: pip install psutil then restart Odoo.

System Load — Docker vs Bare Metal / VPS

The System Load gauge behaves differently depending on how your Odoo is deployed. Understanding this helps you interpret the numbers correctly.

Multi-Worker Mode (Bare Metal / VPS Production)

When Odoo is running in multi-worker mode (the workers setting in odoo.conf is greater than 0), the gauge shows Worker Load:

  • Busy Workers — How many of your configured workers are currently executing a database query
  • Total Workers — The number configured in workers = in odoo.conf
  • Percentage — Busy ÷ Total × 100

Example: If you have 8 workers and 3 are currently handling requests, the gauge shows 3/8 (37%).

This is read live from pg_stat_activity every time you refresh the dashboard — it is not from the 15-minute snapshot. Worker load can spike and resolve within seconds; the live reading gives you the current picture during an incident.

Threaded Mode (Docker / Development)

When workers = 0 (Odoo's default threaded mode, common with Docker setups), there are no separate worker processes. Instead, Odoo uses Python threads inside a single process. In this mode, the gauge shows DB Load:

  • Active Connections — Database connections that are currently executing a query (not idle)
  • Max Connections — PostgreSQL's configured max_connections limit
  • Percentage — Active ÷ Max × 100

This reflects how saturated the database connection pool is. 80%+ means many threads are active simultaneously.

Which mode is your Odoo in? Check your odoo.conf file. If it has workers = 4 (or any number above 0), you are in multi-worker mode. If workers is missing, set to 0, or you are using Docker without explicitly setting workers, you are in threaded mode.

Additional Metrics Below the Gauge

  • Active Users — Users who were active in the last 10 minutes, based on the bus_presence heartbeat table. Odoo browsers send a heartbeat every ~30 seconds, so any user who closed their tab up to 10 minutes ago may still show as active. This is a near-real-time count, not a perfect "open right now" count.
  • DB Connections — Current connections to PostgreSQL vs the maximum limit (e.g., "12 / 200").
  • Idle in Transaction — Connections open but sitting idle inside an open transaction. These hold locks. High numbers here indicate application-level issues (transactions started but not committed).
  • Waiting — Sessions blocked by a lock, waiting for another session to release it. This should always be 0. Any non-zero value means something is being blocked right now.

Failed Emails

The panel on the right shows outgoing emails that failed to send, with the recipient address and reason. The count is scoped to your current company (multi-company aware). To retry a failed message, open it from Settings → Technical → Email → Emails (filter on State = Exception) and use the standard Odoo Retry action on the record.

Seeing "Connection Refused" email errors? This typically means your outgoing SMTP server is unreachable or misconfigured. Go to Settings → Technical → Outgoing Mail Servers and verify the host, port, and credentials.

Database Vitality

A real-time view of your PostgreSQL database's performance, health, and resource utilisation.

Cache Hit Ratio

This metric shows what percentage of database reads are served from PostgreSQL's memory buffer (shared_buffers) rather than from disk. It is calculated as:

Calculation
Cache Hit Ratio = (blks_hit) ÷ (blks_hit + blks_read) × 100

Source: pg_stat_database for the current database
This is cumulative since the last stats reset — not per-session
RatioStatusWhat It Means
99%+ExcellentAlmost all reads from memory. Ideal state for production.
95–99%AcceptableSome disk reads. Normal on large databases that exceed RAM.
Below 95%Needs attentionToo many disk reads. Server needs more RAM or PostgreSQL shared_buffers needs tuning.

Impact on Health Score: Cache hit ratio is shown as an informational metric — it does not directly deduct from the health score, and it does not feed into Module Impact scoring. It is a supporting signal for capacity-planning alerts you configure via alert rules.

Live Slow Queries

The count of SQL queries currently running longer than the configured threshold, read live from pg_stat_activity. This is a single, honest number of what is happening right now — it does not include historical data, and queries disappear from this count the moment they complete.

Threshold: controlled by the system parameter odexalabs.vigil.live_slow_threshold_ms (default 1000 ms). Lower values (e.g. 500) catch more queries but generate more noise from routine slow catalog reads. Higher values (e.g. 2000+) only surface queries that are genuinely a problem. Change the value under Settings → Technical → Parameters → System Parameters — no restart needed.

Click the number to open the Live Slow Queries wizard. The wizard reads the same live slice from pg_stat_activity, so the count and the wizard always match. Each row is classified with a Type badge:

  • Long TX — the query is inside a transaction that has been open for more than 5 seconds
  • Lock Wait — the query is blocked waiting for a database lock
  • Slow Query — a plain slow query (neither of the above)

Historical view: once a query completes it disappears from this count and from the wizard. Completed slow queries are captured to the Captured Transactions log by the cron every 15 minutes — browse the full history under Monitoring → Captured Transactions.

Coverage badge: if pg_stat_statements is not installed a small "partial coverage" badge appears under the count. In that mode only queries captured by the opt-in Slow Query Monitor (see Section 16) get persisted to the log; installing pg_stat_statements provides full historical coverage.

Lock Count

The number of database sessions currently waiting to acquire a lock that another session is holding. This is read live from pg_stat_activity WHERE wait_event_type = 'Lock'.

  • 0 — Normal. No contention.
  • 1–2 — Minor. Usually resolves on its own.
  • 3–9 — Concerning. Something is holding a lock longer than it should. Health score −5.
  • 10–19 — Significant contention. Users are experiencing slowdowns. Health score −10.
  • 20+ — Incident. Multiple operations are blocked. Health score hard-capped at 40.

Click View Locks to open the Live Locks wizard, which shows exactly which process holds the lock and which ones are waiting.

Long Transactions (merged into Live Slow Queries)

Long-running database transactions (running > 5 seconds) no longer have a separate KPI card — they are folded into the Live Slow Queries count above, with a red Long TX badge in the wizard so you can distinguish them from ordinary slow queries. This unification is deliberate: a query slow enough to trip the threshold and a transaction older than 5 seconds are both "queries running slower than they should right now", and one number is easier to reason about than two overlapping counts.

Long-running transactions remain dangerous because:

  • They hold database locks, blocking other operations
  • They prevent PostgreSQL's VACUUM from reclaiming space (increasing bloat)
  • On large tables, a stuck transaction can cause performance to degrade over minutes and hours

Detection remains the same: pg_stat_activity WHERE (now() - xact_start) > interval '5 seconds'. The kill button in the Live Slow Queries wizard applies only to Long TX and Lock Wait rows — it is hidden for Slow Query rows because a completed slow query has no live PID to terminate. Historical long transactions are persisted to the Captured Transactions log with kind='long_tx'.

Index Health

The Index Health card replaces the earlier "Index Bloat" number and combines three separate signals about the state of your indexes. The headline shows whichever signal has the most information right now:

  • N dup — count of drop-safe duplicate index members (REDUNDANT or DUPLICATE badges only; constraint-backed members are never counted here). Duplicates are the highest-confidence cleanup category.
  • Size (e.g. 850 MB) — Actionable Index Waste: bytes in unused btree indexes that do not match any Odoo ORM naming pattern. These are the strongest candidates to investigate for removal.
  • N over — count of tables where total index size exceeds heap size (write-amplification signal, not the same as classic bloat).
  • — every signal is at zero. Card colour is green.

Card colour is derived from the combined signals: green = healthy, amber = attention (any signal present but under thresholds), red = critical (large actionable waste, over-indexed table > 1 GB, or more than 5 drop-safe duplicates).

Why this is not called "Index Bloat"

True index bloat means internal wasted space inside an index due to page splits, dead entries, and churn. Accurately measuring it requires the pgstattuple PostgreSQL extension, which is not present on most Odoo installs and cannot be assumed. What this module measures reliably from stock PostgreSQL catalogs is different: which indexes have never been scanned since the last stats reset, and how much disk they occupy. That is unused index waste, not bloat, so the KPI is labelled honestly. Click the card to open the Index Health wizard for the full three-panel breakdown (Duplicates, Unused, Over-Indexed).

Drop-safety tiers used by the wizard and the KPI

Every unused btree index on an active table is classified into one of four tiers by the same shared classifier used by the wizard and the dashboard KPI — so the two never disagree:

  • Protected — unique or exclusion constraints. Never suggested for drop; would allow duplicate or overlapping data. Also includes non-btree access methods (GIN, GiST, BRIN, hash) which are typically used for full-text search or JSONB and often show zero scans even when actively used (executor takes a different code path that doesn't increment idx_scan).
  • ORM (amber badge) — btree indexes matching Odoo naming conventions: {table}_{field}_index, {table}_{columns}, indexes on _rel tables, or names ending in _idx. Created by core Odoo, Enterprise, or a custom module (PostgreSQL doesn't record who created an index, so this is a name-based signal). Do not drop just because scans are zero — the ORM will re-create them on module upgrade, or a monthly report or record rule may need them once a week.
  • Investigate (red badge, "Unknown") — btree indexes matching no ORM pattern. Most likely added manually by a DBA or migration script. Highest-priority cleanup candidates after verification.

The Actionable Index Waste number on the KPI card counts only the Investigate tier — the bytes a DBA can safely act on. ORM and Protected bytes are reported in the wizard for transparency but are not summed into the headline.

Note: If PostgreSQL stats were reset recently (less than 24 h ago), the Actionable Waste number is suppressed as N/A (stats too fresh) because scan counters have not had time to accumulate. Duplicate and Over-Indexed signals do not depend on scan stats and continue to be reported normally.

Connection Pool Detail

The connection pool panel shows a live breakdown of all database connections:

MetricWhat It MeansHealthy Value
ActiveConnections currently executing a SQL queryVaries with load
IdleConnected but not doing anythingNormal to have many
Idle in TransactionIn an open transaction but not currently executingShould be near 0
WaitingBlocked by a lockMust be 0
MaxPostgreSQL's max_connections settingSet in postgresql.conf
Utilisation %(Active + Idle + Idle in Transaction) ÷ MaxBelow 70%

The connection pool is sampled and stored every 15 minutes alongside the snapshot, giving you a trend chart of pool utilisation over time.

Cron Health & Failures

Scheduled actions (crons) are background jobs Odoo runs automatically. When they fail silently, things break without anyone noticing.

How Vigil Suite Characterises Crons

Every time a scheduled action runs, Vigil Suite records its start time, end time, duration, and whether it succeeded or raised an exception. This is stored in the Cron History table. Based on this data, crons are classified into the following types:

TypeBadgeWhat It MeansHealth Score Impact
Overdue Amber The cron's scheduled next-run time is more than 1 hour in the past. This means it missed its scheduled slot. Causes: server restart, worker overload, or the Odoo cron scheduler hasn't run yet. The cron may run soon on its own. −20 (up to −40 if 10+)
Silently Failed Red The cron ran, but raised an unhandled Python exception. Odoo incremented its failure_count but kept the cron active. It will try again on the next scheduled run. The error is logged but not surfaced to users by default. −15
Auto-Deactivated Dark Odoo automatically set the cron to inactive after repeated failures. This cron will NOT run again until an administrator re-enables it manually in Settings → Technical → Scheduled Actions. −15 (counted as failed)
Normal / Healthy Green The cron is active and ran successfully within its expected interval. No action needed. 0
Vigil Suite's own crons are excluded from the overdue count. The monitoring module's three background jobs (snapshot, heatmap, backup scheduler) are intentionally excluded from the "stuck cron" count to prevent the health score from penalising itself during development restarts or low-traffic windows.

Fixing Stuck Crons

The Stuck & Failed Actions section on the dashboard surfaces overdue, silently-failed, and auto-deactivated crons so you can see them at a glance. To reset a cron, open Settings → Technical → Scheduled Actions, locate the affected entry, and either click Run Manually to execute it now or edit the Next Execution Date so it runs on the next cron tick. For auto-deactivated crons, also flip the Active field back on after fixing the underlying error.

Cron Performance (24h)

Below the stuck/failed list, a performance table shows every scheduled action that ran in the last 24 hours:

  • Runs — How many times the job executed in the past 24 hours
  • Avg (s) — Average execution time in seconds across all runs
  • Max (s) — The single slowest run. Highlighted red if over 10 seconds.
  • Errors — Number of runs that ended with an exception

This table is ideal for spotting crons that are technically running but taking longer each day — a sign that they are processing a growing backlog.

What's a normal cron duration? Most standard Odoo scheduled actions should complete in under 5 seconds. Custom module crons doing data processing may legitimately take 30–60 seconds. If a cron regularly takes over 2 minutes, it is likely doing too much per run and should be refactored to process data in smaller batches.

Module Impact

See which modules are pulling down your health score the most, so you know where to look first.

Module Impact ranks the top five modules by their measurable effect on the health score. Each row is shown in three columns: the Module name, an Est. pts figure (the health-score deduction attributed to this module) with small / badges showing how many stuck / failed crons it owns, and a Load badge (raw signal count for correlational noise). Clicking ⏰ or ⚡ opens the filtered scheduled-actions list for one-click drill-down; clicking Load opens a per-signal breakdown dialog.

Est. pts — Health-Score Impact

Est. pts mirrors the health-score deductions in _calc_health_score. Two categories map to whole-number penalties:

CategoryHealth-score penaltyTrigger
Stuck-cron penalty−20 (or −40 when the system-wide stuck-cron total exceeds 10)Module owns any cron whose nextcall is more than 1 hour overdue
Failed-cron penalty−15Module owns any cron with failure_count > 0

The penalty applies once per category, not per module. If two modules both own stuck crons, both rows display the same −20 value — fixing either one alone will not recover the amount. Every module owning items in that category has to be resolved before the score improves. Correlational signals (slow queries and log entries) do not directly deduct from the health score, so they contribute to Load but not to Est. pts.

Multiple modules showing the same Est. pts value? That is expected — each of them contributes to the same category penalty. The row order within an equal-points tier is decided by tiebreakers: first the module with the most owned stuck / failed crons, then the noisier one on correlational signals, then alphabetical. Whichever row sits at the top is where you should look first.

Load — Raw Signal Count

Load is a composite signal count. It exists to surface modules whose crons are healthy today but whose queries or log lines are already producing noise — an early-warning that they may fail soon. Weights per signal:

SignalLoad weightDirectly deducts from health score?
Stuck cron owned by module (⏰)+5 per cronYes (via Est. pts)
Failed cron owned by module (⚡)+7 per cronYes (via Est. pts)
Slow query traced to a module table (in-memory buffer)+3 per queryNo — correlational
Slow query via pg_stat_statements (mean_exec_time > 1 s)+2 per queryNo — correlational
ERROR or WARNING log entry attributed to module+1 per entryNo — correlational

How Log Entries Are Attributed to Modules

Error and warning attribution uses two sources, merged with a per-entry fingerprint dedupe:

  • In-memory error buffer — a Python logging handler captures every ERROR record into a per-database ring buffer at server start. This works regardless of your --logfile configuration and is what makes Module Impact populate on Docker / stderr-only setups where no log file exists.
  • Log file scan (if --logfile is configured) — reads only the bytes appended since the previous snapshot (capped at 200 KB per cycle) so counts do not inflate 96× per day. Adds WARNING-level entries that the ERROR-only buffer does not carry.

For each captured entry, the owning module is identified by walking three checks in order:

  1. Traceback frame — the innermost File ".../addons//..." line in the Python traceback. This is the code path that actually raised the exception and is treated as the strongest attribution signal.
  2. Logger name — the record's logger of the form odoo.addons..models.X, used when no traceback is present.
  3. Substring match — the module name appearing anywhere in the message text, used as a last-resort fallback.

This replaces the earlier substring-only attribution, which counted a line like odoo.models.execute_query failed for res.partner as coming from base when the real culprit was three frames up in a custom add-on.

Slow-Query Attribution

Slow queries are attributed by walking the query's SQL tokens in document order and matching the first non-keyword token against the table-to-module map (ir_model_data joined against each model's actual _table attribute). SQL keywords (SELECT, FROM, WHERE, etc.) are skipped so they do not shadow real table tokens. Iteration is deterministic, so the same slow query always attributes to the same module across refreshes.

What Is Filtered Out

Rows attributed to base, web, bus, iap, and odexalabs_health_base are suppressed. The four core modules are excluded because attribution to them is almost always an artefact of the algorithm (a query joining res_partner lands on base even when the caller is a custom module), and an admin cannot act on Odoo-core issues anyway. odexalabs_health_base is excluded because its own subsystem failures (backup, alerts) are already surfaced as top-level KPIs on the dashboard — showing them here again would drown out actionable signal from third-party modules.

User-created crons that have no XMLID (typically ones added through the Scheduled Actions UI) are grouped under a single User-defined row rather than suppressed, so a broken user cron is still visible.

Nothing showing up? The most common causes are: (a) the database is genuinely quiet — no stuck / failed crons and no errors; (b) all your issues are on core modules; (c) all your issues are on this module itself (its backup / alert failures already show as top-level KPIs). If you triggered a real error and the panel is still empty, wait one 15-minute snapshot cycle for the cron to publish the new counts.

Session Heatmap

See when your users are most active — essential for scheduling maintenance and understanding usage patterns.

The heatmap is a 7-day × 24-hour grid. Each cell represents one hour of one day of the week (e.g., Tuesday at 2 PM). The colour intensity reflects how many users were active during that hour on average over the last 14 days. Darker = more users.

How to Use It

  • Schedule maintenance windows — Pick the lightest-coloured cells (lowest activity). For most businesses, this is early Sunday morning.
  • Schedule backups — Run backups during off-peak hours so they don't compete with users for database resources. Backups can temporarily slow down the database.
  • Plan VACUUM runs — Running VACUUM ANALYZE causes significant disk I/O. Schedule it when users are offline.
  • Spot unusual activity — If you see unexpectedly high activity at 3 AM on a weekday, that could indicate an automated process, a runaway cron, or unauthorised access.
  • Capacity planning — If peak activity regularly approaches your worker count, you may need to add workers before the load causes slowdowns.
Heatmap is empty or sparse? Data is sampled every 15 minutes by the snapshot cron. Wait at least 48 hours after installation for enough data to form a meaningful pattern. After 14 days, the heatmap reflects your real usage rhythm.

Storage & Bloat

Understand where your disk space is going and whether your database has accumulated unnecessary dead data.

File Storage Breakdown (Pie Chart)

Shows disk space used by binary file attachments stored in Odoo's filestore, broken down by file type. The top 7 file types are shown individually; everything else is grouped into an "other" slice. This ensures the pie always represents 100% of your attachment storage, not just a partial view.

Only binary-type attachments are included — these are files that actually exist on disk. URL-type attachments (external links) have no disk usage and are excluded.

File Storage is NOT the same as Database Size The filestore (data_dir/filestore/) holds actual uploaded files — PDFs, images, Excel files. The PostgreSQL database holds records, metadata, and indexes. They are stored in completely separate locations. On typical Odoo instances, the filestore is larger than the database. The two numbers are not directly comparable.

Table Bloat (Dead Rows)

Every time you UPDATE or DELETE a row in PostgreSQL, the old version of that row is not immediately removed. It stays on disk as a "dead tuple" — invisible to queries but occupying space. This is how PostgreSQL implements MVCC (Multi-Version Concurrency Control), which allows multiple transactions to see consistent data simultaneously.

What Dead Tuples Are

A dead tuple is a row that was either updated or deleted and is no longer visible to any active transaction. PostgreSQL's autovacuum process periodically scans tables and marks dead tuples as available space to be reused.

What VACUUM Does

VACUUM removes dead tuples from tables and makes their disk space available for reuse. It does not return space to the operating system (VACUUM FULL does, but at higher cost). VACUUM ANALYZE additionally refreshes the query planner's statistics about table contents, which helps PostgreSQL choose efficient query plans.

When Bloat Becomes a Problem

  • Below 5% — Normal. Autovacuum is keeping up.
  • 5–20% — Slightly elevated. Monitor but no immediate action needed.
  • 20–80% — Health score −10. Run the Bloat Analyzer and vacuum the affected tables.
  • Above 80% — Health score −20. Serious degradation. Autovacuum is not keeping up with write volume. Run manual VACUUM and review autovacuum settings.

How the Bloat Score Is Calculated

The bloat score (dead tuple ratio %) is calculated as:

Calculation
Bloat Score = SUM(n_dead_tup) ÷ (SUM(n_live_tup) + SUM(n_dead_tup)) × 100

Source: pg_stat_user_tables — covers ALL user tables
This is the database-wide average, not just the worst tables

The Table Bloat Analyzer

Click Rescan (or Analyze Bloat) to open the Table Bloat Analyzer wizard. It shows only tables larger than 1 MB with more than 5% dead rows. Small tables (under 1 MB) are intentionally excluded because:

  • They have no measurable performance impact regardless of their dead-row percentage
  • Odoo's own background operations (session writes, cron bookkeeping, mail queue) continuously create new dead rows on small system tables immediately after any vacuum — they will always show some bloat

From the wizard, you can vacuum individual tables or click Vacuum All to clean all listed tables at once. After vacuuming, the wizard automatically re-scans to show the updated state. VACUUM requires System Administrator access.

Best time to run VACUUM VACUUM causes significant disk I/O and can slow down users during peak hours. Schedule manual VACUUM runs during the quiet periods shown in your Session Heatmap (Section 9).

Module Install / Upgrade Events

Vigil Suite automatically logs every time a custom module is installed or upgraded, recording:

  • Module name and version change (old version → new version)
  • Date and time of the upgrade
  • Which Odoo user triggered it

Standard Odoo and OCA modules are not logged — only your custom or third-party modules. This log helps you correlate problems with deployments. If performance dropped or errors started appearing, check this log to see if a module was upgraded at the same time.

What is NOT captured Only module upgrades triggered through Odoo's interface are captured. Git pulls, Docker image swaps, and file-level code changes that do not go through Odoo's module upgrade mechanism are NOT logged here — they leave no trace in the Odoo database.

Ghost Files Explained

What ghost files are, how Vigil Suite finds them, and how to safely clean them up.

What Are Ghost Files?

A ghost file is a file that exists on disk in the Odoo filestore directory but has no corresponding record in the Odoo database (the ir.attachment table). These orphaned files accumulate over time from:

  • Records that were deleted but whose attachment records were not cleaned up
  • Failed or interrupted uploads where the file was written to disk before the database record was created
  • Module upgrades that removed or renamed attachment models
  • Database restores where the filestore was not restored to match

How Ghost Files Are Detected

Vigil Suite scans the filestore directory and compares every file on disk against the checksums stored in ir.attachment. The matching uses the checksum stored in the database, not the filename — because Odoo stores files using their SHA-1 hash as the filename. A file is a ghost if its checksum does not match any active attachment record in the database.

The following special directories inside the filestore are always excluded from the scan:

  • odexalabs_vigil_quarantine — Vigil Suite's own quarantine folder (files moved here are already being managed). Pre-rebrand installs used odexalabs_quarantine; the module's post_init_hook migrates the folder automatically on upgrade.
  • sessions — Odoo session files (not attachment files)
  • assets and web_assets — Frontend asset cache files (legitimate, not orphans)

Zero-byte (empty) files are always skipped — they cannot be useful content regardless of database status.

Files modified within the last 1 hour are excluded — they may belong to uploads that are in-progress and have not yet been committed to the database. This guards against false positives from slow or network-mounted filesystems.

How to Scan and Clean Ghost Files

  1. From the dashboard KPI card for Database Size, click the Scan button (or go to Diagnostics → Ghost File Scanner).
  2. The wizard opens and immediately starts scanning. Large filestores are scanned in chunks (time-bounded segments) to avoid browser timeouts. Progress is updated automatically.
  3. When the scan finishes, you see the count and total size of detected ghost files.
  4. Click Move to Quarantine to safely move the ghost files to a quarantine folder rather than permanently deleting them. They are still on disk but separated from the active filestore.
  5. Verify nothing is broken in your Odoo (check that images, PDFs, and documents still appear correctly).
  6. After confirmation, click Permanently Delete Quarantine to free the disk space.
Never delete ghost files without quarantining first The ghost-file detection algorithm is based on database checksums, which are reliable. However, unusual configurations or very recent database restores can cause false positives. Always move to quarantine first and verify Odoo is working before permanently deleting.
Race-safe quarantine Immediately before moving each file, Vigil Suite re-checks ir_attachment on a fresh connection to confirm no live attachment has claimed it in the seconds since the scan completed. On a busy system where new uploads are hitting the filestore during quarantine, this guard prevents the rare case where a ghost was quarantined milliseconds after becoming a real attachment. Every attempted move — successful, failed, or aborted by the recheck — leaves a durable record in the Quarantine Log (Monitoring → Quarantine Log) with the outcome and reason.

Ghost File Count on the Dashboard

The ghost file count shown on the KPI card (Database Size card) is from the most recent scan. It does not update automatically — you need to run the Ghost File Scanner to get a fresh count. After scanning, the count in the latest snapshot is updated and the KPI card reflects the new number.

Error Log

A real-time view of recent Python errors from your running Odoo server — no file access or shell needed.

LIVE ERROR BUFFER
!! ERROR: sale.order _compute_amounts() — ZeroDivisionError: division by zero
!! ERROR: stock.picking _action_done() — ValidationError: Insufficient stock
WARN: ir_cron job exceeded timeout
... monitoring all Odoo and Python errors

How It Works — The Ring Buffer

When Vigil Suite is installed, it attaches a custom handler to Odoo's logging system. This handler intercepts every ERROR-level (and above) log entry from both Odoo's own logger and the Python root logger (which catches werkzeug, WebSocket, and other non-Odoo errors).

Captured errors are stored in an in-memory ring buffer — a fixed-size list that holds the most recent 50 error entries. When the 51st error arrives, the oldest one is dropped. This buffer lives entirely in Python memory — it is never written to the database, which means it survives database locks and errors, but it is cleared when the Odoo server restarts.

The buffer is partitioned by database name, so in multi-database Odoo setups, each database sees only its own errors — not errors from other tenants on the same server.

How Many Errors Are Shown?

The ring buffer holds a maximum of 50 entries. The dashboard displays the most recent 5 errors by default.

Duplicate errors that occur within the same second are deduplicated to prevent one bug from flooding the entire buffer (e.g., an error that fires on every HTTP request would otherwise fill the buffer immediately).

What Is Captured

For each error, the buffer stores:

  • Timestamp — When the error occurred (UTC)
  • Error message — If the log message is generic (e.g., "Exception during request"), Vigil Suite digs into the exception info and shows the actual exception class and message (e.g., "ValueError: expected a number, got str")
  • Logger name — Which Odoo module or Python component logged the error
  • Level — ERROR or CRITICAL

Works in All Environments

Because the capture happens at the Python logging layer — not by reading a log file — this works identically whether Odoo's logs go to a file, to STDOUT (Docker), or nowhere. No log file path configuration is needed.

Same error appearing repeatedly? A recurring error indicates a bug in a custom module, a misconfiguration, or a compatibility issue. The message shows which model and method caused the crash. Share this output with your developer so they can locate the offending code path.

Last 24h Activity Feed

An automatically compiled incident timeline for the past 24 hours — the most powerful diagnostic tool in the dashboard when something goes wrong.

Why This Is Valuable

When a user reports "the system was slow around 2 PM yesterday," you need to know exactly what happened. The 24h Activity Feed scans all snapshots from the past 24 hours (up to 96 snapshots at 15-minute intervals) and produces a human-readable timeline of notable events: when problems started, how long they lasted, and whether they resolved on their own.

Without this feed, diagnosing a historical incident requires manually scrolling through the Snapshot History list and comparing numbers row by row. The feed does that work for you automatically.

What Events Are Detected

Event TypeTriggers WhenResolves When
Health Score drop Score falls below 70 (warning) or 50 (critical) Score recovers to 70 or above
High RAM usage RAM reaches 85% of total system memory RAM drops below 70% of total memory
Session capacity warning Active sessions exceed 80% of max_connections Sessions drop below 80% capacity
Overdue cron detected Any cron has been stuck for over 1 hour All crons are back on schedule
Failed cron detected Any cron has a non-zero failure count All cron failures are cleared
Database lock contention 3 or more sessions waiting on a lock Lock count drops below 3
Long transaction detected Any transaction running longer than 30 seconds All long transactions end

Auto-Resolve Detection

Events automatically show a "Resolved at HH:MM" timestamp when the condition clears in a subsequent snapshot. This tells you immediately whether the problem was a brief spike (resolved within 15 minutes) or a prolonged incident (stayed for hours).

An event without a resolved timestamp means the condition was still active as of the most recent snapshot — the problem may still be ongoing.

Peak Values

Below the event timeline, the feed shows the peak (worst) values recorded over the 24-hour window:

  • Lowest Health Score — The worst score recorded, with timestamp
  • Peak RAM Usage — Highest RAM reading in the period
  • Peak Sessions — Most users online simultaneously
  • Peak Lock Count — Highest number of simultaneous lock waiters
  • Peak Long TX Count — Most simultaneous long-running transactions

Module Incidents Summary

The feed also tracks which modules appeared in the Module Impact analysis across the 24-hour window. A module that consistently appears across multiple snapshots is likely the source of a persistent problem — not a one-time spike.

Practical use case A user says "invoicing was broken around 10 AM." You open the 24h feed and see: "Failed cron detected at 09:45 / Resolved at 10:30." The event points to a specific cron failure window. You then open Cron History (Monitoring menu), filter to that time range, and find the exact error message that caused the failure. Total investigation time: under 2 minutes.

Monitoring Menu

Detailed historical views and diagnostic tools accessible from the Monitoring dropdown in the top navigation bar.

Captured Transactions

A unified log of three kinds of database events that the module captures automatically via the 15-minute cron. Access via Monitoring → Captured Transactions. The list opens grouped by Kind by default.

Each record carries a Kind badge that tells you what the row represents:

KindWhat it meansSource
Long TransactionA transaction that was open for more than 5 seconds when the cron scannedLive scan of pg_stat_activity
Lock WaitA session waiting on a database lock for more than 5 secondsLive scan of pg_stat_activity WHERE wait_event_type = 'Lock'
Slow QueryA completed slow query captured from pg_stat_statements or the opt-in Slow Query Monitor bufferPersisted from cumulative PostgreSQL stats + software monitor drainage

Each record shows:

  • Detected At — When the cron scan captured this row
  • Kind — badge as described above
  • Duration (s) — How long the transaction/query had been running
  • Call Count — For Slow Query rows: cumulative call count from pg_stat_statements. Deduplicated within a 15-minute window keyed on the first 100 characters of the query.
  • Stateactive, waiting, ended, or a pg_stat_statements summary line for slow-query rows
  • Wait Event / Blocked By PIDs — For Lock Wait rows: the specific lock type and the PIDs holding it
  • Query Preview — First 100 characters of the SQL query; full query available on the form view
  • User / Client Address — DB user and connection IP (populated for Long TX and Lock Wait; empty for Slow Query rows which have no live connection)
  • Kill button — Manager-only. Kind-aware behavior (see info box below).

Available filters in the search bar: by kind (Long Transactions / Lock Waits / Slow Queries), by live/ended status, by "Last 24 Hours", and grouping by Kind / DB User / Killed status.

About the Kill button — kind-aware Killing a database process immediately rolls back any uncommitted transaction; any unsaved work in that transaction is lost. The Kill button:
  • Long TX rows: re-checks that the transaction is still open and older than 30 seconds before terminating; if the backend has moved on to unrelated work, the kill is refused (protects against stale-row clicks).
  • Lock Wait rows: re-checks that the backend is still waiting on a Lock; if it has since acquired its lock and moved on, the kill is refused.
  • Slow Query rows: the button is not shown — there is no live PID to kill (the query completed when it was captured; this is a historical entry from pg_stat_statements or the software monitor).
Before killing, verify the query is not a legitimate long-running operation (e.g. a large data import) that should be allowed to finish.

Retention: records older than 7 days are automatically deleted by the same cron.

Cron History

Complete execution history of all scheduled actions. Each record shows when the cron ran, how long it took, whether it succeeded or failed, and the full error message if it crashed. History older than 30 days is automatically cleaned up by the daily cleanup cron.

Use the search bar to filter by cron name, date range, or status to quickly find all occurrences of a specific failure.

Session Heatmap Data

The raw data behind the heatmap visualisation. Each record represents one hour of one day. Sampled every 15 minutes (piggybacking on the snapshot cron), kept as the last sample per hour — so each (date, hour) row holds the most recent reading taken within that hour, not all four samples. Records are retained for 30 days and pruned once daily. The heatmap visualisation only uses the most recent 14 days for its averages.

Snapshot History

Browsable list of every 15-minute system snapshot ever taken. Each row shows the complete health picture at that moment: health score, RAM usage, active sessions, stuck cron count, failed cron count, lock count, and long transaction count.

Row colour coding: red background when health score is below 60 or locks are present; yellow when health score is below 80. Click any row to see the full snapshot details including the module impact data captured at that time.

Retention — Two System Parameters, Both Opt-In

By default snapshots are kept forever. This is intentional: MSME instances produce ~35,000 snapshot rows per year (a few hundred megabytes even on large multi-year installs) which is negligible on any Odoo database. Two independent admin knobs let you narrow that if you want to:

System ParameterEffectDefault
odexalabs.vigil.snapshot_retention_days Cap-search / display window. Historical charts, trend aggregations, and analytics only consider snapshots within the last N days. Older rows remain on disk and are queryable via the Snapshot History list; they are simply excluded from computation. Reversible — widen the window later and older data reappears in charts. Unset (unlimited)
odexalabs.vigil.snapshot_hard_delete_older_than_days Physical cleanup (opt-in). When set to a positive integer, a daily cron deletes snapshot rows strictly older than N days. The most recent snapshot is always preserved even if it happens to be older than the cutoff. Destructive — deleted rows cannot be recovered. Unset (never delete)

Both parameters accept any positive integer (number of days). Empty, 0, false, or none all mean "unlimited". Change under Settings → Technical → Parameters → System Parameters — effective immediately, no restart.

Suggested values by profile
  • MSME / small business (1-50 users): Leave both unset. Storage impact is negligible; longer history is more useful for post-incident review.
  • Medium business (50-200 users): Optionally set retention to 180-365 days if the "24h Activity" view feels crowded. Leave hard-delete unset.
  • Large / regulated (data-retention policy in force): Set both. Retention to display-window length; hard-delete to compliance cutoff.
Why cap-search is the default over auto-delete Automatic data deletion is a red flag in audit reviews and cannot be undone. Cap-search gives the same "show me only recent" behavior without destroying anything. Admins who genuinely need physical cleanup opt in by setting the second parameter.

Snapshot Collection Safety Caps

Each individual metric collector inside take_snapshot() runs with a per-metric SET LOCAL statement_timeout = 5s guard. If any one metric exceeds that (e.g. a slow pg_stat_user_indexes scan on a very large DB), it is aborted, recorded as a per-metric error in metric_errors_json, and the snapshot moves on to the next check. The row is marked partial so the dashboard's health score cannot present a snapshot with missing data as "all good". This bounds total snapshot time at roughly 15 × 5 = 75 seconds worst-case, safely inside the 15-minute cron window on any DB size.

The attachment breakdown metric (full-scan GROUP BY on ir_attachment) is additionally cached for 24 hours — recomputed once per day and reused for the other ~95 snapshots in that window. Saves multi-second I/O on instances with hundreds of thousands of attachments. Attachments don't materially change hour-to-hour, so yesterday's breakdown is representative.

Module Install / Upgrade Log

Complete history of all custom module installations and upgrades, with old version, new version, date, and user. Access via Monitoring → Module Install / Upgrade Log.

Error Logs

Persistent capture of every ERROR-level entry produced by Python's logging framework, grouped Sentry-style so a single bug that fires 500 times shows as one row with Occurrences = 500 rather than 500 identical rows. Access via Monitoring → Error Logs.

How grouping works. Each captured error is fingerprinted from three pieces: the exact file:line where the error was logged, the attributed Odoo module, and a normalised form of the message (numbers, quoted strings, UUIDs, and hex hashes are masked so "Record 42 not found" and "Record 43 not found" collapse into the same group). Same fingerprint → same row, count increments, last_seen_at and the stored traceback advance to the latest occurrence.

Retention. A daily cron (Vigil Suite: Error Log Retention Cleanup) deletes groups whose last_seen_at is older than the value of system parameter odexalabs.vigil.error_log_retention_days. Default is 30 days. Set to 0 (or leave empty) to retain forever.

How this differs from Odoo's ir_logging table. Odoo's built-in ir_logging requires launching the server with --log-db and writes one row per log event (no grouping). The Error Logs table here is populated via a Python logging.Handler attached at module load — it works out of the box in every deployment mode (file logs, STDOUT, Docker), automatically groups by fingerprint, and is browsable from the UI without SSH or direct database access.

Relationship to the Dashboard Error Log panel. The dashboard's Error Log panel (Row 5) shows the live per-worker buffer — the last 50 entries captured by the current worker process, ungrouped, cleared on restart. The Error Logs menu shows the persisted grouped history — every fingerprint ever seen, across all workers, surviving restarts. Both work together: the panel answers "what's happening right now on this worker" while the menu answers "what has been happening over time."

Diagnostics — Live Slow Queries

Opens the Live Slow Queries wizard — shows queries in pg_stat_activity currently running longer than the configured threshold (system parameter odexalabs.vigil.live_slow_threshold_ms, default 1000 ms). Live-only: rows disappear as queries complete. Type badges (Long TX / Lock Wait / Slow Query) distinguish the nature of each row. For historical completed slow queries, see the Captured Transactions log above. Full details in Section 16.

Diagnostics — Live Locks

Opens the Live Locks wizard showing all current lock-blocking situations in the database. Each entry shows which process is the blocker and which processes are waiting. Managers can terminate the blocking process from this wizard.

Diagnostics — Index Health

Opens the Index Health wizard. See Section 15 for full details.

Diagnostics — Ghost File Scanner

Opens the Ghost File Scanner wizard. See Section 11 for full details.

Index Health Wizard

Identify duplicate, unused, and over-indexed tables to improve write performance and save disk space.

Access via: Monitoring → Diagnostics → Index Health, or by clicking the Index Health card on the dashboard.

Dashboard KPI ↔ wizard consistency The Actionable Index Waste number on the dashboard card comes from the exact same classifier used here (Panel 2 — Unused Indexes, "Investigate" tier). Dashboard and wizard cannot disagree, by construction: both call the same odexalabs.index.wizard.get_index_health() method. This also means classifier improvements to the wizard automatically improve the KPI, and vice versa.

The wizard has three panels, ordered by drop-safety confidence:

Panel 1 — Duplicate Indexes (highest cleanup confidence)

Shows indexes on the same table that cover exactly the same columns in the same order with the same access method. Duplicate indexes consume extra disk space and slow down every write operation because PostgreSQL must maintain each index separately.

The query checks pg_index.indkey (column order), pg_index.indclass (operator class), pg_index.indoption (sort direction), expression predicates, and partial-index predicates to detect true duplicates.

If duplicates are found, keep the one with the most meaningful name and drop the others.

Panel 2 — Unused Indexes (btree only — Investigate + ORM)

Shows indexes that have never been used (zero scans since the last stats reset) on tables that have had activity. The wizard classifies each unused index into one of four tiers:

BadgeTierCriterionShown in TableRecommended Action
Investigate Unknown btree btree index that matches no known ORM naming pattern — likely added manually by a DBA or migration script Yes — shown first, highest priority Research what this index was created for. If no query uses it, drop it.
ORM ORM-pattern btree btree index matching Odoo's naming conventions: {table}_{field}_index, {table}_{columns}, or _rel suffix (M2M tables) Yes — shown after "Investigate" items Check whether the indexed field is still filtered or ordered on in queries — especially on large tables.
— (excluded) Non-btree GIN, GiST, BRIN, hash, spgist, bloom — used for full-text search, JSONB, range types, etc. No — count shown in footnote only Leave in place. These index types legitimately show zero scan count even when actively used by the query executor.
— (excluded) Constraint Unique or exclusion constraints — enforce data integrity No — count shown in footnote only Never drop. These prevent duplicate data at the database level.
Why ORM-pattern indexes are shown rather than hidden Core Odoo, Enterprise modules, and custom modules all create indexes with identical naming patterns. PostgreSQL has no attribute recording who created an index. Hiding ORM-pattern indexes would silently suppress legitimate candidates for removal — for example, an unused index=True field on a large table that was added in a module but never actually queried. The amber badge prompts you to investigate rather than blindly drop.
Stats freshness warning If PostgreSQL was recently restarted or stats were reset (within 24 hours), unused-index results may be unreliable — all counters start at zero after a restart, making every index look unused. The wizard shows a warning with the stats age when this is the case. Wait at least 48 hours of normal operation for reliable results.

Panel 3 — Over-Indexed Tables (write-amplification signal, not bloat)

Shows tables larger than 100 MB where the total index size exceeds the actual data size. This is a write amplification risk: every INSERT, UPDATE, or DELETE must maintain each index, and when indexes are heavier than data, write performance degrades significantly.

This is not the same as dead-row bloat (which is handled by the Table Bloat Analyzer). Over-indexed means there are too many indexes for the volume of data. The fix is to drop unused indexes from Panel 2 that belong to these tables.

Slow Query Monitor

Two independent pieces work together: (1) a live wizard reading pg_stat_activity, and (2) an opt-in software monitor that captures completed slow queries in memory for the cron to persist.

The Live Slow Queries Wizard

Access via Monitoring → Diagnostics → Live Slow Queries (or click the Live Slow Queries number on the dashboard). The wizard reads only pg_stat_activity — queries running right now that have exceeded the threshold. Rows disappear as queries complete; nothing here is historical.

Each row shows a Type badge:

  • Long TX — the query is inside a transaction older than 5 seconds
  • Lock Wait — the query is blocked waiting for a database lock
  • Slow Query — a plain slow query above threshold (neither of the above)

For historical / completed slow queries, use Monitoring → Captured Transactions filtered by Kind = Slow Query. The wizard and the log are deliberately separate: the wizard tells you what is happening now; the log tells you what has happened.

Configurable Threshold — odexalabs.vigil.live_slow_threshold_ms

The wizard's slow-query threshold is read from the system parameter odexalabs.vigil.live_slow_threshold_ms (milliseconds, default 1000). The parameter is auto-created at module install with the default value — no manual setup needed.

To tune it: Settings → Technical → Parameters → System Parameters, search for odexalabs.vigil.live_slow_threshold_ms, edit the value, save. Effective immediately on the next dashboard refresh or wizard open — no restart. Suggested values:

  • 500 — noisier, catches routine slow catalog reads. Useful when actively hunting a specific issue.
  • 1000 (default) — balanced. Matches pg_stat_statements' threshold used by the cron.
  • 2000–5000 — quiet. Only genuinely problematic queries show up.

The Opt-in Software Query Monitor

A separate, disabled-by-default feature that intercepts every completed query taking over 1 second and stores it in an in-memory ring buffer (max 50 entries per database). The cron then drains this buffer into the Captured Transactions log every 15 minutes, so entries survive worker restarts.

Disabled by default — opt-in only The Software Query Monitor does NOT run when you install the module. It will never intercept any query until a system administrator explicitly enables it. There is zero overhead when disabled. When enabled, the overhead is two time.time() calls per query — approximately 100 nanoseconds each; unmeasurable in practice.

How to Enable the Software Monitor

  1. Go to Settings → Technical → Parameters → System Parameters.
  2. Search for odexalabs.vigil.enable_query_monitor. The parameter was auto-created at module install with value False.
  3. Change the value to True and save. The monitor activates within 60 seconds (parameter cache expiry). No restart needed.

How to Disable

Change the parameter value back to False (or any value other than True). The monitor stops capturing within 60 seconds. No restart needed.

What the Software Monitor Captures (when enabled)

  • Query text — Full SQL string
  • Duration — Execution time in seconds
  • User — Odoo user who triggered the query
  • Diagnosis — Automatic classification (Lock Wait / CTE-Subquery / Full Scan / Slow Query) written into the row's state field for later inspection

These entries flow into Captured Transactions with kind = 'slow_query' at the next cron tick. Dedup within a 15-minute window merges repeated fingerprints and bumps the Call Count on the existing row.

Auto-Provisioned System Parameters

The module ships two system parameters that are auto-created on install (never overwritten on upgrade — user tuning is preserved):

KeyDefaultPurpose
odexalabs.vigil.live_slow_threshold_ms1000Threshold (ms) for the Live Slow Queries wizard and its dashboard KPI count
odexalabs.vigil.enable_query_monitorFalseOpt-in flag for the software query monitor described above

What Is NOT Captured

  • Queries below the threshold — by design
  • Internal monitoring queries (pg_stat_*, odexalabs_*) — excluded to prevent self-pollution
  • Direct database connections that bypass Odoo (psql, migration scripts) — use pg_stat_statements for those; the cron scans it separately if the extension is installed
  • Software-monitor entries that ran while the monitor was disabled

Backup Manager

Automated database backups to cloud storage or local disk. Access via the Backups menu in the top navigation.

Supported Backup Providers

Amazon S3
Google Drive
Microsoft OneDrive
Dropbox
SFTP
FTP
Local Disk

S3-compatible services (DigitalOcean Spaces, Linode Object Storage, Backblaze B2, MinIO) work with the S3 provider — enter your custom endpoint URL in the Endpoint field.

Step 1 — Set Up a Backup Provider

  1. Go to Backups → Cloud Providers in the top navigation and click New.
  2. Give the provider a descriptive Name (e.g., "Production S3 Bucket" or "Hetzner SFTP").
  3. Select the Provider Type from the dropdown.
  4. Fill in the credentials for your chosen provider (see provider-specific instructions below).
  5. Click Test Connection to verify the credentials work before saving.
  6. Click Save.

Provider-Specific Setup

ProviderRequired FieldsNotes
Amazon S3 / S3-Compatible Bucket Name, Access Key, Secret Key, Region (required) Region must be set explicitly — there is no safe universal default because boto3 needs a non-empty region string for AWS Signature V4 signing, and the right value depends on the provider:
  • AWS: your bucket's region (us-east-1, eu-central-1, etc.)
  • Cloudflare R2: auto
  • DigitalOcean Spaces: nyc3 / ams3 / sgp1 / fra1 (matching your endpoint)
  • Backblaze B2: us-west-002 / eu-central-003 (matching your endpoint)
  • Wasabi / Linode: the region code your provider uses
  • MinIO: anything (us-east-1 works)
For non-AWS providers also fill in Endpoint URL (e.g., https://ams3.digitaloceanspaces.com). Requires pip install boto3.
Google Drive Client ID, Client Secret, Folder ID First register the Authorized Redirect URI (see below) in Google Cloud Console, then click Authorize Google Drive — you will be redirected to Google's OAuth consent screen. After approval, you are redirected back to Odoo and the token is saved automatically. Requires pip install google-auth-oauthlib google-api-python-client google-auth-httplib2.
Microsoft OneDrive Client ID, Client Secret (from Azure App Registration) First register the Redirect URI (see below) in your Azure App Registration, then click Authorize OneDrive — same OAuth flow as Google Drive. Requires pip install msal.
Dropbox App Key, App Secret (recommended)or Legacy Access Token Recommended: register an app in the Dropbox App Console, enter the App Key and App Secret, then click Authorize Dropbox — same OAuth flow as Google Drive and OneDrive. The refresh token is saved automatically and Vigil Suite renews access tokens as needed, so the connection does not silently expire.

Legacy: a long-lived access token generated in the Dropbox App Console still works, but Dropbox now issues short-lived tokens by default and this path may stop working without warning. Requires pip install dropbox.
SFTP Host, Port (default 22), Username, Password, Remote Path Works with Hetzner Storage Box, Files.com, AWS Transfer. Requires pip install paramiko.
FTP Host, Port (default 21), Username, Password, Remote Path Plain FTP — no additional packages needed (uses Python's built-in ftplib).
Local Disk Remote Path (absolute folder path on the server) Stores the backup file directly on the server's filesystem. The only path the module blocks is one that resolves inside Odoo's data_dir (that would create a self-loop where the backup writes into the very filestore it is backing up). Every other writable directory is admin discretion — choose a path with enough free space, on a volume separate from the OS root if possible. Local backups cannot be downloaded from the Odoo UI; retrieve the file via SSH/SFTP.

Authorized Redirect URIs (Google Drive & OneDrive)

Google Drive and OneDrive use OAuth. When you register your application in the provider's developer console, it asks for an Authorized Redirect URI (Google) or Redirect URI (Azure / OneDrive) — the address the provider sends the user back to after they approve access. You must register the exact URL shown below, or authorization fails with a redirect_uri_mismatch error.

Replace the domain with your own Odoo URL In the URLs below, replace https://your-odoo-domain.com with the value of your web.base.url system parameter (Settings → Technical → Parameters → System Parameters → search web.base.url). It must match exactly — same scheme (https://), same host, and no trailing slash. If web.base.url is wrong, the OAuth redirect will not return to your instance.
ProviderWhere to enter it in the provider consoleAuthorized Redirect URI to register
Google Drive Google Cloud Console → APIs & Services → Credentials → your OAuth 2.0 Client ID → Authorized redirect URIs → Add URI https://your-odoo-domain.com/odexalabs/backup/google_drive/token
Microsoft OneDrive Azure Portal → App registrations → your app → Authentication → Platform configurations → Web → Redirect URIs → Add URI https://your-odoo-domain.com/odexalabs/backup/onedrive/token
Dropbox (OAuth path) Dropbox App Console → your app → Settings → Redirect URIs → Add https://your-odoo-domain.com/odexalabs/backup/dropbox/token
S3, SFTP, FTP, and Local Disk do not need a redirect URI These providers use keys or credentials rather than OAuth, so they have no browser callback to register. Dropbox falls into two paths: the recommended OAuth path uses the redirect URI above; the legacy Access Token path does not.
OAuth tokens auto-refresh For Google Drive and OneDrive, the module automatically refreshes authentication tokens before they expire. You only need to authorise once. If authorisation expires (usually after ~6 months of inactivity), click Authorize again.

Step 2 — Create a Backup Job

  1. Go to Backups → Backup Jobs and click New.
  2. Name — Give the job a clear name like "Daily DB Backup to S3".
  3. Provider — Select the provider you configured in Step 1.
  4. Include Filestore — Toggle this on to back up both the database AND all uploaded files (PDFs, images, documents). Off backs up the database only.
    • On (Include Filestore) → Creates a .zip file containing both the database dump and the filestore folder. Larger backup size.
    • Off (Database Only) → Creates a .dump file with the PostgreSQL dump only. Smaller and faster.
  5. Backup Frequency — How often the job runs: every 1, 2, 3, 4, 6, 12, or 24 hours.
  6. Retention (Days) — After how many days old backup files are automatically deleted from the cloud provider. Set to 0 to never delete automatically. Keep at least 7 days for safety.
  7. Notify on Failure / Success — Enable email notifications. You must add at least one recipient when either notification is enabled.
  8. Enable Schedule — Click this button to activate the backup job. The job will not run automatically until scheduling is enabled.

Retention Cleanup

Backup files older than the retention period are automatically deleted from the cloud provider and removed from the history. The cleanup runs every hour alongside the backup scheduler. If a backup upload succeeded but the subsequent database write of the history record failed, the failure history entry still stores the cloud reference — so retention finds and deletes the orphan cloud file on schedule. No manual cleanup required.

Large File Uploads

Files are uploaded in small chunks (4 MB for Dropbox, OneDrive, and Google Drive; 10 MB for S3) with automatic retry on transient network errors. This keeps backups reliable even on slow or flaky network connections.

Stale Backup Detection

If a scheduled job hasn't succeeded within its frequency interval + 1 hour grace, the backup status changes to "stale" and the health score deducts −10 points.

Concurrent Run Protection

If a backup job is still running when the next scheduled run arrives, the duplicate run is skipped automatically. Jobs stuck for more than 4 hours are reset.

Step 3 — View Backup History

Go to Backups → Backup History to see every backup run with its date, size, duration, status, and a download link (for cloud providers that support it — S3, GDrive, OneDrive, Dropbox). Local and SFTP backups cannot be downloaded through the UI.

How Backups Work Technically

Vigil Suite uses Odoo's built-in service_db.dump_db API to create the backup — the same mechanism Odoo itself uses for its built-in backup feature. This means:

  • No pg_dump binary needs to be installed or accessible
  • No PostgreSQL password needs to be stored in environment variables
  • Works identically on Docker, bare metal, and any platform Odoo runs on
  • The backup is created in a temporary file, then uploaded to your provider, then the temp file is deleted — no large residual files left on disk

Running Large Backups (Multi-GB Databases)

Backups run inside an Odoo cron worker, which is capped by Odoo's --limit-time-real flag (default: 120 seconds). That budget covers the full pipeline: pg_dump + temp file write + cloud upload + history record.

Approximate ceilings with the default 120 s cron limit:

  • Fast link (100 Mbps up): up to ~1 GB backup
  • Typical link (10–25 Mbps up): ~100–250 MB backup
  • Slow link (~5 Mbps up): under ~50 MB backup

If your database is larger than these ceilings, extend the cron time budget by passing one of these flags when starting Odoo:

  • --limit-time-real-cron=1800 — 30 minutes (recommended up to ~10 GB backups)
  • --limit-time-real-cron=3600 — 1 hour
  • --limit-time-real-cron=0 — unlimited (for very large databases)

Where to add the flag:

  • Docker Compose: add to the command: line in docker-compose.yml (e.g. command: odoo --limit-time-real-cron=1800)
  • systemd: add to ExecStart in the Odoo service unit file, then systemctl daemon-reload and restart Odoo
  • Odoo.sh: configured per-branch in the platform UI under Settings → System

Also make sure:

  • The Odoo host has free disk space ≥ 1× your uncompressed database size — backups land in a temp file before upload
  • Your cloud provider's per-file limit exceeds the expected backup size (Dropbox 350 GB session, OneDrive 250 GB, Google Drive 5 TB, S3 5 TB)
  • Chunked uploads with automatic retry are already built in — 4 MB chunks for Dropbox/OneDrive/Google Drive, 10 MB for S3, up to 3 attempts per chunk on transient network errors

Alert Rules

Get notified automatically the moment a metric crosses your defined threshold. Access via Configuration → Alert Rules.

How Alerts Work

Alert rules are evaluated automatically after every system snapshot (every 15 minutes). Each active rule checks the current value of its chosen metric against the defined threshold. If the condition is met AND the rule is not in its cooldown period, the alert fires immediately.

Alerts are designed to be race-condition safe: even in multi-worker setups where two processes might evaluate the same rule at the same moment, only one alert will fire per cooldown window.

Creating an Alert Rule

  1. Go to Configuration → Alert Rules and click New.
  2. Name — Describe the alert clearly (e.g., "Production DB Over 10 GB").
  3. Metric — Choose what to monitor from the dropdown:
    • Health Score — The overall 0–100 score
    • RAM Usage (MB) — Odoo process memory in megabytes
    • Database Size (MB) — PostgreSQL database size in megabytes
    • Cache Hit Ratio (%) — PostgreSQL buffer hit percentage
    • Active DB Sessions — Number of active database connections
    • Stuck Cron Count — Number of overdue scheduled actions
    • Failed Email Count — Number of emails in exception state. On multi-company instances this metric is global across every company so a single Vigil Suite alert rule catches system-wide SMTP problems (a company-scoped count would miss failures from shared mail servers). The dashboard KPI displays the same global count so the number you see matches the number alert rules fire on; the per-user "recent failed emails" list is still filtered to the user's allowed companies.
    • Ghost File Size (MB) — Size of orphaned files on disk
    • Long Transactions Count — Number of transactions over 30 seconds
    • Waiting Lock Count — Sessions blocked by database locks
  4. Operator — Choose the comparison: greater than, less than, equals, greater-or-equal, or less-or-equal.
  5. Threshold — The value to compare against (e.g., 60 for "Health Score < 60").
  6. Alert Channel — How to notify:
    • In-App Notification — A pop-up banner appears inside Odoo for the selected recipients
    • Email — Sends an email to the selected recipients
    • Both — Both in-app and email
  7. Recipients — Select which Odoo users to notify. If left empty, only the admin user is notified.
  8. Cooldown (minutes) — The minimum time between repeat alerts for the same rule. Default is 60 minutes. Set to 0 for no cooldown (every snapshot fires the alert if the condition is still met). Use a reasonable cooldown to avoid alert fatigue — a health score alert every 15 minutes is noise; every 60 minutes is actionable.
  9. Click Save. The rule is active immediately.

Recommended Starting Alerts

Alert NameMetricConditionCooldownWhy
Health Score CriticalHealth Score< 6060 minCatch serious problems before users call you
Health Score WarningHealth Score< 80120 minEarly warning before problems become critical
High RAMRAM Usage (MB)> 400030 minAlert before out-of-memory crashes occur
Database GrowingDatabase Size (MB)> 200001440 minDaily heads-up on disk usage (adjust threshold to your size)
Email Delivery ProblemFailed Email Count> 1060 minCatch SMTP failures before customers notice
DB Lock ContentionWaiting Lock Count>= 515 minUsers are being actively blocked right now
Runaway TransactionLong Transactions Count>= 315 minMultiple stuck transactions need immediate investigation
Low Cache HitCache Hit Ratio (%)< 95240 minEarly indicator that PostgreSQL needs more RAM
Alert email recipients must have an email address If you choose Email or Both as the channel, each selected user must have an email address in their Odoo user profile. Vigil Suite logs a warning (and skips the email) for users without one — the in-app notification is still sent.

Email Digest

An automatic email summary of Vigil Suite activity, delivered on a schedule. Access via Configuration → Email Digest.

What It Is

Where alert rules fire only when a threshold breaks, the email digest is a recurring heartbeat — it arrives whether or not anything went wrong, so admins get a regular, scannable summary and partners managing multiple instances stay informed without logging in. There are two flavours:

  • Daily digest — covers the past 24 hours. Acute and event-focused: notable events with timestamps, headline peaks (lowest health score, peak RAM, peak sessions), cron activity with the 5 slowest crons, current backup status, and the modules most associated with incidents.
  • Weekly recap — covers the past 7 days. Trend-focused: average and lowest health score, a 7-day text score trend, a "most concerning day" callout, cron activity with the 5 most-failed crons, backup run summary, and 7-day cumulative module impact.

Both always send, even on quiet periods — a quiet digest is a positive signal that monitoring is alive.

Setting It Up

  1. Go to Configuration → Email Digest and click New.
  2. Name — Describe the digest clearly (e.g., "Daily summary for SysAdmin team").
  3. Type — Choose Daily or Weekly.
  4. Recipients — Select the Odoo users who should receive it. Each must have an email address configured on their user profile.
  5. Click Save. The digest is active immediately and will send on the next scheduled run.

You can create as many digest configurations as you need — for example, a daily digest for your internal team and a separate weekly recap for a client contact.

Testing a Digest

Click Send Test Now on the digest form to send the email immediately to the configured recipients. This bypasses the schedule and is the fastest way to verify recipients and preview the content. The Last Status, Last Recipient Count, and Last Error fields update after every attempt so you can confirm delivery or diagnose failures. Last Sent is only stamped on a successful send.

Transient SMTP failures do NOT waste the cooldown window The cooldown that prevents duplicate sends is anchored to Last Sent, which advances only when the email actually goes out. If the SMTP server was briefly down and the digest failed to send, the next scheduled cron tick retries — you do not lose an entire day (daily) or week (weekly) of digests to a transient outage. A stale in-flight claim is automatically released after 10 minutes so a crashed worker never permanently blocks the digest.

Customizing the Email

The digest bodies are standard Odoo email templates. To edit wording, layout, or styling, go to Settings → Technical → Email → Templates and search for "Vigil Suite". There are two templates — one for the daily digest and one for the weekly recap — and they are independent, so editing one does not affect the other.

Recipients must have an email address Recipients are Odoo users, and each must have an email on their profile. If a recipient has no email, they are skipped with a logged warning. If no recipient has a valid email, the digest is marked Skipped and no mail is sent — Vigil Suite never silently falls back to emailing the admin.

Background Jobs Created by Vigil Suite

Vigil Suite creates several scheduled actions in your Odoo instance. Here is what each one does, why it exists, and how often it runs. The two Email Digest jobs only send when you have created and activated a matching digest configuration.

You can view and manage these in Settings → Technical → Scheduled Actions. Search for "Vigil Suite" to find them.

Job Name Interval What It Does What Happens If Disabled
Vigil Suite: Take Snapshot Every 15 minutes Collects all system metrics: RAM usage, database size, cron status, email queue, session count, connection pool, lock count, long transaction count, module impact, Index Health (actionable waste + duplicate count + over-indexed count + status), worker load, and file attachment breakdown. Also evaluates all alert rules and samples the session heatmap. On every run, also reaps any snapshot row that was left in the running collection state for more than 30 minutes (leftover from a worker that crashed mid-collection) — the reaper flips the row to partial so it can never permanently block the dashboard from finding a snapshot to display. Dashboard shows stale data. Health score, charts, and 24h activity feed stop updating. Alert rules stop firing.
Vigil Suite: Cron History Cleanup Once per day Deletes cron history records older than 30 days. Deletes system snapshots older than 90 days. Runs in small batches (1000 records at a time, max 60 seconds per run) to avoid impacting performance on large databases. Cron History and Snapshot History tables grow indefinitely. On very active systems, this can add gigabytes to the database over months.
Vigil Suite: Backup Scheduler Every 1 hour Checks all active, scheduled backup jobs. For each job that is due (based on its frequency setting), runs the backup. After running due jobs, also applies retention cleanup — deletes files older than the job's retention period from the cloud provider and removes their history records. No automated backups run. Last backup status becomes "stale" and the health score deducts −10 points after one missed interval.
Vigil Suite: Daily Email Digest Once per day Iterates every active daily email digest configuration and sends each one a 24-hour summary email. A per-configuration cooldown prevents a digest from being sent twice in the same window, even with multiple cron workers. Daily digest emails stop sending. The dashboard and alert rules are unaffected.
Vigil Suite: Weekly Email Digest Every 7 days Iterates every active weekly email digest configuration and sends each one a 7-day recap email with the score trend and worst-day callout. Weekly recap emails stop sending. The dashboard and alert rules are unaffected.

Database Indexes Created

Vigil Suite creates one additional index to keep the Cron History table fast as it grows:

Index NameTableColumnsPurpose
odexalabs_cron_history_start_cron_idx odexalabs_cron_history start_time, cron_name Makes the 24-hour cron stats query fast even when the table has millions of rows. Without this, the stats table on the dashboard would slow down as history accumulates.
odexalabs_vigil_ir_attachment_idx ir_attachment (core Odoo) store_fname (partial: WHERE NOT NULL) Created at install time via CREATE INDEX CONCURRENTLY so the operation does not block ir_attachment. Speeds up the Ghost File Scanner by allowing it to compare disk filenames against database records without a sequential scan on large filestores. The index remains in place after the module is uninstalled. Pre-rebrand installs had the same index under the name odexalabs_ir_attachment_store_fname_idx; the module drops the old name and creates the new one on upgrade.

All other Odoo-standard indexes (primary keys, foreign keys, and ORM index=True fields) are created automatically by Odoo when the module installs.

Performance impact The snapshot cron is designed to be lightweight. Each metric is collected in its own short database transaction (under 1 second each). If one metric fails (e.g., the log file is temporarily inaccessible), the others continue unaffected. The total snapshot process typically completes in 5–15 seconds depending on database size and server speed.

Troubleshooting

Common issues and how to resolve them.

Dashboard shows "No data" or blank charts

The module needs at least one completed snapshot. Click Refresh to trigger the first snapshot immediately. If the dashboard remains blank, check that the scheduled action Vigil Suite: Take Snapshot is enabled and not stuck: go to Settings → Technical → Scheduled Actions, find it, and click Run Manually.

Dashboard shows a "Partial Snapshot" warning banner

One or more metrics failed to collect during the last snapshot. This usually means a temporary issue (database was briefly locked, log file was inaccessible). Click Refresh to take a fresh snapshot. If the warning persists, check the Odoo log for errors prefixed with Snapshot metric ... failed. If the banner reads _reaper: Snapshot did not complete within 30 min, a prior snapshot process crashed before finishing; the next scheduled snapshot automatically replaces it and the banner clears.

RAM Usage shows 0 MB

Install psutil: pip install psutil and restart Odoo. Without psutil, the module can only read single-process RAM from the OS (which may show 0 in some environments).

Backup fails with "token expired"

For OAuth providers (Google Drive, OneDrive), re-authorise by clicking Authorize on the Provider configuration form. Tokens auto-refresh during backups, but an inactive token (not used for months) may expire.

Backup fails with "boto3 not installed"

Install the required package: pip install boto3 and restart Odoo.

Backup fails with "paramiko not installed"

Install the required package: pip install paramiko and restart Odoo.

Index Health shows "Stats too fresh — wait 24-48 hours"

PostgreSQL was recently restarted or its statistics were reset. After a restart all query counters are zero, which would falsely flag every index as unused. Wait at least 24 hours of normal operation, then reopen the wizard.

Session Heatmap is empty

Data accumulates over time (sampled every 15 minutes). After a fresh install, wait at least 24 hours for meaningful data and 14 days for a representative pattern.

Health Score is low but I don't see obvious issues

The most common hidden causes:

  • A backup job that ran and failed (hard-caps score at 50) — check Backups → Backup History
  • Failed emails you did not notice — check the email panel on the dashboard
  • A cron that was auto-deactivated by Odoo after repeated failures — check Stuck & Failed Actions
  • Database bloat that accumulated over months — run the Bloat Analyzer
  • A partial snapshot (some metrics not collected) — look for the yellow warning banner

Live Slow Queries wizard shows "No live slow queries"

This is the correct answer when no queries in pg_stat_activity are running longer than the configured threshold — i.e. everything is fine right now. The wizard is live-only by design; it does not show historical data. If you were expecting to see something:

  • Verify the threshold. Open Settings → Technical → Parameters → System Parameters and check odexalabs.vigil.live_slow_threshold_ms. Default is 1000 ms; if it has been raised, short-but-still-slow queries won't appear. Lower the value if you want more sensitivity.
  • Timing matters — the wizard is a snapshot in time. If the offending query completes between your dashboard click and the wizard render, you'll see "No live slow queries." Open the wizard again during the incident, or check Monitoring → Captured Transactions for the historical record captured by the last cron.
  • For historical browsing, use Captured Transactions — filter by Kind = Slow Query for completed queries persisted from pg_stat_statements or the opt-in software monitor. If that is empty too, then either pg_stat_statements is not installed AND the software monitor is not enabled, or nothing has actually been slow. Verify odexalabs.vigil.enable_query_monitor is True to enable the software fallback.

Only one "System Health Warning" popup during a long incident

The built-in health notification (the red banner that appears when the health score drops below 60) has a 4-hour cooldown. During a sustained incident it fires at most once every four hours per instance, no matter how many snapshots see the low score in between — a snapshot every 15 minutes would otherwise broadcast 16 popups per manager per hour to every logged-in user. If you need alerting on every tick or with a shorter interval, create a custom Alert Rule on the Health Score metric with your desired cooldown; custom Alert Rules run independently and each rule has its own cooldown timer.

Get the Most From Your Odoo Instance

Odexalabs Vigil Suite identifies the symptoms. Our audit team finds the root cause and fixes it — so you can focus on running your business instead of fighting fires.

Get a Performance Audit