Duck surrounded by continuous computer printout paper spilling from a line printer in a data center, with control panels and indicator lights visible in background.

Every message on its way to the server log passes three gates: whether it gets in at all, whether the SQL that provoked it rides along, and how many of its fields get printed. One parameter per gate. All three control only the log’s copy of a message; what the client sees is client_min_messages’s business, and the two streams are filtered independently. All three are context superuser: settable live in a session, but only by a superuser or a role granted SET on the parameter (15 and later), and a superuser can scope them with ALTER ROLE ... SET or ALTER DATABASE ... SET. A plain login role picks up an ALTER ROLE ... SET log_min_messages at connection time even though it can’t set the parameter itself; I checked.

log_min_messages

The severity floor. Default warning, and the value list is the severity ladder: DEBUG5 through DEBUG1, INFO, NOTICE, WARNING, ERROR, LOG, FATAL, PANIC, each level admitting everything after it. Read that list again. LOG outranks ERROR, and the docs’ entire commentary on the matter is that “LOG has a different rank here than in client_min_messages.” The rank is on purpose: LOG severity is addressed to the administrator (checkpoint reports, autovacuum activity, “database system is ready to accept connections”), so it sits near the top of the log’s ladder and near the bottom of the client’s, where nobody is reading it anyway.

The inversion sets a trap. log_min_messages = 'log' does not mean “normal logging”; it means LOG, FATAL, and PANIC, and nothing else. In my 18.6 sandbox at 'log', a division-by-zero ERROR and a RAISE WARNING left no trace in the log while a RAISE LOG sailed through. A server configured that way narrates its checkpoints and eats its errors. Do not set this parameter to log. The other direction surprises too: 'error' still admits every LOG-severity message (the gate passes LOG whenever the floor is error or lower), so raising the floor is not how you quiet checkpoint and autovacuum chatter; those messages are turned off at the source, by log_checkpoints and its siblings.

Downward: notice and info mostly copy client-directed chatter into the log. The DEBUG levels are for diagnosis, and the operational fact is how uneven they are. In my sandbox, a session at debug1 or debug2 running ordinary statements logged nothing at all; at debug5, one trivial session produced twelve lines, and the cluster emitted 37 lines in under five seconds while doing essentially nothing, narrating every fork, exit, reload, and callback. All five levels print as DEBUG: with no number, so the log won’t tell you which level a line came from. When you need this, need it narrowly: ALTER ROLE that_one_app SET log_min_messages = 'debug2', reproduce, reset. Cluster-wide debug in postgresql.conf is how a disk fills while you’re busy looking at something else.

The default was notice until 8.4. warning is correct; leave it there.

log_min_error_statement

Badly named twice over: it doesn’t log errors, and it doesn’t log statements on its own. (It also isn’t log_min_duration_statement, which selects statements by elapsed time; different mechanism entirely.) What it does: for a message that has already cleared log_min_messages, if the message’s severity also clears this threshold and the backend has a current query, the query text is appended to the entry as a STATEMENT: line. A rider, not a filter. It uses the same value list and the same LOG placement as above, which is why the default of error attaches statements to LOG-severity messages too; a RAISE LOG from PL/pgSQL arrives with its STATEMENT: line under stock settings.

The rider needs a statement to ride: background processes never produce one, and a message raised before the query string is set travels alone. log_statement’s own LOG: statement: lines deliberately suppress it (they would be quoting themselves), but an error in that same statement still gets one, so with log_statement = 'all' a failing query appears twice: once as the statement message, once as the STATEMENT: under its ERROR.

Lower it to warning and warnings carry their SQL, which is occasionally exactly what a flaky application needs pointed at it. Raise it to panic and statement text stays out of the log entirely. That second move sounds perverse until you remember what statements contain: literals, and literals are data. Failing statements are disproportionately the ones carrying malformed or unexpected input, so a log retained for ninety days and shipped to a third-party aggregator is a compliance surface, and error here is how customer data gets onto it. (This was the shipped default, in effect “never,” until 8.2.) That is the one honest reason to raise it. Absent that constraint, keep error: the STATEMENT: line is routinely the single most useful line in the log, because it’s the one that says what you asked for when everything went wrong.

log_error_verbosity

The field selector: terse, default, or verbose, default default, applied to every message the log accepts. terse drops DETAIL, HINT, QUERY, and CONTEXT (and backtraces); the primary line and the STATEMENT: rider survive, since the rider answers to log_min_error_statement, not to this. verbose keeps everything default prints and adds two things: the SQLSTATE spliced in after the severity (ERROR: 23505: duplicate key ...) and a LOCATION: line naming the C function, file, and line that raised it; my duplicate insert reported _bt_check_unique, nbtinsert.c:666.

Note “every message.” At verbose, the routine reload notice in my sandbox arrived tagged LOCATION: ProcessConfigFileInternal, guc.c:568. That line is gold when you’re filing a bug report or reading backend source, and dead weight the rest of the time, on every line, forever. If the SQLSTATE is the part you actually want, %e in log_line_prefix adds it without the tour of the source tree. And if you want the verbose fields interactively, that’s the client’s decision, not the server’s: \set VERBOSITY verbose in psql, because the server sends the full field set to the client regardless and this parameter only shapes the log.

terse looks like a volume optimization and is a bad one: DETAIL is where a unique violation names the key, CONTEXT is where the PL/pgSQL stack trace lives, and the bytes saved are repaid in guesswork. It has one legitimate use, the same one as log_min_error_statement = 'panic': DETAIL also carries data (Key (a)=(1) already exists is a value from your table), so a log that must not contain customer data wants both settings, as a pair. That configuration is a real policy with a real price; you are trading the two most informative lines in the log for a clean audit, and you should make that trade knowing which two lines they are.

For everyone else, this trio’s defaults are the rare set that is simply correct: warning, error, default. Scope your debugging, and spend the configuration effort somewhere it’s needed.