PostgreSQL Mastery
PostgreSQL Mastery lessons / Module 38: Monitoring

Server logs, auto_explain and auditing

Level 7: Operations and HA30 min

The server log is where PostgreSQL tells you about slow statements, lock waits, checkpoints, autovacuum, failed logins and crashes. Configured well it answers most incident questions; left at defaults it records almost nothing useful.

This lesson is interactive. Run every example on a real PostgreSQL 18 server in your browser, read EXPLAIN ANALYZE plans, and take the quiz.
Open the interactive lesson
You will learn to
  • Configure where logs go, how they rotate and what each line starts with
  • Log slow statements, lock waits, temp files and autovacuum with sensible thresholds
  • Capture plans of slow queries with auto_explain and audit activity with pgaudit

conceptThe concept

Where: log_destination is stderr, csvlog, jsonlog (PostgreSQL 15+) or syslog. With logging_collector = on the server captures its own output into log_directory/log_filename and rotates by log_rotation_age and log_rotation_size; pg_current_logfile() tells you the current file. In containers, leave the collector off and let the platform collect stderr.

Prefix: log_line_prefix controls what each line starts with. A useful one is '%m [%p] %q%u@%d/%a ': timestamp, process id, then user, database and application name for client sessions.

What:

SettingGood starting valueCatches
log_min_duration_statement250ms to 1sSlow statements with their duration
log_lock_waitsonAny wait longer than deadlock_timeout, with the blocking pid
log_temp_files0Sorts and hashes spilling to disk (work_mem too small)
log_autovacuum_min_duration10min default; lower to 1s on busy systemsSlow or blocked autovacuum runs
log_checkpointson (default since 15)Checkpoint timing and whether they were requested by WAL volume
log_connectionsauthorization (18 takes a list)Who connected; failed logins are always logged
log_statementddlSchema changes; all is too noisy for production

auto_explain logs the execution plan of any statement slower than auto_explain.log_min_duration, so you see why a query was slow at the moment it was slow, with the parameters it really had. pgaudit writes structured AUDIT lines per statement class (read, write, ddl, role) or per object, for compliance.

whyWhy it matters

Statistics views such as pg_stat_statements tell you which queries are slow on average; the log tells you when one was slow, with which parameters, who ran it, what it waited for and what plan it used. Incident timelines are built from logs.

Use caseReal-world scenario

Checkout latency spikes every night at 02:10. The log shows lock-wait lines naming a reporting job's process, auto_explain shows the job's plan switching to a nested loop over a stale-statistics table, and log_autovacuum lines show autovacuum on that table being cancelled. Three log settings explained the whole incident.

Try itExamples

Run and edit these in the interactive lesson.

What this server logs
SELECT name, setting, context
FROM pg_settings
WHERE name IN ('log_destination', 'logging_collector', 'log_line_prefix', 'log_min_duration_statement',
               'log_statement', 'log_lock_waits', 'log_temp_files', 'log_autovacuum_min_duration',
               'log_checkpoints', 'log_connections', 'log_min_messages')
ORDER BY name;
The context column says how to change each one: sighup means a reload is enough, superuser means it can also be set per session by a superuser, postmaster needs a restart.
auto_explain in one session
LOAD 'auto_explain';
BEGIN;
SET LOCAL auto_explain.log_min_duration = 0;
SET LOCAL auto_explain.log_level = notice;
SET LOCAL client_min_messages = notice;
SELECT count(*) FROM orders WHERE status = 'cancelled';
ROLLBACK;
log_level = notice sends the plan to this client so you can see it here. On a server you leave the level at log and read the plan from the log file.
Plans with actual rows and buffers
LOAD 'auto_explain';
BEGIN;
SET LOCAL auto_explain.log_min_duration = 0;
SET LOCAL auto_explain.log_analyze = on;
SET LOCAL auto_explain.log_buffers = on;
SET LOCAL auto_explain.log_timing = off;
SET LOCAL auto_explain.log_level = notice;
SET LOCAL client_min_messages = notice;
SELECT c.tier, count(*)
FROM orders o JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'paid'
GROUP BY c.tier;
ROLLBACK;
log_timing = off keeps row counts but skips per-node clock calls, which is most of the overhead of log_analyze.
A production logging baseline
# postgresql.conf
logging_collector = on
log_destination = 'jsonlog'
log_directory = 'log'
log_filename = 'postgresql-%a.log'          # one file per weekday
log_truncate_on_rotation = on
log_rotation_age = 1d
log_line_prefix = '%m [%p] %q%u@%d/%a '
log_min_duration_statement = 500ms
log_lock_waits = on
log_temp_files = 0
log_autovacuum_min_duration = 1s
log_checkpoints = on
log_connections = 'authorization'
log_statement = 'ddl'

