Operations¶
This covers what changes once rel is running somewhere real: how to read its logs, what an error response means and what to do about it, and how a schema change reaches a live deployment.
Logging¶
rel logs one line per event to stdout — no file destinations or rotation to configure; that's the job of whatever supervises the process (systemd, your container runtime).
logging.handler—pretty(default) for colored, human-readable single-line output during local development, orJSONfor one JSON object per line in production.logging.level—debug,info(default),warn, orerror; the minimum level actually emitted.logging.filter.<key>— a regexp a log line's<key>attribute must match to be shown at all. A line with nothing to filter against for that key is shown regardless.logging.exclude.<key>— same shape, inverted: suppresses a matching line. Applied after anylogging.filter.*you've also set.
See Configuration for how these compose with every other setting (env var, flag, or config file, in that precedence).
Every line carries a module attribute (query, route, pg, reloadcmd, ...) saying which part
of rel produced it. Every line tied to a specific HTTP request also carries request_id —
either forwarded from that request's own X-Request-Id header, or generated by rel when the
header is absent — so you can grep one request's full lifecycle out of the log, end to end.
/rel and /route/* each log one line per completed request unconditionally, at info, with
the method, path, status code, duration, response size, resolved role, and the full JWT claims
of whoever made the request (including any custom claim a login function attached, such as a
user ID) — you don't need to instrument individual routes to get that. At debug, one further
line per query is logged for the same request, with the exact SQL text and parameters sent to
Postgres — nothing held back or redacted, so narrow it down with logging.filter/
logging.exclude rather than expecting rel to do it for you.
Schema introspection (startup, and every reload) logs one info summary line with the
table/function/type counts it found ; at debug, one line per relation/function/type
discovered, and one line per route/middleware actually registered and exposed over HTTP.
Error responses¶
Every error response, from /rel or /route, carries a code: a stable, safe-to-switch-on
token, always present, never derived from user input. It arrives two ways at once — an
X-Rel-Errorcode response header (works regardless of body framing, including /route's
plain-text error paths), and, for /rel specifically, a code field in the JSON body:
{
"status": "error",
"code": "PG_UNIQUE_VIOLATION",
"error": "a unique constraint was violated",
"pg_error": {
"message": "duplicate key value violates unique constraint \"room_types_property_id_name_key\"",
"constraint_name": "room_types_property_id_name_key",
"table_name": "room_types"
}
}
There are two families of code, and telling them apart matters for how much detail you can expect in the response:
- Postgres-raised (
RSxxx) — araise exception ... using errcode = 'RS404'inside a Postgres function you wrote yourself picks its own HTTP status this way (RS404→404). This is how a function-level check (a business rule, an authorization decision made in SQL) reports back to the client without rel needing to know about it in advance. - rel-internal (
SCREAMING_SNAKE_CASE) — everything rel classifies itself: malformed requests, query-compile rejections, auth gates, connectivity failures. A few worth knowing by name:
code |
Status | Meaning |
|---|---|---|
UNKNOWN_IDENTIFIER |
400 | a relation, column, function, or operator name didn't resolve |
JOIN_MISSING_INDEX |
400 | a join's own columns aren't indexed — rel refused to compile a query that would scan per parent row |
WRITE_FORBIDDEN |
400 | the write touched a relation whose identity isn't writable, or violated a write_mode rule |
QUERY_ROLLBACK_NOT_GRANTED |
400 | rollback: true requested but pg.query.allow_rollback is off — see Complex queries |
QUERY_STATS_QUERY_PLAN_CONFLICT |
400 | stats and query_plan both requested on the same write |
ANONYMOUS_DISABLED |
401 | request has no credentials and no anonymous role is configured |
NO_ROLE_CONFIGURED |
500 | pg.query.anonymous_role is unset and the request is anonymous — a deployment misconfiguration, not an ordinary request failure |
DB_UNAVAILABLE |
500 | acquiring a Postgres connection failed |
TRANSACTION_ERROR |
500 | begin/commit/rollback itself failed |
A Postgres constraint your own request's data triggered — a unique, foreign key, not-null, or
check violation — is always described in full, in production too: the constraint and column
names came from data you supplied, so they're not new information to you. Anything else (a
raw permission-denied message, an unclassified failure) stays a fixed, generic message and
status in production, and only expands to the full underlying text when the server runs with
dev: true. Never rely on parsing that generic message — branch on code, which is always
present regardless of mode.
Migrations and schema reload¶
rel doesn't embed a migration tool — reload.cmd names whatever command should run before
every reload (startup, and every SIGUSR1), and rel reintrospects the schema after. See
Reload for the full detail : the command line/interpolation syntax, output
logging, failure handling, and how to wire up dmut (this
repo's own dev database uses it, just test-db-migrate/just test-db-fresh).
To pick up a schema change on a running deployment without restarting the process, send it
SIGUSR1: