- PostgreSQL and MySQL: single
SELECTstatement returning one row with one column. - Redis: single read-only command from an allowlist (DBSIZE, LLEN, SCARD, ZCARD, HLEN, STRLEN, TTL, PTTL, BITCOUNT, EXISTS, GET).
- MongoDB: a JSON specification with
db,collection, andop(countDocumentsorestimatedDocumentCount); an optionalfilternarrows the count.
When NOT to use this
- The signal already exists in Prometheus (via
postgres_exporter,mysqld_exporter, or your application’s metrics endpoint). Use the Prometheus source. It avoids running a query against the production database on the cron interval. - The query is expensive. A probe runs every minute by default; a query that takes 500 ms is fine, a query that takes 5 seconds is a recipe for a database-CPU regression. The agent enforces a hard statement timeout (default 5 s, max 30 s), but the right answer for expensive queries is to materialize them into a summary table the probe reads cheaply.
- You need transaction-aware metrics. The probe issues a single one-shot query; it cannot observe values inside a transaction at the caller’s isolation level.
Critical: provision a read-only role
The agent parser rejects non-SELECT statements before they leave the host, but that parser is belt-and-suspenders. The actual security boundary is the database role the agent connects as. Provision a dedicated read-only role and use it for every database probe.PostgreSQL
sslmode=verify-full when the agent host trusts the database’s
CA; fall back to sslmode=require only if certificate distribution
isn’t in place. require encrypts the connection but doesn’t verify
the server identity.
MySQL
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'observer_agent'@'%';
so the replication views work. On MySQL 8.0+ the SELECT grant above
is sufficient and avoids letting the role see queries from every
other session.
Export the DSN on the agent host:
Redis
Provision a read-only ACL user. Redis 6+ supports per-command ACLs; restrict the agent user to the same allowlist the agent enforces.requirepass and rely on the agent’s
client-side allowlist; the parser rejects every mutating command at
save time, so a misconfigured legacy user still can’t fire FLUSHALL.
Export the DSN on the agent host:
MongoDB
Provision a read-only role on the specific database (oradmin if
you need cross-database reads).
Configure a metric in the Observer console
Create a metric, pick SQL query, fill in:-
Database:
PostgreSQL,MySQL,Redis, orMongoDB. -
Statement timeout: per-query timeout in milliseconds. Default
5,000; max 30,000. The agent applies this at the database
connection level (Postgres
statement_timeout, MySQLMAX_EXECUTION_TIME) so a stuck query aborts even if the agent-side timeout is late to fire. -
Connection string env var: name of an env var on the agent
host (
OBSERVER_PG_PROD_DSNin the examples above). The cloud never sees the DSN; the agent reads the value at query time and never logs it. -
Query: shape depends on the database kind.
- PostgreSQL / MySQL: a single
SELECT(orWITH … SELECT) returning exactly one row with one column. - Redis: a single command from the allowlist (
DBSIZE,LLEN,SCARD,ZCARD,HLEN,STRLEN,TTL,PTTL,BITCOUNT,EXISTS,GET). Mutating commands are rejected at save time. - MongoDB: a JSON object with
db,collection,op(countDocumentsorestimatedDocumentCount), and an optionalfilter. Server-side JS operators ($where,$function,$expr) are rejected.
- PostgreSQL / MySQL: a single
Result shape
The query must return exactly one row and one column. The value is coerced to a number:
When the metric you care about is naturally multi-row (e.g. one row
per replication slot), reduce it inside the query (
max, sum,
count) so a single scalar reaches the agent.
Sample queries
PostgreSQL
MySQL
Redis
Each command is one line; the agent rejects anything outside the read-only allowlist before it reaches the server.GET) are parsed as numbers. Non-numeric strings surface as
db_non_numeric.
MongoDB
Each metric definition stores a JSON object that names the database, collection, and operation. Whitespace inside the JSON is preserved verbatim, so format it however you prefer.filter accepts any BSON-serialisable JSON, with four exceptions:
$where, $function, $accumulator, and $expr are rejected at
save time. These operators trigger server-side JavaScript or full-
collection evaluation and don’t belong in a per-cron probe.
estimatedDocumentCount reads from cached metadata and never accepts
a filter; use it when an exact count isn’t required.
Reason codes specific to SQL
Thereason field on no_data results carries one of:
db_dsn_missing: the env var named inconnection_string_refis not set on the agent. Export it and restart the agent.db_query_not_allowed: the parser rejected the query (e.g. not a SELECT, multi-statement body, or write inside a CTE).db_empty_result: the query returned zero rows. Fix the WHERE clause or change the metric to acount(*)so it always returns one row.db_multi_row/db_multi_column: the query returned more than one row or column. Reduce withcount,max,sum, or use a subquery.db_null_value: the single returned cell was NULL. Usecoalesce(value, 0)if zero is the right fallback for your metric, or add a WHERE clause that filters out the NULL state.db_non_numeric: the cell wasn’t a number, boolean, or numeric string. Cast inside the query (::int,::numeric,CAST(... AS UNSIGNED)).db_timeout: the query exceededstatement_timeout_ms. Optimize the query or raise the timeout (cap is 30,000 ms).db_invalid_timeout:statement_timeout_msis not a positive number. Set it between 100 and 30,000 ms.db_access_denied: the role lacks SELECT privilege on the table. Grant it explicitly.db_auth_failed: the DSN’s username / password is wrong.db_connection_failed: the database is unreachable (ECONNREFUSED,ENOTFOUND,ETIMEDOUT, or driver-level network errors). The metric goesno_data, notunhealthy. The database being down is a separate concern from the metric being out of range.db_syntax_error: the query is malformed or references a missing table / column.db_error: catch-all for uncategorized driver errors.
Performance
- One probe = one query per cron interval. At the default 1-minute interval, every metric def runs 1,440 queries per day. Keep each query under a few hundred milliseconds.
- The agent caps its connection pool at 2 connections per unique (DSN, timeout) pair. Even with 50 metric defs against the same database, the agent never holds more than 2 connections open.
- Pool entries idle out after 10 seconds, so a probe that runs every 5 minutes typically holds zero open connections between ticks.
- The agent never retries a failed query inside a tick; failures
surface as
no_dataand the next cron tick is the next attempt.
Troubleshooting
db_dsn_missingeven though I set the env var. The agent reads the variable at query time fromprocess.env, so it must be present in the agent process’s environment, not just in your shell. For Docker / Kubernetes deployments, set it on the container or pod spec.db_query_not_allowed: UPDATE statements are not allowed. The parser is strict. Even if your role is read-only, the parser rejects the statement before sending it. Rewrite the query as a SELECT.db_timeouton a query that runs fine inpsql. The database server may be applying a per-sessionstatement_timeoutthat’s lower than the agent’s. Check the role’sstatement_timeoutsetting (SHOW statement_timeout) and either raise it or lower the agent’sstatement_timeout_ms.db_connection_failedfrom inside a Kubernetes pod, but the same DSN works from a debug pod. Most often a NetworkPolicy or egress rule blocks the agent’s pod. Confirm by execing into the agent pod and curling the database host:port.db_multi_rowon a query that returns one row inpsql. Postgres’spsqlsometimes treats multi-row results differently with\gvs\gx. The agent reads every returned row; if your query has any path that returns multiple rows, reduce withLIMIT 1or an aggregate.
Known limits
- PostgreSQL, MySQL, Redis, and MongoDB only. SQLite, BigQuery,
Snowflake, and ClickHouse are not implemented. The Redis runtime
supports a fixed allowlist of read-only commands; the MongoDB
runtime supports
countDocumentsandestimatedDocumentCountonly. Aggregation pipelines + Mongofindare deferred until customer demand surfaces. - No parameterized queries. The query is taken from configuration verbatim. There is no string interpolation surface, so there’s no injection vector, but it also means runtime values (e.g. “alert if more than X rows match”) have to be baked into the SQL.
- No “test connection” button in the form. The cloud has no path
to the customer’s network. Save the metric and watch the agent
dashboard for the first probe result; failures surface as a
no_datarow with a specific reason code within one cron tick.

