Wayseer

User guideWayseer 0.28.3Contents

SQL databases

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.

Configuration

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://[email protected]: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.

Type
string
Default
required

path

For sqlite, the database file, which must exist; ~/ is the home directory, and a relative path is from where the app starts.

Type
string

secret_file

A file holding the secret; ~/ is the home directory.

Type
string

secret_env

Or the environment variable holding it.

Type
string

secret_keyring

Or the keyring entry holding it, as service/account.

Type
string

schemas

Only show these schemas.

Type
list of string
Default
sqlite's main, every postgres user schema

interval

How often to read the schema, and to run queries.

Type
duration
Default
1m

timeout

Longest any one statement may run, 100ms to 5m.

Type
duration
Default
5s

queries

Your own SQL, run on an interval, whose rows become series or events.

Type
list

queries[].name

Names the query in errors and on its events.

Type
string

queries[].sql

One statement; it reads at most 1000 rows.

Type
string
Default
required

queries[].interval

How often it runs.

Type
duration
Default
the module's interval

queries[].table

The table the results belong to, as schema.table.

Type
string
Default
the database

queries[].series

The rows as metrics; give series or events.

Type
mapping

queries[].series.time

The column holding each row's time.

Type
string
Default
one point per metric from the first row

queries[].series.metrics

Metric name to its column and unit.

Type
map

queries[].series.metrics.<name>.field

The column holding the value.

Type
string
Default
required

queries[].series.metrics.<name>.unit

The unit: bytes, bytes_per_second, bits, bits_per_second, percent, ratio, seconds, count or per_second.

Type
string

queries[].events

The rows as events.

Type
mapping

queries[].events.id

The column that identifies a row.

Type
string
Default
the time and message together

queries[].events.time

The column holding the event's time.

Type
string
Default
required

queries[].events.message

The column holding its message.

Type
string
Default
required

queries[].events.severity

The column holding debug, info, warn, error or critical.

Type
string
Default
info

path must name a file that exists; the module never creates one. secret_file and secret_env are for Postgres, and hold the DSN.

What shows

KindStatusAttributes
databaseok while it can be readdriver, version, size (bytes)
sql/schema
tabletype (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:

  • the database is the parent of each schema, and a schema is the parent of its tables;
  • a table with a foreign key depends on the table it refers to.

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.

Metrics

Each schema read also records:

MetricKindsMeaning
table.rowstableThe estimated rows, as above
database.sizedatabaseThe database's size on disk

The module keeps the last 1000 points of each metric, and more history needs a query of its own.

Queries

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.

Read-only

Every statement, the module's own and yours, runs in a read-only transaction and stops at timeout:

  • SQLite files are opened read-only, with writes refused on the connection as well. A statement that writes fails with "attempt to write a readonly database". While another program is writing the file, a read waits up to a second for it before failing with "database is locked".
  • Postgres sessions start with 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.