From e3e3b65fa0956c551bdca28227dd0eb342ae80d7 Mon Sep 17 00:00:00 2001 From: Jim Jones Date: Fri, 20 Mar 2026 11:44:37 +0100 Subject: [PATCH v8] 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 8cdd826fbd..4fd35c6cdd 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -8346,6 +8346,24 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' + + log_statement_max_length (integer) + + log_statement_max_length configuration parameter + + + + + If greater than zero, each statement logged by + is truncated to at most this many bytes. + -1 (the default) disables truncation. + If this value is specified without units, it is taken as bytes. + Only superusers and users with the appropriate SET + privilege can change this setting. + + + + diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index b356311321..06e9fa5063 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -74,6 +74,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" @@ -1024,11 +1025,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); @@ -1073,7 +1078,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; @@ -1371,7 +1376,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; @@ -1382,6 +1387,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 80b78f2526..92dd6c6eb5 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -4231,6 +4231,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 0c9854ad8f..5098bd0463 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -1793,6 +1793,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 1e14b7b4af..ecded52bc4 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -553,6 +553,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 e4abe6c007..371427abf7 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -664,6 +664,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 dc406d6651..8057d7870a 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