log_replication_commands is log_statement = all for walsenders, with the one difference that you should turn it on.
A replication connection (replication=1 for physical, replication=database for logical) speaks a small language of its own: IDENTIFY_SYSTEM, CREATE_REPLICATION_SLOT, START_REPLICATION, BASE_BACKUP, DROP_REPLICATION_SLOT, and a few others. log_statement never sees them. This parameter does, and writes each one to the log at LOG level as received replication command: .... The default is off, the context is superuser (so postgresql.conf and a reload), and it has been there since 9.5. When it is off the same lines are still emitted at DEBUG1, which is to say into the void.
What you get is a record of what every standby, backup tool, and logical consumer asked for, not just that it connected (log_connections covers that half). A standby starting up:
1 LOG: received replication command: START_REPLICATION SLOT "phys1" 0/1000000 TIMELINE 1
A logical subscriber, with the publications and protocol version it asked for, which is the first thing you want to know when a cross-version subscription misbehaves:
1 LOG: received replication command: START_REPLICATION SLOT "sub" LOGICAL 0/0 (proto_version '4', streaming 'parallel', origin 'any', publication_names '"pub"')
And pg_basebackup, with its label and checkpoint mode, timestamped, so “who took a backup at 14:07 and forced a checkpoint” has an answer. Slot creation and removal over the protocol show up the same way (a slot created with pg_create_logical_replication_slot() in SQL is log_statement’s problem). That matters because the slot holding back your WAL was usually created by a tool, not a person, and this is the only place its creation gets a timestamp.
PostgreSQL 17 added more: whenever a walsender acquires or releases a slot, the log says so (acquired physical replication slot "phys1", released logical replication slot "sub"), gated on this same parameter. pg_replication_slots.active_pid tells you whether a slot is in use now, and inactive_since (also 17) tells you when it stopped being; the acquire/release lines give you the history, which is how you learn that the standby everyone believes is stable has reconnected forty times since Tuesday.
The cost is nothing. A replication client sends a handful of commands and then streams for hours or days. The exception is a client stuck in a reconnect loop, which logs its handful every few seconds; that is not a cost, that is the diagnosis.
Two quirks. Every line arrives with a STATEMENT: line repeating it, because the message is emitted at LOG, LOG outranks the default log_min_error_statement of ERROR, and the walsender does not hide the statement the way log_statement does. Two lines per command; live with it, or see log_error_verbosity for the knob. And on a logical replication connection, ordinary SQL (which walsenders in that mode accept, and which a subscriber’s table-sync workers use) is logged by log_statement, not by this; since 10 the two no longer both log it. Physical walsenders, meanwhile, have no database, so %d in log_line_prefix prints [unknown] for them. That is how you will spot them.
Turn it on. The default is off because it arrived in 2015 and new logging parameters ship off; this one never earned that.