shared_preload_libraries = 'pg_stat_statements,auto_explain'
auto_explain.log_min_duration = '2s'
auto_explain.log_analyze = on
auto_explain.log_timing = off
auto_explain.log_nested_statements = on
Auditing with pgaudit
# postgresql.conf (pgaudit is a separate package)
shared_preload_libraries = 'pg_stat_statements,pgaudit'
pgaudit.log = 'ddl, role, write'
pgaudit.log_catalog = off
pgaudit.log_parameter = off      -- parameters can contain personal data

-- object-level auditing: log every SELECT on the cards table only
CREATE ROLE auditor NOLOGIN;
ALTER SYSTEM SET pgaudit.role = 'auditor';
GRANT SELECT ON cards TO auditor;

-- a resulting log line
-- AUDIT: SESSION,12,1,WRITE,UPDATE,TABLE,public.accounts,"UPDATE accounts SET ...",<not logged>
Reading the log
# slowest statements in today's JSON log
jq -r 'select(.message | startswith("duration")) | [.timestamp, .user, .dbname, .message] | @tsv' \
  log/postgresql-Thu.json | sort -t$'\t' -k4 -r | head

# a full report with charts
pgbadger -j 4 -o report.html log/postgresql-*.json

internalsUnder the hood

Backends write log messages to stderr; with the collector on, a dedicated logger process reads a pipe and writes files, so slow log storage can back-pressure the database. Every message has a severity (DEBUG5 to PANIC); log_min_messages filters what reaches the log, client_min_messages what reaches the client. auto_explain hooks the executor: with log_analyze it enables instrumentation for every statement, which costs CPU even for fast queries, so use log_timing = off or sample_rate on busy systems.

Avoid / preferDo this, not that

Avoid
log_statement = 'all'
Prefer
log_min_duration_statement = 500ms
log_statement = 'ddl'

Logging every statement can produce more I/O than the workload itself, fills disks and buries the slow statements you were looking for. For per-statement records use pgaudit with a scoped class list.

Avoid
auto_explain.log_min_duration = 0
auto_explain.log_analyze = on
Prefer
auto_explain.log_min_duration = '2s'
auto_explain.log_analyze = on
auto_explain.log_timing = off

Analyzing and logging every plan slows every query and floods the log. Only slow statements need a plan.

PitfallsCommon mistakes

  • Leaving log_line_prefix without user, database and application name, so a slow statement cannot be traced to a service.
  • Putting the log directory on the same volume as the data directory with no rotation, then filling the disk.
  • Enabling log_parameter or log_min_duration_statement with bind parameters on tables containing card numbers or personal data.
  • Reading only pg_stat_statements during an incident and never looking at lock-wait and autovacuum log lines.

LimitsWhen not to use it

Logs are not a metrics system: do not parse them for request rates or connection counts that pg_stat_* views give you directly and cheaply. And do not use log_statement = 'all' as an audit trail; pgaudit produces structured, filterable records for that.

SecuritySecurity notes

Logs contain SQL text, which can include personal data, passwords in ALTER ROLE ... PASSWORD statements and literal values. Restrict log file permissions (log_file_mode), ship them to a store with access control and retention rules, and never paste raw logs into tickets or chat tools without redaction.

ChallengeTry it yourself

Use auto_explain to capture the plan of SELECT rating, count(*) FROM reviews GROUP BY rating with actual row counts, without leaving any setting changed.

Show one solution
One solution
LOAD 'auto_explain';
BEGIN;
SET LOCAL auto_explain.log_min_duration = 0;
SET LOCAL auto_explain.log_analyze = on;
SET LOCAL auto_explain.log_level = notice;
SET LOCAL client_min_messages = notice;
SELECT rating, count(*) FROM reviews GROUP BY rating;
ROLLBACK;

InterviewQuestions you should be able to answer

Which logging settings do you change on a fresh production server, and why?
log_line_prefix to include timestamp, pid, user, database and application; log_min_duration_statement at a few hundred milliseconds for slow statements; log_lock_waits on to see blocking; log_temp_files = 0 to spot work_mem spills; log_autovacuum_min_duration low enough to see slow vacuums; log_checkpoints on; log_connections for access records; log_statement = ddl for schema changes. Then rotation and a destination the platform can ship, ideally jsonlog.
How do you find out why a query was slow last night when it is fast now?
pg_stat_statements only shows averages, and running EXPLAIN now shows today's plan. auto_explain with a duration threshold logs the actual plan and parameters at the moment the statement was slow. Combine it with log_lock_waits and autovacuum log lines from the same minutes to see whether it was a plan change, a lock wait or I/O contention.
pgaudit or log_statement = all?
pgaudit. It logs by class (read, write, ddl, role, function) or by object through an audit role, marks each line as AUDIT with a statement class and object name, and can exclude catalog queries. log_statement = all records everything without structure, at a volume that often hurts performance and still misses the object context auditors need.

QuizCheck your understanding

5 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.

Go deeperOfficial documentation