On 05/02/2026 11:22, Álvaro Herrera wrote:
> My only comment at this point is that the proposed GUC name is not
> great.  I think it should be something like log_statement_max_length, or
> something like that.  Reading just the thread subject, people would
> imagine this is about the size of the log file.

Done. GUC changed to log_statement_max_length.

> Another point is that the current patch does strlen() twice on each
> query.  It might be better to do away with need_truncate_query_log() and
> have a single routine that both determines whether the truncation is
> needed and returns the truncated query if it is.  If it returns NULL
> then caller assumes it's not needed.

Done. Now truncate_query_log() returns a palloc'd truncated copy of the
statement if truncation is needed, and NULL otherwise.

== issue with issue with empty STATEMENT ==

This issue was mentioned by Fujii upthread. It is now fixed:

postgres=# SET log_statement TO 'all';
SET
postgres=# SET log_statement_max_length TO 3;
SET
postgres=# SELECT 1/0;
ERROR:  division by zero

Log entries:

2026-02-05 17:54:47.521 CET [570568] LOG:  statement: SEL
2026-02-05 17:54:47.521 CET [570568] ERROR:  division by zero
2026-02-05 17:54:47.521 CET [570568] STATEMENT:  SELECT 1/0;

== default value ==

To be consistent with other parameters, such as
log_parameter_max_length, the default (disabled) value is now -1.

== tests ==

Added new tests for -1 and multi-byte characters.


Best, Jim
From d12a565220d1827079c033507919ac5229004ce7 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Thu, 5 Feb 2026 18:00:13 +0100
Subject: [PATCH v7] Add log_statement_max_length GUC to limit logged statement
  size

When log_statement is enabled, queries can be arbitrarily long and may
consume significant disk space in server logs. This patch introduces a
new GUC, log_statement_max_length, which limits the maximum byte length
of logged statements.

A value greater than zero truncates each logged statement to the given
number of bytes. The default is -1, which disables truncation and logs
full statements. If specified without units, the value is interpreted
as bytes.
---
 doc/src/sgml/config.sgml                      | 18 ++++++++++
 src/backend/tcop/postgres.c                   | 11 ++++--
 src/backend/utils/error/elog.c                | 34 +++++++++++++++++++
 src/backend/utils/misc/guc_parameters.dat     | 10 ++++++
 src/backend/utils/misc/guc_tables.c           |  1 +
 src/backend/utils/misc/postgresql.conf.sample |  2 ++
 src/bin/pg_ctl/t/004_logrotate.pl             | 24 +++++++++++++
 src/include/utils/elog.h                      |  2 ++
 src/include/utils/guc.h                       |  1 +
 9 files changed, 101 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5560b95ee6..d9d708d7b9 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8235,6 +8235,24 @@ log_line_prefix = '%m [%p] %q%u@%d/%a '
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-log-statement-max-length" xreflabel="log_statement_max_length">
+      <term><varname>log_statement_max_length</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>log_statement_max_length</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        If greater than zero, each statement logged by <xref linkend="guc-log-statement"/>
+        is truncated to at most this many bytes.
+        <literal>-1</literal> (the default) disables truncation.
+        If this value is specified without units, it is taken as bytes.
+        Only superusers and users with the appropriate <literal>SET</literal>
+        privilege can change this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
      <sect2 id="runtime-config-logging-csvlog">
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 02e9aaa6bc..14265ecd08 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/pquery.h"
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
+#include "utils/elog.h"
 #include "utils/guc_hooks.h"
 #include "utils/injection_point.h"
 #include "utils/lsyscache.h"
@@ -1023,11 +1024,15 @@ exec_simple_query(const char *query_string)
 	bool		was_logged = false;
 	bool		use_implicit_block;
 	char		msec_str[32];
+	char	   *truncated_query = NULL;
+	const char *query_log;
 
 	/*
 	 * Report query to various monitoring facilities.
 	 */
 	debug_query_string = query_string;
