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.
Table of Contents
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:
| Environment | Supported | Notes |
|---|---|---|
| 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. |
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
- Place the module — Copy the
odexalabs_health_basefolder into your Odoo addons directory (the same folder where your other custom modules live). - 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.
- Find and install — In the Apps search box, type Vigil Suite. Click Install on the matching card.
- 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
The general rule:
psutilis 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-oauthlibgoogle-api-python-clientgoogle-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 |
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.
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:
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:
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
/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.
- 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:
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:
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.
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:
- Takes a new system snapshot immediately (same as the 15-minute cron, but on-demand), updating all stored metrics
- 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.
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:
# 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
sudo systemctl restart postgresql
# On Docker, restart the postgres container:
docker restart your_postgres_container_name
psql -U postgres -d your_odoo_database_name
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
\q
SHOW shared_preload_libraries; in psql.
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:
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 Range | Rating | What It Means |
|---|---|---|
| 80 – 100 | Good | Your system is running smoothly. No immediate action needed. |
| 50 – 79 | Fair | Some issues detected. Review and address the warnings shown. |
| 0 – 49 | Critical | Significant problems found. Immediate attention recommended. |
How Points Are Deducted
| Condition | Points Lost | How 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:
| Condition | Score Capped At | Why |
|---|---|---|
| Last backup status is "failed" (backup ran and failed) | 50 | A 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) | 70 | Backup protection is degrading. The job is configured but no longer producing fresh recovery points |
| Last backup status is "none" (no backup job configured) | 75 | A 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) | 60 | Missing data means we can't honestly say the system is healthy |
| 20 or more sessions waiting on DB locks | 40 | This level of lock contention means users are experiencing severe slowdowns |
| 10 or more long-running transactions | 40 | 10+ runaway queries usually means a stuck migration or serious runaway process |
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.
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_connectionslimit - Percentage — Active ÷ Max × 100
This reflects how saturated the database connection pool is. 80%+ means many threads are active simultaneously.
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_presenceheartbeat 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.
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:
Source: pg_stat_database for the current database
This is cumulative since the last stats reset — not per-session
| Ratio | Status | What It Means |
|---|---|---|
| 99%+ | Excellent | Almost all reads from memory. Ideal state for production. |
| 95–99% | Acceptable | Some disk reads. Normal on large databases that exceed RAM. |
| Below 95% | Needs attention | Too 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_reltables, 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:
| Metric | What It Means | Healthy Value |
|---|---|---|
| Active | Connections currently executing a SQL query | Varies with load |
| Idle | Connected but not doing anything | Normal to have many |
| Idle in Transaction | In an open transaction but not currently executing | Should be near 0 |
| Waiting | Blocked by a lock | Must be 0 |
| Max | PostgreSQL's max_connections setting | Set in postgresql.conf |
| Utilisation % | (Active + Idle + Idle in Transaction) ÷ Max | Below 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:
| Type | Badge | What It Means | Health 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 |
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.
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:
| Category | Health-score penalty | Trigger |
|---|---|---|
| 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 | −15 | Module 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.
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:
| Signal | Load weight | Directly deducts from health score? |
|---|---|---|
| Stuck cron owned by module (⏰) | +5 per cron | Yes (via Est. pts) |
| Failed cron owned by module (⚡) | +7 per cron | Yes (via Est. pts) |
| Slow query traced to a module table (in-memory buffer) | +3 per query | No — correlational |
Slow query via pg_stat_statements (mean_exec_time > 1 s) | +2 per query | No — correlational |
| ERROR or WARNING log entry attributed to module | +1 per entry | No — 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
--logfileconfiguration and is what makes Module Impact populate on Docker / stderr-only setups where no log file exists. - Log file scan (if
--logfileis 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:
- 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./..." - Logger name — the record's logger of the form
odoo.addons., used when no traceback is present..models.X - 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.
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.
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.
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:
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.
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.
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 usedodexalabs_quarantine; the module'spost_init_hookmigrates the folder automatically on upgrade.sessions— Odoo session files (not attachment files)assetsandweb_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
- From the dashboard KPI card for Database Size, click the Scan button (or go to Diagnostics → Ghost File Scanner).
- The wizard opens and immediately starts scanning. Large filestores are scanned in chunks (time-bounded segments) to avoid browser timeouts. Progress is updated automatically.
- When the scan finishes, you see the count and total size of detected ghost files.
- 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.
- Verify nothing is broken in your Odoo (check that images, PDFs, and documents still appear correctly).
- After confirmation, click Permanently Delete Quarantine to free the disk space.
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.
!! 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.
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 Type | Triggers When | Resolves 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.
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:
| Kind | What it means | Source |
|---|---|---|
| Long Transaction | A transaction that was open for more than 5 seconds when the cron scanned | Live scan of pg_stat_activity |
| Lock Wait | A session waiting on a database lock for more than 5 seconds | Live scan of pg_stat_activity WHERE wait_event_type = 'Lock' |
| Slow Query | A completed slow query captured from pg_stat_statements or the opt-in Slow Query Monitor buffer | Persisted 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.
- State —
active,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.
- 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).
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 Parameter | Effect | Default |
|---|---|---|
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.
- 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.
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.
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:
| Badge | Tier | Criterion | Shown in Table | Recommended 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. |
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.
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.
time.time() calls per query — approximately 100 nanoseconds each; unmeasurable in practice.
How to Enable the Software Monitor
- Go to Settings → Technical → Parameters → System Parameters.
- Search for
odexalabs.vigil.enable_query_monitor. The parameter was auto-created at module install with valueFalse. - Change the value to
Trueand 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
statefield 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):
| Key | Default | Purpose |
|---|---|---|
odexalabs.vigil.live_slow_threshold_ms | 1000 | Threshold (ms) for the Live Slow Queries wizard and its dashboard KPI count |
odexalabs.vigil.enable_query_monitor | False | Opt-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
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
- Go to Backups → Cloud Providers in the top navigation and click New.
- Give the provider a descriptive Name (e.g., "Production S3 Bucket" or "Hetzner SFTP").
- Select the Provider Type from the dropdown.
- Fill in the credentials for your chosen provider (see provider-specific instructions below).
- Click Test Connection to verify the credentials work before saving.
- Click Save.
Provider-Specific Setup
| Provider | Required Fields | Notes |
|---|---|---|
| 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:
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.
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.
| Provider | Where to enter it in the provider console | Authorized 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 |
Step 2 — Create a Backup Job
- Go to Backups → Backup Jobs and click New.
- Name — Give the job a clear name like "Daily DB Backup to S3".
- Provider — Select the provider you configured in Step 1.
- 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
.zipfile containing both the database dump and the filestore folder. Larger backup size. - Off (Database Only) → Creates a
.dumpfile with the PostgreSQL dump only. Smaller and faster.
- On (Include Filestore) → Creates a
- Backup Frequency — How often the job runs: every 1, 2, 3, 4, 6, 12, or 24 hours.
- 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.
- Notify on Failure / Success — Enable email notifications. You must add at least one recipient when either notification is enabled.
- 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_dumpbinary 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 indocker-compose.yml(e.g.command: odoo --limit-time-real-cron=1800) - systemd: add to
ExecStartin the Odoo service unit file, thensystemctl daemon-reloadand 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
- Go to Configuration → Alert Rules and click New.
- Name — Describe the alert clearly (e.g., "Production DB Over 10 GB").
- 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
- Operator — Choose the comparison: greater than, less than, equals, greater-or-equal, or less-or-equal.
- Threshold — The value to compare against (e.g., 60 for "Health Score < 60").
- 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
- Recipients — Select which Odoo users to notify. If left empty, only the admin user is notified.
- 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.
- Click Save. The rule is active immediately.
Recommended Starting Alerts
| Alert Name | Metric | Condition | Cooldown | Why |
|---|---|---|---|---|
| Health Score Critical | Health Score | < 60 | 60 min | Catch serious problems before users call you |
| Health Score Warning | Health Score | < 80 | 120 min | Early warning before problems become critical |
| High RAM | RAM Usage (MB) | > 4000 | 30 min | Alert before out-of-memory crashes occur |
| Database Growing | Database Size (MB) | > 20000 | 1440 min | Daily heads-up on disk usage (adjust threshold to your size) |
| Email Delivery Problem | Failed Email Count | > 10 | 60 min | Catch SMTP failures before customers notice |
| DB Lock Contention | Waiting Lock Count | >= 5 | 15 min | Users are being actively blocked right now |
| Runaway Transaction | Long Transactions Count | >= 3 | 15 min | Multiple stuck transactions need immediate investigation |
| Low Cache Hit | Cache Hit Ratio (%) | < 95 | 240 min | Early indicator that PostgreSQL needs more RAM |
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
- Go to Configuration → Email Digest and click New.
- Name — Describe the digest clearly (e.g., "Daily summary for SysAdmin team").
- Type — Choose Daily or Weekly.
- Recipients — Select the Odoo users who should receive it. Each must have an email address configured on their user profile.
- 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.
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.
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 Name | Table | Columns | Purpose |
|---|---|---|---|
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.
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_monitorisTrueto 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