PostgreSQL provides one of the most powerful and flexible internal logging systems among open-source databases. Understanding this logging system is essential for developers who modify PostgreSQL source code, build custom extensions, or debug core internals such as heap insert, vacuum, buffer access, and transaction processing.
In this blog, we will explain how PostgreSQL logging works at the source-code level using ereport, how the debug levels DEBUG1 to DEBUG5 behave internally, and how the configuration parameters client_min_messages and log_min_messages control what we see in the terminal and the log file. All results in this blog are taken from real experiments performed by modifying the heap_insert function in PostgreSQL 18 source code.
PostgreSQL Source Code Logging Using ereport
In backend source code, PostgreSQL uses the ereport macro for all structured logging and error handling. This macro is defined through the utils elog headers and internally routed through errstart and errfinish.
You can explore more about these ereport macros from the header file of the PostgreSQL source code
#include "utils/elog.h"
The general format is:
ereport(LOG, (errmsg("message")));
ereport(WARNING, (errmsg("message")));
ereport(INFO, (errmsg("message")));
ereport(DEBUG1, (errmsg("message")));
ereport(DEBUG2, (errmsg("message")));
ereport(DEBUG3, (errmsg("message")));
ereport(DEBUG4, (errmsg("message")));
ereport(DEBUG5, (errmsg("message")));
ereport(ERROR, (errmsg("message")));To test logging behavior, the following code was inserted directly inside the heap_insert function from the Postgres source codeThis is the path of the heap_insert function from the postgres source code
/home/cybrosys/postgres_18/postgresql/src/backend/access/heap/heapam.c
Add the ereport macro’s inside the heap_insert function
Purpose of heap_insert function in postgres :
- It physically inserts a new tuple (row) into a heap table by finding a suitable page with enough free space.
- It sets the necessary transaction and visibility information (xmin, xmax, command ID) for MVCC consistency.
- It writes WAL (Write-Ahead Log) records so the insert can be safely recovered after a crash.
- It updates page-level metadata such as free space information for future inserts.
void
heap_insert(Relation relation, HeapTuple tup, CommandId cid,
int options, BulkInsertState bistate)
{
ereport(LOG, (errmsg("Cybro Postgres Team Test Log")));
ereport(WARNING, (errmsg("Cybro Postgres Team Test Warning ")));
ereport(INFO, (errmsg("Cybro Postgres Team Test Info")));
ereport(DEBUG1, (errmsg("Cybro Postgres Team Test Debug1")));
ereport(DEBUG2, (errmsg("Cybro Postgres Team Test Debug2")));
ereport(DEBUG3, (errmsg("Cybro Postgres Team Test Debug3")));
ereport(DEBUG4, (errmsg("Cybro Postgres Team Test Debug4")));
ereport(DEBUG5, (errmsg("Cybro Postgres Team Test Debug5")));
ereport(ERROR, (errmsg("Cybro Postgres Team Test Error")));
}
After compiling and installing PostgreSQL again, insert queries were executed to observe the behavior of each logging level.
Create a demo table and do the insert operation for calling this function from postgres source code to evoke the logging.
Create table demo(id int,name varchar);
Insert into demo(id, name) values (1,'marc_demo');
Understanding log_min_messages and client_min_messages
PostgreSQL provides two key parameters to control logging visibility:
log_min_messages
This controls what messages go to the PostgreSQL server log file.
client_min_messages
This controls what messages are sent to the client, such as psql.
You can check these parameters on the postgres config file
Enter the command below to see the path of the config file
Show config_file;
You get a result like this
config_file
---------------------------------------------------------------
/home/cybrosys/postgres_18/postgresql/pg_data/postgresql.conf
See the parameters named client_min_messages and log_min_messages
The values are ordered from most detailed to least detailed as follows:
#------------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
#------------------------------------------------------------------------------
# - Statement Behavior -
client_min_messages = debug5 # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# log
# notice
# warning
# error
Behavior with client_min_messages = debug1 and log_min_messages = debug1
Configuration:
client_min_messages = debug1
log_min_messages = debug1
Insert executed:
insert into demo(id,name) values (2,'jhon');
Terminal output:
LOG: Cybro Postgres Team Test Log with ereport - log
WARNING: Cybro Postgres Team Test Warning
INFO: Cybro Postgres Team Test Info
DEBUG: Cybro Postgres Team Test Debug1
INSERT 0 1
Log file output:
LOG: Cybro Postgres Team Test Log with ereport - log
STATEMENT: insert into demo(id,name) values (2,'jhon');
WARNING: Cybro Postgres Team Test Warning
INFO: Cybro Postgres Team Test Info
DEBUG: Cybro Postgres Team Test Debug1
Important rule:
PostgreSQL shows only messages that are equal to or more important than the configured level.
DEBUG5 = most detailedDEBUG1 = least detailed among debug levels
Behavior with client_min_messages = debug2 and log_min_messages = debug2
Configuration:
client_min_messages = debug2log_min_messages = debug2
Insert executed:
insert into demo(id,name) values (3,'mitchell_admin');
Terminal output:
LOG: Cybro Postgres Team Test Log with ereport - logWARNING: Cybro Postgres Team Test Warning INFO: Cybro Postgres Team Test InfoDEBUG: Cybro Postgres Team Test Debug1DEBUG: Cybro Postgres Team Test Debug2INSERT 0 1
Log file output:
LOG: Cybro Postgres Team Test Log with ereport - logSTATEMENT: insert into demo(id,name) values (3,'mitchell_admin');WARNING: Cybro Postgres Team Test Warning qINFO: Cybro Postgres Team Test InfoDEBUG: Cybro Postgres Team Test Debug1DEBUG: Cybro Postgres Team Test Debug2
This proves that when the threshold is set to debug2, both DEBUG1 and DEBUG2 are allowed.
Behavior with client_min_messages = debug3
Configuration:
client_min_messages = debug3log_min_messages = debug3
Query executed:
select * from demo;
At this level, apart from custom debug logs, PostgreSQL started printing a large number of internal debug messages related to asynchronous IO and buffer access such as:
DEBUG: io 1 |op readv|target smgr|state STAGED
DEBUG: io 1 |op readv|target smgr|state COMPLETED_IO
DEBUG: io 1 |op readv|target smgr|state COMPLETED_SHARED
DEBUG: io 1 |op readv|target smgr|state COMPLETED_LOCAL
This confirms that debug3 already enables deep storage and IO subsystem debugging.
Behavior with client_min_messages = debug4 and debug5
When both parameters were set to debug4 and debug5:
client_min_messages = debug5
log_min_messages = debug5
Even a simple insert caused PostgreSQL to log:
- Transaction begin and commit states
- Autovacuum decision logic
- Shared memory exit handlers
- Asynchronous IO lifecycle
- Buffer read and completion stages
Example insert:
insert into demo(id,name) values (5,'paul'');
Terminal output contained:
DEBUG: StartTransaction
DEBUG: io readv HANDED_OUT
DEBUG: io state STAGED
DEBUG: io state SUBMITTED
DEBUG: io state COMPLETED_IO
DEBUG: io state COMPLETED_SHARED
DEBUG: CommitTransaction
Log file also showed the same messages along with vacuum statistics and background worker activity.
This demonstrates that debug5 activates the deepest possible internal debugging mode of PostgreSQL.
Why All Debug Messages Appear as DEBUG Without Numbers
Even when using DEBUG1 through DEBUG5 in the source code, PostgreSQL prints the severity label only as DEBUG in both the terminal and the log file. The numeric depth is not displayed. The numeric levels are only used internally for filtering.
For example:
ereport(DEBUG3, (errmsg("Cybro Postgres Team Test Debug3")));Appears as:
DEBUG: Cybro Postgres Team Test Debug3
There is no visible difference in the prefix between DEBUG1 and DEBUG5.
Summary of Observed Behavior
When log_min_messages and client_min_messages are set:
- debug1 - Only DEBUG1, INFO, WARNING, LOG, and ERROR appear.
- debug2 - DEBUG1 and DEBUG2 appear along with higher levels.
- debug3 - PostgreSQL begins printing internal IO, buffer, and transaction debug messages.
- debug4 - Very deep internal state transitions appear.
- debug5 - Complete internal debugging of PostgreSQL including autovacuum, shared memory, async IO, and commit lifecycle.
Lower debug numbers mean fewer details. Higher debug numbers mean deeper internal visibility.
Key Differences Between log_min_messages and client_min_messages
log_min_messages controls what goes to the server log file.client_min_messages controls what appears in the client terminal such as psql.
Both use the same severity hierarchy, but they operate independently. You can log everything to the file while showing only warnings to the user.
Practical Use Case for PostgreSQL Developers
By inserting ereport calls directly into PostgreSQL source code and tuning log_min_messages and client_min_messages, you can:
- Trace execution of core functions like heap_insert
- Debug asynchronous IO behavior
- Observe buffer reads and writes
- Track transaction lifecycle
- Analyze autovacuum decision logic
- Build internal PostgreSQL monitoring tools
This technique is especially useful when working on PostgreSQL performance optimization, storage engine research, and custom instrumentation layers.
Conclusion
PostgreSQL’s ereport-based logging system is not just for error handling. It is a complete, structured diagnostic framework that can expose every internal subsystem of the database when used with DEBUG1 to DEBUG5 and the correct runtime configuration.
Through real source code modification and controlled experiments, we verified exactly how each debug level behaves and how PostgreSQL internally filters messages using client_min_messages and log_min_messages. This hands-on exploration shows that PostgreSQL’s logging system is both precise and extremely powerful for developers working at the database core level.