+	truncated_query = truncate_query_log(query_string);
+	query_log = truncated_query ? truncated_query : query_string;
 
 	pgstat_report_activity(STATE_RUNNING, query_string);
 
@@ -1072,7 +1077,7 @@ exec_simple_query(const char *query_string)
 	if (check_log_statement(parsetree_list))
 	{
 		ereport(LOG,
-				(errmsg("statement: %s", query_string),
+				(errmsg("statement: %s", query_log),
 				 errhidestmt(true),
 				 errdetail_execute(parsetree_list)));
 		was_logged = true;
@@ -1370,7 +1375,7 @@ exec_simple_query(const char *query_string)
 		case 2:
 			ereport(LOG,
 					(errmsg("duration: %s ms  statement: %s",
-							msec_str, query_string),
+							msec_str, query_log),
 					 errhidestmt(true),
 					 errdetail_execute(parsetree_list)));
 			break;
@@ -1381,6 +1386,8 @@ exec_simple_query(const char *query_string)
 
 	TRACE_POSTGRESQL_QUERY_DONE(query_string);
 
+	if (truncated_query)
+		pfree(truncated_query);
 	debug_query_string = NULL;
 }
 
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index e6a4ef9905..ffabbfbceb 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3805,6 +3805,40 @@ write_stderr(const char *fmt,...)
 	va_end(ap);
 }
 
