driver
The database: sqlite or postgres.
- Type
- string
- Default
- required
User guideWayseer 0.28.3Contents
The sql module shows a SQLite or Postgres database as its schemas and tables, with foreign keys as links between tables. It can also run queries you write, on an interval, and show their results as metrics or as events. It only reads. Every statement runs in a read-only transaction, so it cannot change the database.
The SQL module is built into Wayseer, so there is nothing to install. A SQLite database is a file:
modules:
- kind: sql
name: shop
options:
driver: sqlite
path: ~/data/shop.db
A Postgres database is reached through a connection string (a DSN), which can hold a password. So the DSN is never written in the config. Put it in a file, or in an environment variable, and name that instead:
modules:
- kind: sql
name: orders
options:
driver: postgres
secret_env: ORDERS_DSN
schemas: [public, billing]
The DSN is one line, such as postgres://wayseer@db.internal:5432/orders?sslmode=verify-full. Both URL and key=value forms work. The usual PG* environment variables fill in anything the DSN leaves out, from the environment Wayseer was started with. With secret_file, the file holds the same line, and with secret_keyring the keyring entry does (secrets). Errors never show the DSN, the password or the server's address. For example, a server that is down shows as "cannot connect: nothing is listening at the server's address and port".
Nothing connects to a database until a kind: sql instance is configured.
driver
The database: sqlite or postgres.
path
For sqlite, the database file, which must exist; ~/ is the home directory, and a relative path is from where the app starts.
secret_file
A file holding the secret; ~/ is the home directory.
secret_env
Or the environment variable holding it.
secret_keyring
Or the keyring entry holding it, as service/account.
schemas
Only show these schemas.
interval
How often to read the schema, and to run queries.
timeout
Longest any one statement may run, 100ms to 5m.
queries
Your own SQL, run on an interval, whose rows become series or events.
queries[].name
Names the query in errors and on its events.
queries[].sql
One statement; it reads at most 1000 rows.
queries[].interval
How often it runs.
queries[].table
The table the results belong to, as schema.table.
queries[].series
The rows as metrics; give series or events.
queries[].series.time
The column holding each row's time.
queries[].series.metrics
Metric name to its column and unit.
queries[].series.metrics.<name>.field
The column holding the value.
queries[].series.metrics.<name>.unit
The unit: bytes, bytes_per_second, bits, bits_per_second, percent, ratio, seconds, count or per_second.
queries[].events
The rows as events.
queries[].events.id
The column that identifies a row.
queries[].events.time
The column holding the event's time.
queries[].events.message
The column holding its message.
queries[].events.severity
The column holding debug, info, warn, error or critical.
path must name a file that exists; the module never creates one. secret_file and secret_env are for Postgres, and hold the DSN.
| Kind | Status | Attributes |
|---|---|---|
database | ok while it can be read | driver, version, size (bytes) |
sql/schema | ||
table | type (table, view or materialized view), columns, rows |
rows is an estimate. On Postgres it is the planner's estimate, which is up to date after ANALYZE or autovacuum; a table never analyzed has no rows. On SQLite it comes from sqlite_stat1 after ANALYZE, and otherwise from the largest row ID. Views have no rows.
The links are:
Partitions of a Postgres partitioned table are not shown; the partitioned table is.
Some filters:
/source:shop kind:table
/kind:table rows>1000000
/kind:table type=view
A table added or dropped shows at the next schema read. If the database cannot be read, the module's health shows why, and the last known schema stays on screen.
Each schema read also records:
| Metric | Kinds | Meaning |
|---|---|---|
table.rows | table | The estimated rows, as above |
database.size | database | The database's size on disk |
The module keeps the last 1000 points of each metric, and more history needs a query of its own.
A query is SQL you write. It runs on its interval, and its results show as a metric or as events.
modules:
- kind: sql
name: shop
options:
driver: sqlite
path: ~/data/shop.db
queries:
- name: orders
sql: select count(*) as n, sum(total) as revenue from orders
interval: 30s
series:
metrics:
orders.count: {field: n, unit: count}
orders.revenue: {field: revenue}
- name: failures
sql: select id, at, level, msg, customer_id from log where level <> 'debug' order by at desc limit 200
table: main.log
events: {id: id, time: at, severity: level, message: msg}
Each query has a name, used in errors and on its events, of lowercase letters, digits, _, - and .. Its sql is one statement, and it reads at most 1000 rows. Its table, as schema.table, must be in the schemas shown. Give it series or events, which say what the rows become. The table above lists every field.
Series. Each entry under metrics names a metric, the column it comes from, and its unit, which can be left out. The units are bytes, bytes_per_second, bits, bits_per_second, percent, ratio, seconds, count and per_second. Without time, the query's first row gives one point per metric, at the time the query ran. With time: <column>, every row gives a point at that row's time. That suits a table that already keeps a history:
modules:
- kind: sql
name: plant
options:
driver: sqlite
path: ~/data/plant.db
queries:
- name: temperature
sql: select at, celsius from readings where at > datetime('now', '-1 hour') order by at
series: {time: at, metrics: {plant.temperature: {field: celsius}}}
Events. Each row becomes an event on the Timeline, with the kind query. time and message name the columns they come from, and both are needed. severity names a column holding debug, info, warn, error or critical; without it, or for any other value, the event is info. id names a column that identifies the row; without it, the time and message together do. The other columns become the event's fields, together with a query field holding the query's name.
An event shows once. Later runs that return the same row do not show it again. So a query can simply return the latest rows each time, as in the example above.
Times can be timestamp columns, text such as 2026-09-01T10:00:00Z or 2026-09-01 10:00:00, or Unix seconds. Text without a time zone is read as UTC.
When a query fails, for example because of a typo, or because a column it names is missing, the module's health shows the error, starting with query <name>:. The rest of the module carries on.
Every statement, the module's own and yours, runs in a read-only transaction and stops at timeout:
default_transaction_read_only on and statement_timeout set to timeout, and each statement runs inside BEGIN READ ONLY. A statement that writes fails with "cannot execute … in a read-only transaction".For Postgres, also connect as a role that can only read. This read-only role covers the schemas the module shows:
create role wayseer login password '…';
grant connect on database orders to wayseer;
grant usage on schema public to wayseer;
grant select on all tables in schema public to wayseer;
alter default privileges in schema public grant select on tables to wayseer;
The module reads the catalog (pg_class, pg_namespace, pg_constraint), which every role can read, so the schema shows even for tables the role cannot select from. Only queries need select.