# Getting Started with odoo-activity

> odoo-activity puts an Odoo instance's workers and its PostgreSQL backends in one screen, dumps stacks without log archaeology, and exposes the same data to an agent over MCP.

**Date:** 2026-08-11
**Source:** <https://trobz.com/insights/introducing-odoo-activity/>

---



For years `pg_activity` has answered one question well: what is postgres
doing right now. Open it, and the queries are there, longest-running first.

There is no equivalent for Odoo. When an instance is pegged and someone asks
what it is busy with, you fall back on improvisation:

- **`ps`** tells you a worker has been alive for six days. It says nothing
  about the request it has been stuck in for the last forty seconds.
- **`kill -3 <pid>`** makes Odoo print the stacks of all its threads into the
  log. Then you go digging through the log to find them, per worker, and if
  the worker is blocked in postgres or on IO the traceback is accurate and
  useless.
- **`py-spy dump`** gives better output, but it needs root on a machine where
  you probably only have the `odoo` user.
- **Patching Odoo** to write the current call into the process title with
  `setproctitle`, [which we did try a long time ago][odoo-pr-41365]. It reads
  well in `ps`, at the cost of a core patch to maintain per project.

Each of these answers a slice of the question, through a different tool, with
output in a different shape. Correlating them is manual work done under time
pressure, which is exactly when a diagnostic step gets skipped.

`odoo-activity` (`oa`) is our attempt at the missing screen.

## One Screen

{{< img src="/images/insights/introducing-odoo-activity/one-screen-tuico-int02.webp" alt="odoo-activity main screen: host CPU, memory, swap and load in the header, Odoo instances with nested databases in the second pane, and a tabbed detail view in the third" >}}

Host CPU, memory, swap and load in the header. The second pane shows every Odoo
instance on the box, with its databases nested underneath. The third pane is a
detail view whose tabs change with what is highlighted:

| Highlighted | Tabs |
| --- | --- |
| an instance | Top, Processes, Stacks, Logs, Config |
| one of its databases | Queries, Users, Locks, Jobs, Crons, Modules |

The tab set is not settled and will move as we use the tool; the rule we hold
to is that whatever earns a tab has to be worth reading during a diagnostic,
not merely available.

Instances are discovered, not configured: `systemd --user` units,
`supervisor` programs, odoo.sh builds, and local instances run directly from
a terminal, merged into one list. From there
everything about an instance resolves from a single file, its config under
`<workdir>/config/`: `odoo.conf` or `server.conf`, and `odooNN.conf` for a
node of a multi-node instance, whose name carries the matching `-NN` suffix
(`foo-01`). That is where the databases come from, the logfile, the db port,
the whole lot.

Remote hosts work the same way:

```bash
oa                       # this machine
oa odoo@somehost         # over ssh
oa odoo@somehost -p 10113
```

The ssh connection is multiplexed, so the first probe opens the session and
everything after reuses it. Refresh runs on a slower tick against a remote
host, and `R` forces the active tab immediately.

## Odoo and Postgres in the Same Table

The **Top** tab lists the instance's workers (walked down the PPID tree
from the master PID the manager reports) and, in the same table, the
postgres backends serving that instance's databases.

That pairing is the part we use most. A backend that is chewing has its
client TCP port in its `ps` title; `lsof` on that port gives back the PID on
the other end, which is the Odoo worker that opened it. "Which query is
slow" and "which worker is stuck" stop being two separate investigations.

## When an Instance Is on Fire

A rough order of operations, from cheapest to most invasive:

1. **Read the header first.** Swap climbing and load high means memory, not
   code, and no amount of stack dumping will tell you that.

2. **Top (`p`).** How many workers, how much memory each, and how many postgres
   backends they are holding open. A single worker at high memory is a different
   problem from all of them being busy.

3. **Dump stacks (`D`).** SIGQUIT to the master and every worker, then read
   the fresh stack dumps back out of the log and parse them. Roughly two
   seconds. Workers come back sorted busy-first, threads busy-before-idle,
   and each traceback is reversed so the innermost frame (the line actually
   running) is the first thing you read, py-spy style.

   This is the `kill -3` trick, minus the log archaeology. SIGQUIT here does
   not kill anything: Odoo installs a handler for it that only prints stacks.

4. **Frames point into postgres?** Move down to the database row and go to
   the **Queries** tab with `[` / `]`. Non-idle queries from
   `pg_stat_activity`, longest-running first.

5. **Stuck rather than slow?** **Locks** (`l`) on the database. Waiting on a
   cron or a queue job? **Crons** (`c`) and **Jobs** (`j`), same place.

6. **Then act.** `K` kills the one bad worker with `-9`. `L` sends `-3` and
   jumps you to the Logs tab if you want the traceback recorded before you
   kill it. `r` restarts the whole instance, `s` toggles start/stop. All of
   them behind a confirm popup.

Steps 1 to 5 change nothing on the target host. That matters when the host in
question is production and you are three minutes into an incident.

## The Same Data, for an Agent

Everything above is exposed as an MCP server:

```bash
oa-mcp odoo@somehost
```

The tools are read-only, deliberately: `list_instances`, `host_stats`,
`instance_top`, `instance_databases`, `instance_version`, `instance_config`,
`instance_log_tail`, `instance_dump_stacks`, `long_queries`, and `db_query` for
a scoped subset of `odoo-db`'s diagnostic commands (modules, crons, jobs, users,
locks). No start, no stop, no restart, no kill.

`oa-mcp` is pinned to one host, the same way `oa <host>` is. A tool call
that names a different host is rejected rather than silently retargeted.
`oa-mcp-multi` leaves the target per-call instead, capped by a `--host-filter`
regex and a `--host-file` of ssh aliases.

The intended shape is two people on one incident, where one of them is an
agent: you on `oa somehost`, it on `oa-mcp somehost`, both looking at the
same instance. You ask why the thing is slow, it dumps stacks and reads
`pg_stat_activity`, and you are already staring at the same table it is
describing.

Action tools will be run by you, not the agent. Starting read-only targets the
real bottleneck: diagnosis

## What It Needs

- The instance managed by `systemd --user`, `supervisor`, running on
  odoo.sh, or run locally, directly from a terminal.
- `odoo-db` on `PATH` for the database tabs, `odoo-config` and
  `odoo-addons-path` for the Config tab.
- `psql` for database discovery and the Queries tab, `lsof` for the
  backend-to-worker link.
- For a remote target: those same tools on the remote host, not on yours.

```bash
uv tool install odoo-activity
```

[odoo-pr-41365]: https://github.com/odoo/odoo/pull/41365