+/*
+ * truncate_query_log - truncate query string if needed for logging
+ *
+ * Returns a palloc'd truncated copy if truncation is needed,
+ * or NULL if no truncation is required.
+ */
+char *
+truncate_query_log(const char *query)
+{
+	size_t		query_len;
+	size_t		truncated_len;
+	char	   *truncated_query;
+
+	/* Check if truncation is disabled (-1) or no query string provided */
+	if (!query || log_statement_max_length < 0)
+		return NULL;
+
+	query_len = strlen(query);
+
+	/*
+	 * No need to allocate a truncated copy if the query is shorter
+	 * than log_statement_max_length.
+	 */
+	if (query_len <= (size_t) log_statement_max_length)
+		return NULL;
+
+	/* Truncate at multibyte character boundary */
+	truncated_len = pg_mbcliplen(query, query_len, log_statement_max_length);
+	truncated_query = (char *) palloc(truncated_len + 1);
+	memcpy(truncated_query, query, truncated_len);
+	truncated_query[truncated_len] = '\0';
+
+	return truncated_query;
+}
 
 /*
  * Write errors to stderr (or by equal means when stderr is
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index f0260e6e41..4864dbbbb2 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1777,6 +1777,16 @@
   options => 'log_statement_options',
 },
 
+{ name => 'log_statement_max_length', type => 'int', context => 'PGC_SUSET', group => 'LOGGING_WHAT',
+  short_desc => 'Sets the maximum length in bytes of logged statements.',
+  long_desc => '-1 means no truncation.',
+  flags => 'GUC_UNIT_BYTE',
+  variable => 'log_statement_max_length',
+  boot_val => '-1',
+  min => '-1',
+  max => 'INT_MAX / 2',
+},
+
 { name => 'log_statement_sample_rate', type => 'real', context => 'PGC_SUSET', group => 'LOGGING_WHEN',
   short_desc => 'Fraction of statements exceeding "log_min_duration_sample" to be logged.',
   long_desc => 'Use a value between 0.0 (never log) and 1.0 (always log).',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 13c569d879..dbcb25e130 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -544,6 +544,7 @@ int			log_min_duration_statement = -1;
 int			log_parameter_max_length = -1;
 int			log_parameter_max_length_on_error = 0;
 int			log_temp_files = -1;
+int			log_statement_max_length = -1;
 double		log_statement_sample_rate = 1.0;
 double		log_xact_sample_rate = 0;
 char	   *backtrace_functions;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c4f92fcdac..3cc4399026 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -642,6 +642,8 @@
                                         # bind-parameter values to N bytes;
                                         # -1 means print in full, 0 disables
 #log_statement = 'none'                 # none, ddl, mod, all
+#log_statement_max_length = -1          # max length of logged statements
+                                        # -1 disables truncation
 #log_replication_commands = off
 #log_temp_files = -1                    # log temporary files equal or larger
                                         # than the specified size in kilobytes;
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index 7b19f86467..dd1aa21f4c 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -135,6 +135,30 @@ check_log_pattern('stderr', $new_current_logfiles, 'syntax error', $node);
 check_log_pattern('csvlog', $new_current_logfiles, 'syntax error', $node);
 check_log_pattern('jsonlog', $new_current_logfiles, 'syntax error', $node);
 
+# Verify truncation works with ASCII
+$node->append_conf('postgresql.conf', "log_statement = 'all'\nlog_statement_max_length = 20\n");
+$node->reload();
+$node->psql('postgres', "SELECT '123456789ABCDEF'");
+check_log_pattern('stderr',  $new_current_logfiles, "SELECT '123456789ABC", $node);
+check_log_pattern('csvlog',  $new_current_logfiles, "SELECT '123456789ABC", $node);
+check_log_pattern('jsonlog', $new_current_logfiles, "SELECT '123456789ABC", $node);
+
+# Verify -1 disables truncation (logs full query)
+$node->append_conf('postgresql.conf', "log_statement_max_length = -1\n");
+$node->reload();
+$node->psql('postgres', "SELECT '123456789ABCDEF'");
+check_log_pattern('stderr',  $new_current_logfiles, "SELECT '123456789ABCDEF'", $node);
+check_log_pattern('csvlog',  $new_current_logfiles, "SELECT '123456789ABCDEF'", $node);
+check_log_pattern('jsonlog', $new_current_logfiles, "SELECT '123456789ABCDEF'", $node);
+
+# Verify multibyte character handling (must not produce invalid UTF-8)
+$node->append_conf('postgresql.conf', "log_statement_max_length = 12\n");
+$node->reload();
+$node->psql('postgres', "SELECT '🐘test'");
+check_log_pattern('stderr',  $new_current_logfiles, "SELECT '", $node);
+check_log_pattern('csvlog',  $new_current_logfiles, "SELECT '", $node);
+check_log_pattern('jsonlog', $new_current_logfiles, "SELECT '", $node);
+
 $node->stop();
 
 done_testing();
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index a12b379e09..aabee73aa4 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -494,6 +494,7 @@ extern PGDLLIMPORT int Log_destination;
 extern PGDLLIMPORT char *Log_destination_string;
 extern PGDLLIMPORT bool syslog_sequence_numbers;
 extern PGDLLIMPORT bool syslog_split_messages;
+extern PGDLLIMPORT int log_statement_max_length;
 
 /* Log destination bitmap */
 #define LOG_DESTINATION_STDERR	 1
@@ -508,6 +509,7 @@ extern void log_status_format(StringInfo buf, const char *format,
 extern void DebugFileOpen(void);
 extern char *unpack_sql_state(int sql_state);
 extern bool in_error_recursion_trouble(void);
+extern char *truncate_query_log(const char *query);
 
 /* Common functions shared across destinations */
 extern void reset_formatted_start_time(void);
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index bf39878c43..8043d8fa45 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -300,6 +300,7 @@ extern PGDLLIMPORT int client_min_messages;
 extern PGDLLIMPORT int log_min_duration_sample;
 extern PGDLLIMPORT int log_min_duration_statement;
 extern PGDLLIMPORT int log_temp_files;
+extern PGDLLIMPORT int log_statement_max_length;
 extern PGDLLIMPORT double log_statement_sample_rate;
 extern PGDLLIMPORT double log_xact_sample_rate;
 extern PGDLLIMPORT char *backtrace_functions;
-- 
2.43.0

Reply via email to