All three of these do nothing unless logging_collector is on; they describe the files the collector writes. Where, what they’re called, and who can read them. The defaults for all three are fine for a laptop and wrong for a server, and the reasons are more mechanical than the documentation lets on.
log_directory
Default log, context sighup. A relative path is relative to the data directory, not to wherever the postmaster was started from; an absolute path is an absolute path. (The default was pg_log until 10, when the pg_ prefix was reserved for things that are actually part of the cluster.)
Changing it on reload works, mostly. The collector notices the new value, forces a rotation so the next file lands in the new place, and tries to mkdir the directory first, with the same permissions as the data directory (0700, or 0750 if the cluster was created with group access). It ignores a failure to create it. It does not ignore a failure to open a file there: in the sandbox, pointing log_directory at /var/log/pgtest before that directory existed produced could not open log file ... No such file or directory followed by disabling automatic rotation (use SIGHUP to re-enable), and the collector went on writing to the old file in the old directory, with rotation off, until the next reload. A typo in this parameter doesn’t lose your logs. It quietly stops rotating them, and current_logfiles still points at the old file, so nothing looks wrong.
The real question is inside the data directory or out, and the answer is out, for three reasons that have nothing to do with tidiness.
First, pg_basebackup copies it. The base backup code carries an exclusion list (pg_dynshmem, pg_notify, pg_replslot, pg_stat_tmp, pgsql_tmp, and so on), and log is not on it. My sandbox backup came with 4.4 MB of log files. A replica built from a base backup starts life holding the primary’s logs, and every restore drags the logs from before the disaster along with it, which is a confusing thing to find at three in the morning. pg_rewind uses the same filter list, and so does anything else that copies the data directory wholesale.
Second, disk. Logs in the data directory share a filesystem with the heap and, usually, the WAL. A log flood (log_statement = 'all' left on after a debugging session is the classic) fills the data disk, and a full data disk is a stopped server. On their own filesystem, the same flood becomes a logging outage: the collector fails to write, drops messages, and the database carries on. That is the failure you want.
Third, permissions. The directory the collector creates is 0700, and nothing you set in log_file_mode changes that. If anyone other than the postgres user needs to read logs, and the on-call engineer, the log shipper, and the monitoring agent all do, the directory has to be one you created yourself with the mode you meant, which means outside the data directory. (The documentation says the same thing, in the log_file_mode entry, where nobody looks for it.)
Nothing that reads logs through the server cares where they are. pg_ls_logdir() lists whatever log_directory points at, pg_current_logfile() reports the absolute path, and pg_read_file() accepts absolute paths under log_directory even for roles without pg_read_server_files. The one thing to know is that the collector’s mkdir will fail if the parent isn’t writable by postgres, which /var/log is not, so create the directory before you reload, not after.
log_filename
Default postgresql-%Y-%m-%d_%H%M%S.log, context sighup. It’s a strftime pattern, rendered by PostgreSQL’s own strftime (so the standard escapes, none of your libc’s extensions) in log_timezone. For csvlog and jsonlog the collector swaps a trailing .log for .csv or .json, and if the name doesn’t end in .log, appends the suffix instead: server_log.%H%M gives you server_log.0244 and server_log.0244.json side by side.
The name is not cosmetic. It is the retention policy, because of one fact the documentation states in the negative and I’ll state in the positive: PostgreSQL never deletes a log file. The collector’s only calls to unlink are for its own metadata files. Every rotation, whether by age, by size, or by pg_rotate_logfile(), computes a name from the pattern and opens it. If the name is new, you have one more file, forever. If the name already exists, the collector appends to it, unless log_truncate_on_rotation is on and the rotation was time-based, in which case it truncates. (I checked: a constant postgresql.log survived two forced rotations with everything from before and after still in it.)
So the pattern chooses between two regimes. A name that changes every rotation, like the default, gives you complete history and an unbounded directory; something outside PostgreSQL has to compress and delete. A cyclic name like postgresql-%a.log with truncation on gives you a bounded seven files and a hard ceiling on history: last Tuesday is gone when this Tuesday starts. There is no third option inside the server, and the default’s second-granular timestamp is a hint that the project expects you to pick one.
Pick the first, name files by day (postgresql-%Y-%m-%d.log), and run a job that compresses yesterday and deletes last month. The one caveat comes from the docs’ example: if you also rotate by size, a name that only changes daily means the size rotation reopens the same file, so include %H%M if log_rotation_size is in play. All of that is the rotation post’s business; the point here is that the filename decides what rotation can do.
log_file_mode
Default 0600, context sighup, and it’s a number: write it with the leading zero. 600 and 640 without it are decimal, above the 0777 ceiling, and rejected at reload, which is the good outcome; a smaller decimal is accepted and means something you didn’t intend. It’s applied through the umask at the moment the collector creates a file, so a change takes effect on the next rotation and leaves existing files alone. The collector forces the owner’s write bit back on whatever you set (0000 yields 0200, write-only, which I confirmed mostly out of curiosity). It’s ignored on Windows, and it never touches the directory.
0600 means only postgres reads the log. 0640 is the other setting worth having, for a group containing the people and agents that need to read it, and as above it accomplishes nothing inside a 0700 directory. Do not go past that. The log contains every statement that errored, every statement log_statement catches, every connection’s identity, and, if someone has been generous with log_min_duration_statement, the parameters those statements ran with. A world-readable log is a copy of the database’s most interesting data with none of the database’s access controls.
The three together, then: an absolute log_directory on the log filesystem, created by you, 0750, group-owned by whoever ships and reads the logs; log_filename = 'postgresql-%Y-%m-%d.log'; log_file_mode = 0640; and a cron job that does the deleting the server never will.