mirror of
https://github.com/postgres/postgres.git
synced 2026-04-15 22:10:45 -04:00
Referring to the WAL as just "log" invites confusion with the postmaster log, so avoid doing that in docs and error messages. Also shorten "WAL segment file" to just "WAL file" in various places. Bharath Rupireddy, reviewed by Nathan Bossart and Kyotaro Horiguchi Discussion: https://postgr.es/m/CALj2ACUeXa8tDPaiTLexBDMZ7hgvaN+RTb957-cn5qwv9zf-MQ@mail.gmail.com
1182 lines
30 KiB
C
1182 lines
30 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* pg_waldump.c - decode and display WAL
|
|
*
|
|
* Copyright (c) 2013-2022, PostgreSQL Global Development Group
|
|
*
|
|
* IDENTIFICATION
|
|
* src/bin/pg_waldump/pg_waldump.c
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
|
|
#define FRONTEND 1
|
|
#include "postgres.h"
|
|
|
|
#include <dirent.h>
|
|
#include <signal.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include "access/transam.h"
|
|
#include "access/xlog_internal.h"
|
|
#include "access/xlogreader.h"
|
|
#include "access/xlogrecord.h"
|
|
#include "access/xlogstats.h"
|
|
#include "common/fe_memutils.h"
|
|
#include "common/logging.h"
|
|
#include "getopt_long.h"
|
|
#include "rmgrdesc.h"
|
|
|
|
/*
|
|
* NOTE: For any code change or issue fix here, it is highly recommended to
|
|
* give a thought about doing the same in pg_walinspect contrib module as well.
|
|
*/
|
|
|
|
static const char *progname;
|
|
|
|
static int WalSegSz;
|
|
static volatile sig_atomic_t time_to_stop = false;
|
|
|
|
static const RelFileLocator emptyRelFileLocator = {0, 0, 0};
|
|
|
|
typedef struct XLogDumpPrivate
|
|
{
|
|
TimeLineID timeline;
|
|
XLogRecPtr startptr;
|
|
XLogRecPtr endptr;
|
|
bool endptr_reached;
|
|
} XLogDumpPrivate;
|
|
|
|
typedef struct XLogDumpConfig
|
|
{
|
|
/* display options */
|
|
bool quiet;
|
|
bool bkp_details;
|
|
int stop_after_records;
|
|
int already_displayed_records;
|
|
bool follow;
|
|
bool stats;
|
|
bool stats_per_record;
|
|
|
|
/* filter options */
|
|
bool filter_by_rmgr[RM_MAX_ID + 1];
|
|
bool filter_by_rmgr_enabled;
|
|
TransactionId filter_by_xid;
|
|
bool filter_by_xid_enabled;
|
|
RelFileLocator filter_by_relation;
|
|
bool filter_by_extended;
|
|
bool filter_by_relation_enabled;
|
|
BlockNumber filter_by_relation_block;
|
|
bool filter_by_relation_block_enabled;
|
|
ForkNumber filter_by_relation_forknum;
|
|
bool filter_by_fpw;
|
|
} XLogDumpConfig;
|
|
|
|
|
|
/*
|
|
* When sigint is called, just tell the system to exit at the next possible
|
|
* moment.
|
|
*/
|
|
#ifndef WIN32
|
|
|
|
static void
|
|
sigint_handler(SIGNAL_ARGS)
|
|
{
|
|
time_to_stop = true;
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
print_rmgr_list(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i <= RM_MAX_BUILTIN_ID; i++)
|
|
{
|
|
printf("%s\n", GetRmgrDesc(i)->rm_name);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Check whether directory exists and whether we can open it. Keep errno set so
|
|
* that the caller can report errors somewhat more accurately.
|
|
*/
|
|
static bool
|
|
verify_directory(const char *directory)
|
|
{
|
|
DIR *dir = opendir(directory);
|
|
|
|
if (dir == NULL)
|
|
return false;
|
|
closedir(dir);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Split a pathname as dirname(1) and basename(1) would.
|
|
*
|
|
* XXX this probably doesn't do very well on Windows. We probably need to
|
|
* apply canonicalize_path(), at the very least.
|
|
*/
|
|
static void
|
|
split_path(const char *path, char **dir, char **fname)
|
|
{
|
|
char *sep;
|
|
|
|
/* split filepath into directory & filename */
|
|
sep = strrchr(path, '/');
|
|
|
|
/* directory path */
|
|
if (sep != NULL)
|
|
{
|
|
*dir = pnstrdup(path, sep - path);
|
|
*fname = pg_strdup(sep + 1);
|
|
}
|
|
/* local directory */
|
|
else
|
|
{
|
|
*dir = NULL;
|
|
*fname = pg_strdup(path);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Open the file in the valid target directory.
|
|
*
|
|
* return a read only fd
|
|
*/
|
|
static int
|
|
open_file_in_directory(const char *directory, const char *fname)
|
|
{
|
|
int fd = -1;
|
|
char fpath[MAXPGPATH];
|
|
|
|
Assert(directory != NULL);
|
|
|
|
snprintf(fpath, MAXPGPATH, "%s/%s", directory, fname);
|
|
fd = open(fpath, O_RDONLY | PG_BINARY, 0);
|
|
|
|
if (fd < 0 && errno != ENOENT)
|
|
pg_fatal("could not open file \"%s\": %m", fname);
|
|
return fd;
|
|
}
|
|
|
|
/*
|
|
* Try to find fname in the given directory. Returns true if it is found,
|
|
* false otherwise. If fname is NULL, search the complete directory for any
|
|
* file with a valid WAL file name. If file is successfully opened, set the
|
|
* wal segment size.
|
|
*/
|
|
static bool
|
|
search_directory(const char *directory, const char *fname)
|
|
{
|
|
int fd = -1;
|
|
DIR *xldir;
|
|
|
|
/* open file if valid filename is provided */
|
|
if (fname != NULL)
|
|
fd = open_file_in_directory(directory, fname);
|
|
|
|
/*
|
|
* A valid file name is not passed, so search the complete directory. If
|
|
* we find any file whose name is a valid WAL file name then try to open
|
|
* it. If we cannot open it, bail out.
|
|
*/
|
|
else if ((xldir = opendir(directory)) != NULL)
|
|
{
|
|
struct dirent *xlde;
|
|
|
|
while ((xlde = readdir(xldir)) != NULL)
|
|
{
|
|
if (IsXLogFileName(xlde->d_name))
|
|
{
|
|
fd = open_file_in_directory(directory, xlde->d_name);
|
|
fname = pg_strdup(xlde->d_name);
|
|
break;
|
|
}
|
|
}
|
|
|
|
closedir(xldir);
|
|
}
|
|
|
|
/* set WalSegSz if file is successfully opened */
|
|
if (fd >= 0)
|
|
{
|
|
PGAlignedXLogBlock buf;
|
|
int r;
|
|
|
|
r = read(fd, buf.data, XLOG_BLCKSZ);
|
|
if (r == XLOG_BLCKSZ)
|
|
{
|
|
XLogLongPageHeader longhdr = (XLogLongPageHeader) buf.data;
|
|
|
|
WalSegSz = longhdr->xlp_seg_size;
|
|
|
|
if (!IsValidWalSegSize(WalSegSz))
|
|
pg_fatal(ngettext("WAL segment size must be a power of two between 1 MB and 1 GB, but the WAL file \"%s\" header specifies %d byte",
|
|
"WAL segment size must be a power of two between 1 MB and 1 GB, but the WAL file \"%s\" header specifies %d bytes",
|
|
WalSegSz),
|
|
fname, WalSegSz);
|
|
}
|
|
else if (r < 0)
|
|
pg_fatal("could not read file \"%s\": %m",
|
|
fname);
|
|
else
|
|
pg_fatal("could not read file \"%s\": read %d of %d",
|
|
fname, r, XLOG_BLCKSZ);
|
|
close(fd);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Identify the target directory.
|
|
*
|
|
* Try to find the file in several places:
|
|
* if directory != NULL:
|
|
* directory /
|
|
* directory / XLOGDIR /
|
|
* else
|
|
* .
|
|
* XLOGDIR /
|
|
* $PGDATA / XLOGDIR /
|
|
*
|
|
* The valid target directory is returned.
|
|
*/
|
|
static char *
|
|
identify_target_directory(char *directory, char *fname)
|
|
{
|
|
char fpath[MAXPGPATH];
|
|
|
|
if (directory != NULL)
|
|
{
|
|
if (search_directory(directory, fname))
|
|
return pg_strdup(directory);
|
|
|
|
/* directory / XLOGDIR */
|
|
snprintf(fpath, MAXPGPATH, "%s/%s", directory, XLOGDIR);
|
|
if (search_directory(fpath, fname))
|
|
return pg_strdup(fpath);
|
|
}
|
|
else
|
|
{
|
|
const char *datadir;
|
|
|
|
/* current directory */
|
|
if (search_directory(".", fname))
|
|
return pg_strdup(".");
|
|
/* XLOGDIR */
|
|
if (search_directory(XLOGDIR, fname))
|
|
return pg_strdup(XLOGDIR);
|
|
|
|
datadir = getenv("PGDATA");
|
|
/* $PGDATA / XLOGDIR */
|
|
if (datadir != NULL)
|
|
{
|
|
snprintf(fpath, MAXPGPATH, "%s/%s", datadir, XLOGDIR);
|
|
if (search_directory(fpath, fname))
|
|
return pg_strdup(fpath);
|
|
}
|
|
}
|
|
|
|
/* could not locate WAL file */
|
|
if (fname)
|
|
pg_fatal("could not locate WAL file \"%s\"", fname);
|
|
else
|
|
pg_fatal("could not find any WAL file");
|
|
|
|
return NULL; /* not reached */
|
|
}
|
|
|
|
/* pg_waldump's XLogReaderRoutine->segment_open callback */
|
|
static void
|
|
WALDumpOpenSegment(XLogReaderState *state, XLogSegNo nextSegNo,
|
|
TimeLineID *tli_p)
|
|
{
|
|
TimeLineID tli = *tli_p;
|
|
char fname[MAXPGPATH];
|
|
int tries;
|
|
|
|
XLogFileName(fname, tli, nextSegNo, state->segcxt.ws_segsize);
|
|
|
|
/*
|
|
* In follow mode there is a short period of time after the server has
|
|
* written the end of the previous file before the new file is available.
|
|
* So we loop for 5 seconds looking for the file to appear before giving
|
|
* up.
|
|
*/
|
|
for (tries = 0; tries < 10; tries++)
|
|
{
|
|
state->seg.ws_file = open_file_in_directory(state->segcxt.ws_dir, fname);
|
|
if (state->seg.ws_file >= 0)
|
|
return;
|
|
if (errno == ENOENT)
|
|
{
|
|
int save_errno = errno;
|
|
|
|
/* File not there yet, try again */
|
|
pg_usleep(500 * 1000);
|
|
|
|
errno = save_errno;
|
|
continue;
|
|
}
|
|
/* Any other error, fall through and fail */
|
|
break;
|
|
}
|
|
|
|
pg_fatal("could not find file \"%s\": %m", fname);
|
|
}
|
|
|
|
/*
|
|
* pg_waldump's XLogReaderRoutine->segment_close callback. Same as
|
|
* wal_segment_close
|
|
*/
|
|
static void
|
|
WALDumpCloseSegment(XLogReaderState *state)
|
|
{
|
|
close(state->seg.ws_file);
|
|
/* need to check errno? */
|
|
state->seg.ws_file = -1;
|
|
}
|
|
|
|
/* pg_waldump's XLogReaderRoutine->page_read callback */
|
|
static int
|
|
WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
|
|
XLogRecPtr targetPtr, char *readBuff)
|
|
{
|
|
XLogDumpPrivate *private = state->private_data;
|
|
int count = XLOG_BLCKSZ;
|
|
WALReadError errinfo;
|
|
|
|
if (private->endptr != InvalidXLogRecPtr)
|
|
{
|
|
if (targetPagePtr + XLOG_BLCKSZ <= private->endptr)
|
|
count = XLOG_BLCKSZ;
|
|
else if (targetPagePtr + reqLen <= private->endptr)
|
|
count = private->endptr - targetPagePtr;
|
|
else
|
|
{
|
|
private->endptr_reached = true;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!WALRead(state, readBuff, targetPagePtr, count, private->timeline,
|
|
&errinfo))
|
|
{
|
|
WALOpenSegment *seg = &errinfo.wre_seg;
|
|
char fname[MAXPGPATH];
|
|
|
|
XLogFileName(fname, seg->ws_tli, seg->ws_segno,
|
|
state->segcxt.ws_segsize);
|
|
|
|
if (errinfo.wre_errno != 0)
|
|
{
|
|
errno = errinfo.wre_errno;
|
|
pg_fatal("could not read from file %s, offset %d: %m",
|
|
fname, errinfo.wre_off);
|
|
}
|
|
else
|
|
pg_fatal("could not read from file %s, offset %d: read %d of %d",
|
|
fname, errinfo.wre_off, errinfo.wre_read,
|
|
errinfo.wre_req);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/*
|
|
* Boolean to return whether the given WAL record matches a specific relation
|
|
* and optionally block.
|
|
*/
|
|
static bool
|
|
XLogRecordMatchesRelationBlock(XLogReaderState *record,
|
|
RelFileLocator matchRlocator,
|
|
BlockNumber matchBlock,
|
|
ForkNumber matchFork)
|
|
{
|
|
int block_id;
|
|
|
|
for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++)
|
|
{
|
|
RelFileLocator rlocator;
|
|
ForkNumber forknum;
|
|
BlockNumber blk;
|
|
|
|
if (!XLogRecGetBlockTagExtended(record, block_id,
|
|
&rlocator, &forknum, &blk, NULL))
|
|
continue;
|
|
|
|
if ((matchFork == InvalidForkNumber || matchFork == forknum) &&
|
|
(RelFileLocatorEquals(matchRlocator, emptyRelFileLocator) ||
|
|
RelFileLocatorEquals(matchRlocator, rlocator)) &&
|
|
(matchBlock == InvalidBlockNumber || matchBlock == blk))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Boolean to return whether the given WAL record contains a full page write.
|
|
*/
|
|
static bool
|
|
XLogRecordHasFPW(XLogReaderState *record)
|
|
{
|
|
int block_id;
|
|
|
|
for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++)
|
|
{
|
|
if (!XLogRecHasBlockRef(record, block_id))
|
|
continue;
|
|
|
|
if (XLogRecHasBlockImage(record, block_id))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Print a record to stdout
|
|
*/
|
|
static void
|
|
XLogDumpDisplayRecord(XLogDumpConfig *config, XLogReaderState *record)
|
|
{
|
|
const char *id;
|
|
const RmgrDescData *desc = GetRmgrDesc(XLogRecGetRmid(record));
|
|
uint32 rec_len;
|
|
uint32 fpi_len;
|
|
uint8 info = XLogRecGetInfo(record);
|
|
XLogRecPtr xl_prev = XLogRecGetPrev(record);
|
|
StringInfoData s;
|
|
|
|
XLogRecGetLen(record, &rec_len, &fpi_len);
|
|
|
|
printf("rmgr: %-11s len (rec/tot): %6u/%6u, tx: %10u, lsn: %X/%08X, prev %X/%08X, ",
|
|
desc->rm_name,
|
|
rec_len, XLogRecGetTotalLen(record),
|
|
XLogRecGetXid(record),
|
|
LSN_FORMAT_ARGS(record->ReadRecPtr),
|
|
LSN_FORMAT_ARGS(xl_prev));
|
|
|
|
id = desc->rm_identify(info);
|
|
if (id == NULL)
|
|
printf("desc: UNKNOWN (%x) ", info & ~XLR_INFO_MASK);
|
|
else
|
|
printf("desc: %s ", id);
|
|
|
|
initStringInfo(&s);
|
|
desc->rm_desc(&s, record);
|
|
printf("%s", s.data);
|
|
|
|
resetStringInfo(&s);
|
|
XLogRecGetBlockRefInfo(record, true, config->bkp_details, &s, NULL);
|
|
printf("%s", s.data);
|
|
pfree(s.data);
|
|
}
|
|
|
|
/*
|
|
* Display a single row of record counts and sizes for an rmgr or record.
|
|
*/
|
|
static void
|
|
XLogDumpStatsRow(const char *name,
|
|
uint64 n, uint64 total_count,
|
|
uint64 rec_len, uint64 total_rec_len,
|
|
uint64 fpi_len, uint64 total_fpi_len,
|
|
uint64 tot_len, uint64 total_len)
|
|
{
|
|
double n_pct,
|
|
rec_len_pct,
|
|
fpi_len_pct,
|
|
tot_len_pct;
|
|
|
|
n_pct = 0;
|
|
if (total_count != 0)
|
|
n_pct = 100 * (double) n / total_count;
|
|
|
|
rec_len_pct = 0;
|
|
if (total_rec_len != 0)
|
|
rec_len_pct = 100 * (double) rec_len / total_rec_len;
|
|
|
|
fpi_len_pct = 0;
|
|
if (total_fpi_len != 0)
|
|
fpi_len_pct = 100 * (double) fpi_len / total_fpi_len;
|
|
|
|
tot_len_pct = 0;
|
|
if (total_len != 0)
|
|
tot_len_pct = 100 * (double) tot_len / total_len;
|
|
|
|
printf("%-27s "
|
|
"%20" INT64_MODIFIER "u (%6.02f) "
|
|
"%20" INT64_MODIFIER "u (%6.02f) "
|
|
"%20" INT64_MODIFIER "u (%6.02f) "
|
|
"%20" INT64_MODIFIER "u (%6.02f)\n",
|
|
name, n, n_pct, rec_len, rec_len_pct, fpi_len, fpi_len_pct,
|
|
tot_len, tot_len_pct);
|
|
}
|
|
|
|
|
|
/*
|
|
* Display summary statistics about the records seen so far.
|
|
*/
|
|
static void
|
|
XLogDumpDisplayStats(XLogDumpConfig *config, XLogStats *stats)
|
|
{
|
|
int ri,
|
|
rj;
|
|
uint64 total_count = 0;
|
|
uint64 total_rec_len = 0;
|
|
uint64 total_fpi_len = 0;
|
|
uint64 total_len = 0;
|
|
double rec_len_pct,
|
|
fpi_len_pct;
|
|
|
|
/*
|
|
* Leave if no stats have been computed yet, as tracked by the end LSN.
|
|
*/
|
|
if (XLogRecPtrIsInvalid(stats->endptr))
|
|
return;
|
|
|
|
/*
|
|
* Each row shows its percentages of the total, so make a first pass to
|
|
* calculate column totals.
|
|
*/
|
|
|
|
for (ri = 0; ri <= RM_MAX_ID; ri++)
|
|
{
|
|
if (!RmgrIdIsValid(ri))
|
|
continue;
|
|
|
|
total_count += stats->rmgr_stats[ri].count;
|
|
total_rec_len += stats->rmgr_stats[ri].rec_len;
|
|
total_fpi_len += stats->rmgr_stats[ri].fpi_len;
|
|
}
|
|
total_len = total_rec_len + total_fpi_len;
|
|
|
|
printf("WAL statistics between %X/%X and %X/%X:\n",
|
|
LSN_FORMAT_ARGS(stats->startptr), LSN_FORMAT_ARGS(stats->endptr));
|
|
|
|
/*
|
|
* 27 is strlen("Transaction/COMMIT_PREPARED"), 20 is strlen(2^64), 8 is
|
|
* strlen("(100.00%)")
|
|
*/
|
|
|
|
printf("%-27s %20s %8s %20s %8s %20s %8s %20s %8s\n"
|
|
"%-27s %20s %8s %20s %8s %20s %8s %20s %8s\n",
|
|
"Type", "N", "(%)", "Record size", "(%)", "FPI size", "(%)", "Combined size", "(%)",
|
|
"----", "-", "---", "-----------", "---", "--------", "---", "-------------", "---");
|
|
|
|
for (ri = 0; ri <= RM_MAX_ID; ri++)
|
|
{
|
|
uint64 count,
|
|
rec_len,
|
|
fpi_len,
|
|
tot_len;
|
|
const RmgrDescData *desc;
|
|
|
|
if (!RmgrIdIsValid(ri))
|
|
continue;
|
|
|
|
desc = GetRmgrDesc(ri);
|
|
|
|
if (!config->stats_per_record)
|
|
{
|
|
count = stats->rmgr_stats[ri].count;
|
|
rec_len = stats->rmgr_stats[ri].rec_len;
|
|
fpi_len = stats->rmgr_stats[ri].fpi_len;
|
|
tot_len = rec_len + fpi_len;
|
|
|
|
if (RmgrIdIsCustom(ri) && count == 0)
|
|
continue;
|
|
|
|
XLogDumpStatsRow(desc->rm_name,
|
|
count, total_count, rec_len, total_rec_len,
|
|
fpi_len, total_fpi_len, tot_len, total_len);
|
|
}
|
|
else
|
|
{
|
|
for (rj = 0; rj < MAX_XLINFO_TYPES; rj++)
|
|
{
|
|
const char *id;
|
|
|
|
count = stats->record_stats[ri][rj].count;
|
|
rec_len = stats->record_stats[ri][rj].rec_len;
|
|
fpi_len = stats->record_stats[ri][rj].fpi_len;
|
|
tot_len = rec_len + fpi_len;
|
|
|
|
/* Skip undefined combinations and ones that didn't occur */
|
|
if (count == 0)
|
|
continue;
|
|
|
|
/* the upper four bits in xl_info are the rmgr's */
|
|
id = desc->rm_identify(rj << 4);
|
|
if (id == NULL)
|
|
id = psprintf("UNKNOWN (%x)", rj << 4);
|
|
|
|
XLogDumpStatsRow(psprintf("%s/%s", desc->rm_name, id),
|
|
count, total_count, rec_len, total_rec_len,
|
|
fpi_len, total_fpi_len, tot_len, total_len);
|
|
}
|
|
}
|
|
}
|
|
|
|
printf("%-27s %20s %8s %20s %8s %20s %8s %20s\n",
|
|
"", "--------", "", "--------", "", "--------", "", "--------");
|
|
|
|
/*
|
|
* The percentages in earlier rows were calculated against the column
|
|
* total, but the ones that follow are against the row total. Note that
|
|
* these are displayed with a % symbol to differentiate them from the
|
|
* earlier ones, and are thus up to 9 characters long.
|
|
*/
|
|
|
|
rec_len_pct = 0;
|
|
if (total_len != 0)
|
|
rec_len_pct = 100 * (double) total_rec_len / total_len;
|
|
|
|
fpi_len_pct = 0;
|
|
if (total_len != 0)
|
|
fpi_len_pct = 100 * (double) total_fpi_len / total_len;
|
|
|
|
printf("%-27s "
|
|
"%20" INT64_MODIFIER "u %-9s"
|
|
"%20" INT64_MODIFIER "u %-9s"
|
|
"%20" INT64_MODIFIER "u %-9s"
|
|
"%20" INT64_MODIFIER "u %-6s\n",
|
|
"Total", stats->count, "",
|
|
total_rec_len, psprintf("[%.02f%%]", rec_len_pct),
|
|
total_fpi_len, psprintf("[%.02f%%]", fpi_len_pct),
|
|
total_len, "[100%]");
|
|
}
|
|
|
|
static void
|
|
usage(void)
|
|
{
|
|
printf(_("%s decodes and displays PostgreSQL write-ahead logs for debugging.\n\n"),
|
|
progname);
|
|
printf(_("Usage:\n"));
|
|
printf(_(" %s [OPTION]... [STARTSEG [ENDSEG]]\n"), progname);
|
|
printf(_("\nOptions:\n"));
|
|
printf(_(" -b, --bkp-details output detailed information about backup blocks\n"));
|
|
printf(_(" -B, --block=N with --relation, only show records that modify block N\n"));
|
|
printf(_(" -e, --end=RECPTR stop reading at WAL location RECPTR\n"));
|
|
printf(_(" -f, --follow keep retrying after reaching end of WAL\n"));
|
|
printf(_(" -F, --fork=FORK only show records that modify blocks in fork FORK;\n"
|
|
" valid names are main, fsm, vm, init\n"));
|
|
printf(_(" -n, --limit=N number of records to display\n"));
|
|
printf(_(" -p, --path=PATH directory in which to find WAL segment files or a\n"
|
|
" directory with a ./pg_wal that contains such files\n"
|
|
" (default: current directory, ./pg_wal, $PGDATA/pg_wal)\n"));
|
|
printf(_(" -q, --quiet do not print any output, except for errors\n"));
|
|
printf(_(" -r, --rmgr=RMGR only show records generated by resource manager RMGR;\n"
|
|
" use --rmgr=list to list valid resource manager names\n"));
|
|
printf(_(" -R, --relation=T/D/R only show records that modify blocks in relation T/D/R\n"));
|
|
printf(_(" -s, --start=RECPTR start reading at WAL location RECPTR\n"));
|
|
printf(_(" -t, --timeline=TLI timeline from which to read WAL records\n"
|
|
" (default: 1 or the value used in STARTSEG)\n"));
|
|
printf(_(" -V, --version output version information, then exit\n"));
|
|
printf(_(" -w, --fullpage only show records with a full page write\n"));
|
|
printf(_(" -x, --xid=XID only show records with transaction ID XID\n"));
|
|
printf(_(" -z, --stats[=record] show statistics instead of records\n"
|
|
" (optionally, show per-record statistics)\n"));
|
|
printf(_(" -?, --help show this help, then exit\n"));
|
|
printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
|
|
printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
uint32 xlogid;
|
|
uint32 xrecoff;
|
|
XLogReaderState *xlogreader_state;
|
|
XLogDumpPrivate private;
|
|
XLogDumpConfig config;
|
|
XLogStats stats;
|
|
XLogRecord *record;
|
|
XLogRecPtr first_record;
|
|
char *waldir = NULL;
|
|
char *errormsg;
|
|
|
|
static struct option long_options[] = {
|
|
{"bkp-details", no_argument, NULL, 'b'},
|
|
{"block", required_argument, NULL, 'B'},
|
|
{"end", required_argument, NULL, 'e'},
|
|
{"follow", no_argument, NULL, 'f'},
|
|
{"fork", required_argument, NULL, 'F'},
|
|
{"fullpage", no_argument, NULL, 'w'},
|
|
{"help", no_argument, NULL, '?'},
|
|
{"limit", required_argument, NULL, 'n'},
|
|
{"path", required_argument, NULL, 'p'},
|
|
{"quiet", no_argument, NULL, 'q'},
|
|
{"relation", required_argument, NULL, 'R'},
|
|
{"rmgr", required_argument, NULL, 'r'},
|
|
{"start", required_argument, NULL, 's'},
|
|
{"timeline", required_argument, NULL, 't'},
|
|
{"xid", required_argument, NULL, 'x'},
|
|
{"version", no_argument, NULL, 'V'},
|
|
{"stats", optional_argument, NULL, 'z'},
|
|
{NULL, 0, NULL, 0}
|
|
};
|
|
|
|
int option;
|
|
int optindex = 0;
|
|
|
|
#ifndef WIN32
|
|
pqsignal(SIGINT, sigint_handler);
|
|
#endif
|
|
|
|
pg_logging_init(argv[0]);
|
|
set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_waldump"));
|
|
progname = get_progname(argv[0]);
|
|
|
|
if (argc > 1)
|
|
{
|
|
if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
|
|
{
|
|
usage();
|
|
exit(0);
|
|
}
|
|
if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0)
|
|
{
|
|
puts("pg_waldump (PostgreSQL) " PG_VERSION);
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
memset(&private, 0, sizeof(XLogDumpPrivate));
|
|
memset(&config, 0, sizeof(XLogDumpConfig));
|
|
memset(&stats, 0, sizeof(XLogStats));
|
|
|
|
private.timeline = 1;
|
|
private.startptr = InvalidXLogRecPtr;
|
|
private.endptr = InvalidXLogRecPtr;
|
|
private.endptr_reached = false;
|
|
|
|
config.quiet = false;
|
|
config.bkp_details = false;
|
|
config.stop_after_records = -1;
|
|
config.already_displayed_records = 0;
|
|
config.follow = false;
|
|
/* filter_by_rmgr array was zeroed by memset above */
|
|
config.filter_by_rmgr_enabled = false;
|
|
config.filter_by_xid = InvalidTransactionId;
|
|
config.filter_by_xid_enabled = false;
|
|
config.filter_by_extended = false;
|
|
config.filter_by_relation_enabled = false;
|
|
config.filter_by_relation_block_enabled = false;
|
|
config.filter_by_relation_forknum = InvalidForkNumber;
|
|
config.filter_by_fpw = false;
|
|
config.stats = false;
|
|
config.stats_per_record = false;
|
|
|
|
stats.startptr = InvalidXLogRecPtr;
|
|
stats.endptr = InvalidXLogRecPtr;
|
|
|
|
if (argc <= 1)
|
|
{
|
|
pg_log_error("no arguments specified");
|
|
goto bad_argument;
|
|
}
|
|
|
|
while ((option = getopt_long(argc, argv, "bB:e:fF:n:p:qr:R:s:t:wx:z",
|
|
long_options, &optindex)) != -1)
|
|
{
|
|
switch (option)
|
|
{
|
|
case 'b':
|
|
config.bkp_details = true;
|
|
break;
|
|
case 'B':
|
|
if (sscanf(optarg, "%u", &config.filter_by_relation_block) != 1 ||
|
|
!BlockNumberIsValid(config.filter_by_relation_block))
|
|
{
|
|
pg_log_error("invalid block number: \"%s\"", optarg);
|
|
goto bad_argument;
|
|
}
|
|
config.filter_by_relation_block_enabled = true;
|
|
config.filter_by_extended = true;
|
|
break;
|
|
case 'e':
|
|
if (sscanf(optarg, "%X/%X", &xlogid, &xrecoff) != 2)
|
|
{
|
|
pg_log_error("invalid WAL location: \"%s\"",
|
|
optarg);
|
|
goto bad_argument;
|
|
}
|
|
private.endptr = (uint64) xlogid << 32 | xrecoff;
|
|
break;
|
|
case 'f':
|
|
config.follow = true;
|
|
break;
|
|
case 'F':
|
|
config.filter_by_relation_forknum = forkname_to_number(optarg);
|
|
if (config.filter_by_relation_forknum == InvalidForkNumber)
|
|
{
|
|
pg_log_error("invalid fork name: \"%s\"", optarg);
|
|
goto bad_argument;
|
|
}
|
|
config.filter_by_extended = true;
|
|
break;
|
|
case 'n':
|
|
if (sscanf(optarg, "%d", &config.stop_after_records) != 1)
|
|
{
|
|
pg_log_error("invalid value \"%s\" for option %s", optarg, "-n/--limit");
|
|
goto bad_argument;
|
|
}
|
|
break;
|
|
case 'p':
|
|
waldir = pg_strdup(optarg);
|
|
break;
|
|
case 'q':
|
|
config.quiet = true;
|
|
break;
|
|
case 'r':
|
|
{
|
|
int rmid;
|
|
|
|
if (pg_strcasecmp(optarg, "list") == 0)
|
|
{
|
|
print_rmgr_list();
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
/*
|
|
* First look for the generated name of a custom rmgr, of
|
|
* the form "custom###". We accept this form, because the
|
|
* custom rmgr module is not loaded, so there's no way to
|
|
* know the real name. This convention should be
|
|
* consistent with that in rmgrdesc.c.
|
|
*/
|
|
if (sscanf(optarg, "custom%03d", &rmid) == 1)
|
|
{
|
|
if (!RmgrIdIsCustom(rmid))
|
|
{
|
|
pg_log_error("custom resource manager \"%s\" does not exist",
|
|
optarg);
|
|
goto bad_argument;
|
|
}
|
|
config.filter_by_rmgr[rmid] = true;
|
|
config.filter_by_rmgr_enabled = true;
|
|
}
|
|
else
|
|
{
|
|
/* then look for builtin rmgrs */
|
|
for (rmid = 0; rmid <= RM_MAX_BUILTIN_ID; rmid++)
|
|
{
|
|
if (pg_strcasecmp(optarg, GetRmgrDesc(rmid)->rm_name) == 0)
|
|
{
|
|
config.filter_by_rmgr[rmid] = true;
|
|
config.filter_by_rmgr_enabled = true;
|
|
break;
|
|
}
|
|
}
|
|
if (rmid > RM_MAX_BUILTIN_ID)
|
|
{
|
|
pg_log_error("resource manager \"%s\" does not exist",
|
|
optarg);
|
|
goto bad_argument;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'R':
|
|
if (sscanf(optarg, "%u/%u/%u",
|
|
&config.filter_by_relation.spcOid,
|
|
&config.filter_by_relation.dbOid,
|
|
&config.filter_by_relation.relNumber) != 3 ||
|
|
!OidIsValid(config.filter_by_relation.spcOid) ||
|
|
!RelFileNumberIsValid(config.filter_by_relation.relNumber))
|
|
{
|
|
pg_log_error("invalid relation specification: \"%s\"", optarg);
|
|
pg_log_error_detail("Expecting \"tablespace OID/database OID/relation filenode\".");
|
|
goto bad_argument;
|
|
}
|
|
config.filter_by_relation_enabled = true;
|
|
config.filter_by_extended = true;
|
|
break;
|
|
case 's':
|
|
if (sscanf(optarg, "%X/%X", &xlogid, &xrecoff) != 2)
|
|
{
|
|
pg_log_error("invalid WAL location: \"%s\"",
|
|
optarg);
|
|
goto bad_argument;
|
|
}
|
|
else
|
|
private.startptr = (uint64) xlogid << 32 | xrecoff;
|
|
break;
|
|
case 't':
|
|
if (sscanf(optarg, "%u", &private.timeline) != 1)
|
|
{
|
|
pg_log_error("invalid timeline specification: \"%s\"", optarg);
|
|
goto bad_argument;
|
|
}
|
|
break;
|
|
case 'w':
|
|
config.filter_by_fpw = true;
|
|
break;
|
|
case 'x':
|
|
if (sscanf(optarg, "%u", &config.filter_by_xid) != 1)
|
|
{
|
|
pg_log_error("invalid transaction ID specification: \"%s\"",
|
|
optarg);
|
|
goto bad_argument;
|
|
}
|
|
config.filter_by_xid_enabled = true;
|
|
break;
|
|
case 'z':
|
|
config.stats = true;
|
|
config.stats_per_record = false;
|
|
if (optarg)
|
|
{
|
|
if (strcmp(optarg, "record") == 0)
|
|
config.stats_per_record = true;
|
|
else if (strcmp(optarg, "rmgr") != 0)
|
|
{
|
|
pg_log_error("unrecognized value for option %s: %s",
|
|
"--stats", optarg);
|
|
goto bad_argument;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
goto bad_argument;
|
|
}
|
|
}
|
|
|
|
if (config.filter_by_relation_block_enabled &&
|
|
!config.filter_by_relation_enabled)
|
|
{
|
|
pg_log_error("option %s requires option %s to be specified",
|
|
"-B/--block", "-R/--relation");
|
|
goto bad_argument;
|
|
}
|
|
|
|
if ((optind + 2) < argc)
|
|
{
|
|
pg_log_error("too many command-line arguments (first is \"%s\")",
|
|
argv[optind + 2]);
|
|
goto bad_argument;
|
|
}
|
|
|
|
if (waldir != NULL)
|
|
{
|
|
/* validate path points to directory */
|
|
if (!verify_directory(waldir))
|
|
{
|
|
pg_log_error("could not open directory \"%s\": %m", waldir);
|
|
goto bad_argument;
|
|
}
|
|
}
|
|
|
|
/* parse files as start/end boundaries, extract path if not specified */
|
|
if (optind < argc)
|
|
{
|
|
char *directory = NULL;
|
|
char *fname = NULL;
|
|
int fd;
|
|
XLogSegNo segno;
|
|
|
|
split_path(argv[optind], &directory, &fname);
|
|
|
|
if (waldir == NULL && directory != NULL)
|
|
{
|
|
waldir = directory;
|
|
|
|
if (!verify_directory(waldir))
|
|
pg_fatal("could not open directory \"%s\": %m", waldir);
|
|
}
|
|
|
|
waldir = identify_target_directory(waldir, fname);
|
|
fd = open_file_in_directory(waldir, fname);
|
|
if (fd < 0)
|
|
pg_fatal("could not open file \"%s\"", fname);
|
|
close(fd);
|
|
|
|
/* parse position from file */
|
|
XLogFromFileName(fname, &private.timeline, &segno, WalSegSz);
|
|
|
|
if (XLogRecPtrIsInvalid(private.startptr))
|
|
XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
|
|
else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
|
|
{
|
|
pg_log_error("start WAL location %X/%X is not inside file \"%s\"",
|
|
LSN_FORMAT_ARGS(private.startptr),
|
|
fname);
|
|
goto bad_argument;
|
|
}
|
|
|
|
/* no second file specified, set end position */
|
|
if (!(optind + 1 < argc) && XLogRecPtrIsInvalid(private.endptr))
|
|
XLogSegNoOffsetToRecPtr(segno + 1, 0, WalSegSz, private.endptr);
|
|
|
|
/* parse ENDSEG if passed */
|
|
if (optind + 1 < argc)
|
|
{
|
|
XLogSegNo endsegno;
|
|
|
|
/* ignore directory, already have that */
|
|
split_path(argv[optind + 1], &directory, &fname);
|
|
|
|
fd = open_file_in_directory(waldir, fname);
|
|
if (fd < 0)
|
|
pg_fatal("could not open file \"%s\"", fname);
|
|
close(fd);
|
|
|
|
/* parse position from file */
|
|
XLogFromFileName(fname, &private.timeline, &endsegno, WalSegSz);
|
|
|
|
if (endsegno < segno)
|
|
pg_fatal("ENDSEG %s is before STARTSEG %s",
|
|
argv[optind + 1], argv[optind]);
|
|
|
|
if (XLogRecPtrIsInvalid(private.endptr))
|
|
XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
|
|
private.endptr);
|
|
|
|
/* set segno to endsegno for check of --end */
|
|
segno = endsegno;
|
|
}
|
|
|
|
|
|
if (!XLByteInSeg(private.endptr, segno, WalSegSz) &&
|
|
private.endptr != (segno + 1) * WalSegSz)
|
|
{
|
|
pg_log_error("end WAL location %X/%X is not inside file \"%s\"",
|
|
LSN_FORMAT_ARGS(private.endptr),
|
|
argv[argc - 1]);
|
|
goto bad_argument;
|
|
}
|
|
}
|
|
else
|
|
waldir = identify_target_directory(waldir, NULL);
|
|
|
|
/* we don't know what to print */
|
|
if (XLogRecPtrIsInvalid(private.startptr))
|
|
{
|
|
pg_log_error("no start WAL location given");
|
|
goto bad_argument;
|
|
}
|
|
|
|
/* done with argument parsing, do the actual work */
|
|
|
|
/* we have everything we need, start reading */
|
|
xlogreader_state =
|
|
XLogReaderAllocate(WalSegSz, waldir,
|
|
XL_ROUTINE(.page_read = WALDumpReadPage,
|
|
.segment_open = WALDumpOpenSegment,
|
|
.segment_close = WALDumpCloseSegment),
|
|
&private);
|
|
if (!xlogreader_state)
|
|
pg_fatal("out of memory while allocating a WAL reading processor");
|
|
|
|
/* first find a valid recptr to start from */
|
|
first_record = XLogFindNextRecord(xlogreader_state, private.startptr);
|
|
|
|
if (first_record == InvalidXLogRecPtr)
|
|
pg_fatal("could not find a valid record after %X/%X",
|
|
LSN_FORMAT_ARGS(private.startptr));
|
|
|
|
/*
|
|
* Display a message that we're skipping data if `from` wasn't a pointer
|
|
* to the start of a record and also wasn't a pointer to the beginning of
|
|
* a segment (e.g. we were used in file mode).
|
|
*/
|
|
if (first_record != private.startptr &&
|
|
XLogSegmentOffset(private.startptr, WalSegSz) != 0)
|
|
printf(ngettext("first record is after %X/%X, at %X/%X, skipping over %u byte\n",
|
|
"first record is after %X/%X, at %X/%X, skipping over %u bytes\n",
|
|
(first_record - private.startptr)),
|
|
LSN_FORMAT_ARGS(private.startptr),
|
|
LSN_FORMAT_ARGS(first_record),
|
|
(uint32) (first_record - private.startptr));
|
|
|
|
if (config.stats == true && !config.quiet)
|
|
stats.startptr = first_record;
|
|
|
|
for (;;)
|
|
{
|
|
if (time_to_stop)
|
|
{
|
|
/* We've been Ctrl-C'ed, so leave */
|
|
break;
|
|
}
|
|
|
|
/* try to read the next record */
|
|
record = XLogReadRecord(xlogreader_state, &errormsg);
|
|
if (!record)
|
|
{
|
|
if (!config.follow || private.endptr_reached)
|
|
break;
|
|
else
|
|
{
|
|
pg_usleep(1000000L); /* 1 second */
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* apply all specified filters */
|
|
if (config.filter_by_rmgr_enabled &&
|
|
!config.filter_by_rmgr[record->xl_rmid])
|
|
continue;
|
|
|
|
if (config.filter_by_xid_enabled &&
|
|
config.filter_by_xid != record->xl_xid)
|
|
continue;
|
|
|
|
/* check for extended filtering */
|
|
if (config.filter_by_extended &&
|
|
!XLogRecordMatchesRelationBlock(xlogreader_state,
|
|
config.filter_by_relation_enabled ?
|
|
config.filter_by_relation :
|
|
emptyRelFileLocator,
|
|
config.filter_by_relation_block_enabled ?
|
|
config.filter_by_relation_block :
|
|
InvalidBlockNumber,
|
|
config.filter_by_relation_forknum))
|
|
continue;
|
|
|
|
if (config.filter_by_fpw && !XLogRecordHasFPW(xlogreader_state))
|
|
continue;
|
|
|
|
/* perform any per-record work */
|
|
if (!config.quiet)
|
|
{
|
|
if (config.stats == true)
|
|
{
|
|
XLogRecStoreStats(&stats, xlogreader_state);
|
|
stats.endptr = xlogreader_state->EndRecPtr;
|
|
}
|
|
else
|
|
XLogDumpDisplayRecord(&config, xlogreader_state);
|
|
}
|
|
|
|
/* check whether we printed enough */
|
|
config.already_displayed_records++;
|
|
if (config.stop_after_records > 0 &&
|
|
config.already_displayed_records >= config.stop_after_records)
|
|
break;
|
|
}
|
|
|
|
if (config.stats == true && !config.quiet)
|
|
XLogDumpDisplayStats(&config, &stats);
|
|
|
|
if (time_to_stop)
|
|
exit(0);
|
|
|
|
if (errormsg)
|
|
pg_fatal("error in WAL record at %X/%X: %s",
|
|
LSN_FORMAT_ARGS(xlogreader_state->ReadRecPtr),
|
|
errormsg);
|
|
|
|
XLogReaderFree(xlogreader_state);
|
|
|
|
return EXIT_SUCCESS;
|
|
|
|
bad_argument:
|
|
pg_log_error_hint("Try \"%s --help\" for more information.", progname);
|
|
return EXIT_FAILURE;
|
|
}
|