From 12e518a786f9532e3264976e3fe8abf638a5ffdf Mon Sep 17 00:00:00 2001 From: Gyan Sreejith Date: Tue, 17 Mar 2026 19:10:28 -0400 Subject: [PATCH v12 2/3] Add a new argument -l to pg_createsubscriber. Enabling the option to write messages to log files in the specified directory. A new directory is created if required. A subdirectory is created with timestamp as its name, and it will contain two new logfiles: 1. pg_createsubscriber_server.log - captures messages related to starting and stopping the standby server. 2. pg_createsubscriber_internal.log - captures internal diagnostic output from pg_createsubscriber. For example, if we specify -l abc as an argument, and if the timestamp on running it is 20260119T204317.204, a directory abc is created if it doesn't exist already, with 20260119T204317.204 as its subdirectory and it will contain the two log files pg_createsubscriber_server.log and pg_createsubscriber_internal.log --- doc/src/sgml/ref/pg_createsubscriber.sgml | 28 +++ src/bin/pg_basebackup/pg_createsubscriber.c | 190 +++++++++++++++++- .../t/040_pg_createsubscriber.pl | 41 +++- 3 files changed, 248 insertions(+), 11 deletions(-) diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml index cf45ff3573d..2898a5ea111 100644 --- a/doc/src/sgml/ref/pg_createsubscriber.sgml +++ b/doc/src/sgml/ref/pg_createsubscriber.sgml @@ -136,6 +136,34 @@ PostgreSQL documentation + + + + + + Specify the name of the log directory. A new directory is created with + this name if it does not exist. A subdirectory with a timestamp + indicating the time at which pg_createsubscriber was run will be created. + The following two log files will be created in the subdirectory with a + umask of 077 so that access is disallowed to other users by default. + + + + pg_createsubscriber_server.log which captures logs related to stopping + and starting the standby server, + + + + + pg_createsubscriber_internal.log which captures internal diagnostic + output (validations, checks, etc.) + + + + + + + diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c index 0770e163041..bd15b9c1015 100644 --- a/src/bin/pg_basebackup/pg_createsubscriber.c +++ b/src/bin/pg_basebackup/pg_createsubscriber.c @@ -55,10 +55,14 @@ #define INCLUDED_CONF_FILE "pg_createsubscriber.conf" #define INCLUDED_CONF_FILE_DISABLED INCLUDED_CONF_FILE ".disabled" +#define SERVER_LOG_FILE_NAME "pg_createsubscriber_server" +#define INTERNAL_LOG_FILE_NAME "pg_createsubscriber_internal" + /* Command-line options */ struct CreateSubscriberOptions { char *config_file; /* configuration file */ + char *log_dir; /* log directory name */ char *pub_conninfo_str; /* publisher connection string */ char *socket_dir; /* directory for Unix-domain socket, if any */ char *sub_port; /* subscriber port number */ @@ -155,8 +159,14 @@ static void get_publisher_databases(struct CreateSubscriberOptions *opt, static void pg_createsub_log(enum pg_log_level, enum pg_log_part, const char *pg_restrict fmt,...) pg_attribute_printf(3, 4); +static void pg_createsub_log_v(enum pg_log_level level, enum pg_log_part part, + const char *pg_restrict fmt, va_list args) + pg_attribute_printf(3, 0); pg_noreturn static void pg_fatal(const char *pg_restrict fmt,...) pg_attribute_printf(1, 2); +static void internal_log_file_write(enum pg_log_level level, + const char *pg_restrict fmt, va_list args) + pg_attribute_printf(2, 0); #define WAIT_INTERVAL 1 /* 1 second */ @@ -178,6 +188,10 @@ static pg_prng_state prng_state; static char *pg_ctl_path = NULL; static char *pg_resetwal_path = NULL; +static FILE *internal_log_file_fp = NULL; /* File ptr to log all messages to */ +static char *log_timestamp = NULL; /* Timestamp to be used in all log file + * names */ + /* standby / subscriber data directory */ static char *subscriber_dir = NULL; @@ -185,6 +199,30 @@ static bool recovery_ended = false; static bool standby_running = false; static bool recovery_params_set = false; +static void +pg_createsub_log_v(enum pg_log_level level, enum pg_log_part part, + const char *pg_restrict fmt, va_list args) +{ + if (internal_log_file_fp != NULL) + { + /* + * Output to both stderr and specified log files if the log level is + * warning or higher. + */ + if (level > PG_LOG_INFO) + { + va_list arg_cpy; + + va_copy(arg_cpy, args); + pg_log_generic_v(level, part, fmt, arg_cpy); + va_end(arg_cpy); + } + internal_log_file_write(level, fmt, args); + } + else + pg_log_generic_v(level, part, fmt, args); +} + static void pg_createsub_log(enum pg_log_level level, enum pg_log_part part, const char *pg_restrict fmt,...) @@ -193,7 +231,7 @@ pg_createsub_log(enum pg_log_level level, enum pg_log_part part, va_start(args, fmt); - pg_log_generic_v(level, part, fmt, args); + pg_createsub_log_v(level, part, fmt, args); va_end(args); } @@ -205,10 +243,11 @@ pg_fatal(const char *pg_restrict fmt,...) va_start(args, fmt); - pg_log_generic_v(PG_LOG_ERROR, PG_LOG_PRIMARY, fmt, args); + pg_createsub_log_v(PG_LOG_ERROR, PG_LOG_PRIMARY, fmt, args); va_end(args); + exit(1); } @@ -313,6 +352,12 @@ cleanup_objects_atexit(void) if (standby_running) stop_standby_server(subscriber_dir); + + if (internal_log_file_fp != NULL) + { + fclose(internal_log_file_fp); + internal_log_file_fp = NULL; + } } static void @@ -327,6 +372,7 @@ usage(void) " databases and databases that don't allow connections\n")); printf(_(" -d, --database=DBNAME database in which to create a subscription\n")); printf(_(" -D, --pgdata=DATADIR location for the subscriber data directory\n")); + printf(_(" -l, --logdir=LOGDIR location for the new log directory\n")); printf(_(" -n, --dry-run dry run, just show what would be done\n")); printf(_(" -p, --subscriber-port=PORT subscriber port number (default %s)\n"), DEFAULT_SUB_PORT); printf(_(" -P, --publisher-server=CONNSTR publisher connection string\n")); @@ -761,6 +807,7 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt) bool crc_ok; struct timeval tv; + char *out_file; char *cmd_str; pg_createsub_log(PG_LOG_INFO, PG_LOG_PRIMARY, @@ -799,8 +846,13 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt) pg_createsub_log(PG_LOG_INFO, PG_LOG_PRIMARY, "running pg_resetwal on the subscriber"); - cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path, - subscriber_dir, DEVNULL); + if (opt->log_dir != NULL) + out_file = psprintf("%s/%s/%s.log", opt->log_dir, log_timestamp, SERVER_LOG_FILE_NAME); + else + out_file = DEVNULL; + + cmd_str = psprintf("\"%s\" -D \"%s\" >> \"%s\"", pg_resetwal_path, + subscriber_dir, out_file); pg_createsub_log(PG_LOG_DEBUG, PG_LOG_PRIMARY, "pg_resetwal command is: %s", cmd_str); @@ -1023,6 +1075,110 @@ server_is_in_recovery(PGconn *conn) return ret == 0; } +static void +internal_log_file_write(enum pg_log_level level, const char *pg_restrict fmt, + va_list args) +{ + if (level < __pg_log_level) + return; + + if (internal_log_file_fp == NULL) + return; + + vfprintf(internal_log_file_fp, fmt, args); + + fprintf(internal_log_file_fp, "\n"); + fflush(internal_log_file_fp); +} + +/* + * Open a new logfile with proper permissions. + * From src/backend/postmaster/syslogger.c + */ +static FILE * +logfile_open(const char *filename, const char *mode) +{ + FILE *fh; + mode_t oumask; + + oumask = umask((mode_t) ((~(S_IRUSR | S_IWUSR)) & (S_IRWXU | S_IRWXG | S_IRWXO))); + fh = fopen(filename, mode); + umask(oumask); + + if (fh) + { + setvbuf(fh, NULL, PG_IOLBF, 0); + +#ifdef WIN32 + /* use CRLF line endings on Windows */ + _setmode(_fileno(fh), _O_TEXT); +#endif + } + else + pg_fatal("could not open log file \"%s\": %m", + filename); + + return fh; +} + +static void +make_dir(const char *dir) +{ + struct stat statbuf; + + if (stat(dir, &statbuf) == 0) + return; + + if (errno != ENOENT) + pg_fatal("could not stat directory \"%s\": %m", dir); + + if (mkdir(dir, S_IRWXU) == 0) + { + pg_log_info("directory %s created", dir); + return; + } + + pg_fatal("could not create log directory \"%s\": %m", dir); +} + +static void +make_output_dirs(const char *log_dir) +{ + char timestamp[128]; + struct timeval tval; + time_t now; + struct tm tmbuf; + char timestamp_dir[MAXPGPATH]; + int len; + + /* Generate timestamp */ + gettimeofday(&tval, NULL); + now = tval.tv_sec; + + strftime(timestamp, sizeof(timestamp), "%Y%m%dT%H%M%S", + localtime_r(&now, &tmbuf)); + + /* append milliseconds */ + snprintf(timestamp + strlen(timestamp), + sizeof(timestamp) - strlen(timestamp), ".%03u", + (unsigned int) (tval.tv_usec / 1000)); + + log_timestamp = pg_strdup(timestamp); + + /* Create base directory (ignore if exists) */ + make_dir(log_dir); + + /* Build timestamp directory path */ + len = snprintf(timestamp_dir, MAXPGPATH, "%s/%s", log_dir, timestamp); + + if (len >= MAXPGPATH) + pg_fatal("directory path for log files, %s/%s, is too long", + log_dir, timestamp); + + /* Create timestamp directory */ + make_dir(timestamp_dir); +} + /* * Is the primary server ready for logical replication? * @@ -1781,6 +1937,9 @@ start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_ if (restrict_logical_worker) appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c max_logical_replication_workers=0\""); + if (opt->log_dir != NULL) + appendPQExpBuffer(pg_ctl_cmd, " -l %s/%s/%s.log", opt->log_dir, log_timestamp, SERVER_LOG_FILE_NAME); + pg_createsub_log(PG_LOG_DEBUG, PG_LOG_PRIMARY, "pg_ctl command is: %s", pg_ctl_cmd->data); rc = system(pg_ctl_cmd->data); @@ -2351,6 +2510,7 @@ main(int argc, char **argv) {"all", no_argument, NULL, 'a'}, {"database", required_argument, NULL, 'd'}, {"pgdata", required_argument, NULL, 'D'}, + {"logdir", required_argument, NULL, 'l'}, {"dry-run", no_argument, NULL, 'n'}, {"subscriber-port", required_argument, NULL, 'p'}, {"publisher-server", required_argument, NULL, 'P'}, @@ -2409,6 +2569,7 @@ main(int argc, char **argv) /* Default settings */ subscriber_dir = NULL; opt.config_file = NULL; + opt.log_dir = NULL; opt.pub_conninfo_str = NULL; opt.socket_dir = NULL; opt.sub_port = DEFAULT_SUB_PORT; @@ -2439,7 +2600,7 @@ main(int argc, char **argv) get_restricted_token(); - while ((c = getopt_long(argc, argv, "ad:D:np:P:s:t:TU:v", + while ((c = getopt_long(argc, argv, "ad:D:l:np:P:s:t:TU:v", long_options, &option_index)) != -1) { switch (c) @@ -2460,6 +2621,10 @@ main(int argc, char **argv) subscriber_dir = pg_strdup(optarg); canonicalize_path(subscriber_dir); break; + case 'l': + opt.log_dir = pg_strdup(optarg); + canonicalize_path(opt.log_dir); + break; case 'n': dry_run = true; break; @@ -2530,6 +2695,18 @@ main(int argc, char **argv) } } + if (opt.log_dir != NULL) + { + char *internal_log_file; + + make_output_dirs(opt.log_dir); + internal_log_file = psprintf("%s/%s/%s.log", opt.log_dir, log_timestamp, + INTERNAL_LOG_FILE_NAME); + + if ((internal_log_file_fp = logfile_open(internal_log_file, "a")) == NULL) + pg_fatal("could not open log file \"%s\": %m", internal_log_file); + } + /* Validate that --all is not used with incompatible options */ if (opt.all_dbs) { @@ -2826,5 +3003,8 @@ main(int argc, char **argv) pg_createsub_log(PG_LOG_INFO, PG_LOG_PRIMARY, "Done!"); + if (internal_log_file_fp != NULL) + fclose(internal_log_file_fp); + return 0; } diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl index 0c27fca7bb7..4ddfb621a5d 100644 --- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl +++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl @@ -14,6 +14,7 @@ program_version_ok('pg_createsubscriber'); program_options_handling_ok('pg_createsubscriber'); my $datadir = PostgreSQL::Test::Utils::tempdir; +my $logdir = PostgreSQL::Test::Utils::tempdir + "/logdir"; # Generate a database with a name made of a range of ASCII characters. # Extracted from 002_pg_upgrade.pl. @@ -362,9 +363,35 @@ command_ok( '--subscription' => 'sub2', '--database' => $db1, '--database' => $db2, + '--logdir' => $logdir, ], 'run pg_createsubscriber --dry-run on node S'); +# Check that the log files were created +my @server_log_files = glob "$logdir/*/pg_createsubscriber_server.log"; +is( scalar(@server_log_files), 1, " + pg_createsubscriber_server.log file was created"); +my $server_log_file_size = -s $server_log_files[0]; +isnt($server_log_file_size, 0, + "pg_createsubscriber_server.log file not empty"); +my $server_log = slurp_file($server_log_files[0]); +like( + $server_log, + qr/consistent recovery state reached/, + "server reached consistent recovery state"); + +my @internal_log_files = glob "$logdir/*/pg_createsubscriber_internal.log"; +is( scalar(@internal_log_files), 1, " + pg_createsubscriber_internal.log file was created"); +my $internal_log_file_size = -s $internal_log_files[0]; +isnt($internal_log_file_size, 0, + "pg_createsubscriber_internal.log file not empty"); +my $internal_log = slurp_file($internal_log_files[0]); +like( + $internal_log, + qr/target server reached the consistent state/, + "log shows consistent state reached"); + # Check if node S is still a standby $node_s->start; is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'), @@ -444,7 +471,8 @@ is(scalar(() = $stderr =~ /would create subscription/g), # Create a user-defined publication, and a table that is not a member of that # publication. -$node_p->safe_psql($db1, qq( +$node_p->safe_psql( + $db1, qq( CREATE PUBLICATION test_pub3 FOR TABLE tbl1; CREATE TABLE not_replicated (a int); )); @@ -540,8 +568,7 @@ second row third row), "logical replication works in database $db1"); $result = $node_s->safe_psql($db1, 'SELECT * FROM not_replicated'); -is($result, qq(), - "table is not replicated in database $db1"); +is($result, qq(), "table is not replicated in database $db1"); # Check result in database $db2 $result = $node_s->safe_psql($db2, 'SELECT * FROM tbl2'); @@ -555,8 +582,10 @@ my $sysid_s = $node_s->safe_psql('postgres', isnt($sysid_p, $sysid_s, 'system identifier was changed'); # Verify that pub2 was created in $db2 -is($node_p->safe_psql($db2, "SELECT COUNT(*) FROM pg_publication WHERE pubname = 'pub2'"), - '1', "publication pub2 was created in $db2"); +is( $node_p->safe_psql( + $db2, "SELECT COUNT(*) FROM pg_publication WHERE pubname = 'pub2'"), + '1', + "publication pub2 was created in $db2"); # Get subscription and publication names $result = $node_s->safe_psql( @@ -581,7 +610,7 @@ $result = $node_s->safe_psql( ) ); -is($result, qq($db1|{test_pub3} +is( $result, qq($db1|{test_pub3} $db2|{pub2}), "subscriptions use the correct publications"); -- 2.47.3