Add jsonlog log_destination for JSON server logs
Hi,
This patch adds a new log_destination, "jsonlog", that writes log entries
as lines of JSON. It was originally started by David Fetter using
the jsonlog module by Michael Paquier (
https://github.com/michaelpq/pg_plugins/blob/master/jsonlog/jsonlog.c) as a
basis for how to serialize the log messages. Thanks to both of them because
this wouldn't be possible without that starting point.
The first commit splits out the destination in log pipe messages into its
own field. Previously it would piggyback on the "is_last" field. This adds
an int to the message size but makes the rest of the code easier to follow.
The second commit adds a TAP test for log_destination "csvlog". This was
done to both confirm that the previous change didn't break anything and as
a skeleton for the test in the next commit.
The third commit adds the new log_destination "jsonlog". The output format
is one line per entry with the top level output being a JSON object keyed
with the log fields. Newlines in the output fields are escaped as \n so the
output file has exactly one line per log entry. It also includes a new test
for verifying the JSON output with some basic regex checks (similar to the
csvlog test).
Here's a sample of what the log entries look like:
{"timestamp":"2021-08-31 10:15:25.129
EDT","user":"sehrope","dbname":"postgres","pid":12012,"remote_host":"[local]","session_id":"612e397d.2eec","line_num":1,"ps":"idle","session_start":"2021-08-31
10:15:25
EDT","vxid":"3/2","txid":"0","error_severity":"LOG","application_name":"
006_jsonlog.pl","message":"statement: SELECT 1/0"}
It builds and passes "make check-world" on Linux. It also includes code to
handle Windows as well but I have not actually tried building it there.
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
Attachments:
v001-0001-Adds-separate-dest-field-to-log-protocol-PipeProtoHe.patchtext/x-patch; charset=US-ASCII; name=v001-0001-Adds-separate-dest-field-to-log-protocol-PipeProtoHe.patchDownload
From d5b3f5fe44e91d35aefdd570758d5b2a9e9c1a36 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Wed, 10 Jul 2019 10:02:31 -0400
Subject: [PATCH 1/3] Adds separate dest field to log protocol PipeProtoHeader
Adds a separate dest field to PipeProtoHeader to store the log destination
requested by the sending process. Also changes the is_last field to only
store whether the chunk is the last one for a message rather than also
including whether the destination is csvlog.
---
src/backend/postmaster/syslogger.c | 15 ++++++---------
src/backend/utils/error/elog.c | 4 +++-
src/include/postmaster/syslogger.h | 4 ++--
3 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index cad43bdef2..edd8f33204 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -878,7 +878,6 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
{
char *cursor = logbuffer;
int count = *bytes_in_logbuffer;
- int dest = LOG_DESTINATION_STDERR;
/* While we have enough for a header, process data... */
while (count >= (int) (offsetof(PipeProtoHeader, data) + 1))
@@ -891,8 +890,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
- (p.is_last == 't' || p.is_last == 'f' ||
- p.is_last == 'T' || p.is_last == 'F'))
+ (p.is_last == 't' || p.is_last == 'f') &&
+ (p.dest == LOG_DESTINATION_CSVLOG ||
+ p.dest == LOG_DESTINATION_STDERR))
{
List *buffer_list;
ListCell *cell;
@@ -906,9 +906,6 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
if (count < chunklen)
break;
- dest = (p.is_last == 'T' || p.is_last == 'F') ?
- LOG_DESTINATION_CSVLOG : LOG_DESTINATION_STDERR;
-
/* Locate any existing buffer for this source pid */
buffer_list = buffer_lists[p.pid % NBUFFER_LISTS];
foreach(cell, buffer_list)
@@ -924,7 +921,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
free_slot = buf;
}
- if (p.is_last == 'f' || p.is_last == 'F')
+ if (p.is_last == 'f')
{
/*
* Save a complete non-final chunk in a per-pid buffer
@@ -970,7 +967,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
appendBinaryStringInfo(str,
cursor + PIPE_HEADER_SIZE,
p.len);
- write_syslogger_file(str->data, str->len, dest);
+ write_syslogger_file(str->data, str->len, p.dest);
/* Mark the buffer unused, and reclaim string storage */
existing_slot->pid = 0;
pfree(str->data);
@@ -979,7 +976,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
{
/* The whole message was one chunk, evidently. */
write_syslogger_file(cursor + PIPE_HEADER_SIZE, p.len,
- dest);
+ p.dest);
}
}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a3e1c59a82..cd13111708 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3250,6 +3250,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.nuls[0] = p.proto.nuls[1] = '\0';
p.proto.pid = MyProcPid;
+ p.proto.dest = (int32) dest;
+ p.proto.is_last = 'f';
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
@@ -3264,7 +3266,7 @@ write_pipe_chunks(char *data, int len, int dest)
}
/* write the last chunk */
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'T' : 't');
+ p.proto.is_last = 't';
p.proto.len = len;
memcpy(p.proto.data, data, len);
rc = write(fd, &p, PIPE_HEADER_SIZE + len);
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index 1491eecb0f..41d026a474 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -46,8 +46,8 @@ typedef struct
char nuls[2]; /* always \0\0 */
uint16 len; /* size of this chunk (counts data only) */
int32 pid; /* writer's pid */
- char is_last; /* last chunk of message? 't' or 'f' ('T' or
- * 'F' for CSV case) */
+ int32 dest; /* log destination */
+ char is_last; /* last chunk of message? 't' or 'f'*/
char data[FLEXIBLE_ARRAY_MEMBER]; /* data payload starts here */
} PipeProtoHeader;
--
2.17.1
v001-0002-Add-TAP-test-for-csvlog.patchtext/x-patch; charset=US-ASCII; name=v001-0002-Add-TAP-test-for-csvlog.patchDownload
From dfb17c0b1804b9e54a287e6a058d02dd1be27ffb Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Tue, 31 Aug 2021 10:00:54 -0400
Subject: [PATCH 2/3] Add TAP test for csvlog
---
src/bin/pg_ctl/t/005_csvlog.pl | 118 +++++++++++++++++++++++++++++++++
1 file changed, 118 insertions(+)
create mode 100644 src/bin/pg_ctl/t/005_csvlog.pl
diff --git a/src/bin/pg_ctl/t/005_csvlog.pl b/src/bin/pg_ctl/t/005_csvlog.pl
new file mode 100644
index 0000000000..c6ab0ddbcc
--- /dev/null
+++ b/src/bin/pg_ctl/t/005_csvlog.pl
@@ -0,0 +1,118 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 4;
+use Time::HiRes qw(usleep);
+
+# Set up node with logging collector
+my $node = PostgresNode->new('primary');
+$node->init();
+$node->append_conf(
+ 'postgresql.conf', qq(
+logging_collector = on
+lc_messages = 'C'
+log_destination = 'csvlog'
+));
+
+$node->start();
+
+note "Before sleep";
+usleep(100_000);
+note "Before rotate";
+$node->logrotate();
+note "After rotate";
+usleep(100_000);
+note "After rotate sleep";
+
+# Verify that log output gets to the file
+
+$node->psql('postgres', 'SELECT 1/0');
+
+my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+
+for(my $tmp=0;$tmp < 10;$tmp++) {
+ my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+ note "current_logfiles = $current_logfiles";
+ usleep(100_000);
+}
+
+
+like(
+ $current_logfiles,
+ qr|^csvlog log/postgresql-.*$|,
+ 'current_logfiles is sane');
+
+my $lfname = $current_logfiles;
+$lfname =~ s/^csvlog //;
+chomp $lfname;
+
+note "current_logfiles = $current_logfiles";
+note "lfname = $lfname";
+
+# might need to retry if logging collector process is slow...
+my $max_attempts = 180 * 10;
+
+my $first_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ my $foo = $node->data_dir . '/' . $lfname;
+ note "will slurp: $foo";
+ $first_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $first_logfile =~ m/division by zero/;
+ usleep(100_000);
+}
+
+note "first_logfile = $first_logfile";
+# Our log entry should the error message and errant SQL
+like(
+ $first_logfile,
+ qr/division by zero.*"SELECT 1\/0"/,
+ 'found expected log file content');
+
+# Sleep 2 seconds and ask for log rotation; this should result in
+# output into a different log file name.
+sleep(2);
+$node->logrotate();
+
+# pg_ctl logrotate doesn't wait for rotation request to be completed.
+# Allow a bit of time for it to happen.
+my $new_current_logfiles;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $new_current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+ last if $new_current_logfiles ne $current_logfiles;
+ usleep(100_000);
+}
+
+note "now current_logfiles = $new_current_logfiles";
+
+like(
+ $new_current_logfiles,
+ qr|^csvlog log/postgresql-.*$|,
+ 'new current_logfiles is sane');
+
+$lfname = $new_current_logfiles;
+$lfname =~ s/^csvlog //;
+chomp $lfname;
+
+# Verify that log output gets to this file, too
+
+$node->psql('postgres', 'fee fi fo fum');
+
+my $second_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $second_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $second_logfile =~ m/syntax error/;
+ usleep(100_000);
+}
+
+like(
+ $second_logfile,
+ # Our log entry should have our bad string wrapped in quotes after the error
+ qr/syntax error.*,"fee fi fo fum"/,
+ 'found expected log file content in new log file');
+
+$node->stop();
--
2.17.1
v001-0003-Add-jsonlog-log_destination-for-JSON-server-logs.patchtext/x-patch; charset=US-ASCII; name=v001-0003-Add-jsonlog-log_destination-for-JSON-server-logs.patchDownload
From fef57e34ab84f07f92b9c16ac3709a8070b5591b Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Tue, 31 Aug 2021 10:01:34 -0400
Subject: [PATCH 3/3] Add jsonlog log_destination for JSON server logs
Adds option to write server log files as JSON. Each log entry is written as its
own line with any internal newlines escaped as \n. Other non-ASCII and special
characters are also escaped using standard JSON escape sequences.
JSON logging is enabled by setting the GUC log_destination to "jsonlog" and
defaults to a log file with a ".json" extension.
---
doc/src/sgml/config.sgml | 67 ++++++-
src/backend/postmaster/syslogger.c | 177 +++++++++++++++--
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/elog.c | 296 ++++++++++++++++++++++++++++-
src/backend/utils/misc/guc.c | 2 +
src/bin/pg_ctl/t/006_jsonlog.pl | 98 ++++++++++
src/include/utils/elog.h | 1 +
7 files changed, 624 insertions(+), 22 deletions(-)
create mode 100644 src/bin/pg_ctl/t/006_jsonlog.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 2c31c35a6b..fe4bcd61ef 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5926,7 +5926,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5944,6 +5945,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ CSV-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5955,13 +5964,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6101,6 +6111,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7433,6 +7450,50 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ Each log line is serialized as a JSON object with the following fields:
+<programlisting>
+ {
+ "timestamp": time stamp with milliseconds (string),
+ "user": user name (string),
+ "dbname": database name (string),
+ "pid": process ID (number),
+ "remote_host": client host (string)
+ "remote_port": port number (string),
+ "session_id": session ID (string),
+ "line_num": per-session line number (number),
+ "ps": current ps display (string),
+ "session_start": session start time (string),
+ "vxid": virtual transaction ID (string),
+ "txid": regular transaction ID (string),
+ "error_severity": error severity (string),
+ "state_code": SQLSTATE code (string),
+ "detail": error message detail (string),
+ "hint": hint (string),
+ "internal_query": internal query that led to the error (string),
+ "internal_position": cursor index into internal query (number),
+ "context": error context (string),
+ "statement": client supplied query string (string),
+ "cursor_position": cursor index into query string (string),
+ "func_name": error location function name (string),
+ "file_name": error location file name (string),
+ "file_line_num": error location file line number (number),
+ "application_name": client application name (string),
+ "message": error message (string)
+ }
+</programlisting>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ </sect2>
<sect2>
<title>Process Title</title>
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index edd8f33204..c8f6ef56fc 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -85,9 +85,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -274,6 +276,8 @@ SysLoggerMain(int argc, char *argv[])
last_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -360,6 +364,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -410,6 +422,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -419,7 +437,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -625,6 +643,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -722,6 +754,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -744,6 +781,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -771,14 +809,25 @@ syslogger_forkexec(void)
fileno(csvlogFile));
else
strcpy(csvfilenobuf, "-1");
+ if (jsonlogFile != NULL)
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ fileno(jsonlogFile));
+ else
+ strcpy(jsonfilenobuf, "-1");
#else /* WIN32 */
if (csvlogFile != NULL)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
(long) _get_osfhandle(_fileno(csvlogFile)));
else
strcpy(csvfilenobuf, "0");
+ if (jsonlogFile != NULL)
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%ld",
+ (long) _get_osfhandle(_fileno(jsonlogFile)));
+ else
+ strcpy(jsonfilenobuf, "0");
#endif /* WIN32 */
av[ac++] = csvfilenobuf;
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -796,8 +845,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -819,6 +868,12 @@ syslogger_parseArgs(int argc, char *argv[])
csvlogFile = fdopen(fd, "a");
setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
}
+ fd = atoi(*argv++);
+ if (fd != -1)
+ {
+ jsonlogFile = fdopen(fd, "a");
+ setvbuf(jsonlogFile, NULL, PG_IOLBF, 0);
+ }
#else /* WIN32 */
fd = atoi(*argv++);
if (fd != 0)
@@ -840,6 +895,16 @@ syslogger_parseArgs(int argc, char *argv[])
setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
}
}
+ fd = atoi(*argv++);
+ if (fd != 0)
+ {
+ fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
+ if (fd > 0)
+ {
+ jsonlogFile = fdopen(fd, "a");
+ setvbuf(jsonlogFile, NULL, PG_IOLBF, 0);
+ }
+ }
#endif /* WIN32 */
}
#endif /* EXEC_BACKEND */
@@ -892,6 +957,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
p.pid != 0 &&
(p.is_last == 't' || p.is_last == 'f') &&
(p.dest == LOG_DESTINATION_CSVLOG ||
+ p.dest == LOG_DESTINATION_JSONLOG ||
p.dest == LOG_DESTINATION_STDERR))
{
List *buffer_list;
@@ -1079,19 +1145,23 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
+ * If we're told to write to a structured log file, but it's not open, dump the data
+ * to syslogFile (which is always open) instead. This can happen if structured
* output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
+ * logFile. There are also race conditions during a parameter change
+ * whereby backends might send us structured output before we open the logFile or
+ * after we close it. Writing formatted output to the regular log
* file isn't great, but it beats dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1162,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1235,6 +1306,7 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
char *filename;
char *csvfilename = NULL;
+ char *jsonfilename = NULL;
pg_time_t fntime;
FILE *fh;
@@ -1252,6 +1324,9 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
filename = logfile_getname(fntime, NULL);
if (Log_destination & LOG_DESTINATION_CSVLOG)
csvfilename = logfile_getname(fntime, ".csv");
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ jsonfilename = logfile_getname(fntime, ".json");
+
/*
* Decide whether to overwrite or append. We can overwrite if (a)
@@ -1289,6 +1364,8 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
pfree(filename);
if (csvfilename)
pfree(csvfilename);
+ if (jsonfilename)
+ pfree(jsonfilename);
return;
}
@@ -1303,10 +1380,10 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
}
/*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
+ * Same as above, but for a structured file. Note that if LOG_DESTINATION_[STRUCTURED]LOG
+ * was just turned on, we might have to open logFile here though it was
* not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
+ * last_*_file_name will be NULL); that is consistent with the normal
* rules since it's not a time-based rotation.
*/
if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
@@ -1362,11 +1439,66 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
pfree(last_csv_file_name);
last_csv_file_name = NULL;
}
+ else if ((Log_destination & LOG_DESTINATION_JSONLOG) &&
+ (jsonlogFile == NULL ||
+ time_based_rotation || (size_rotation_for & LOG_DESTINATION_JSONLOG)))
+ {
+ if (Log_truncate_on_rotation && time_based_rotation &&
+ last_json_file_name != NULL &&
+ strcmp(jsonfilename, last_json_file_name) != 0)
+ fh = logfile_open(jsonfilename, "w", true);
+ else
+ fh = logfile_open(jsonfilename, "a", true);
+
+ if (!fh)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just
+ * keep using the old file till we manage to get a new one.
+ * Otherwise, assume something's wrong with Log_directory and stop
+ * trying to create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ if (filename)
+ pfree(filename);
+ if (jsonfilename)
+ pfree(jsonfilename);
+ return;
+ }
+
+ if (jsonlogFile != NULL)
+ fclose(jsonlogFile);
+ jsonlogFile = fh;
+
+ /* instead of pfree'ing filename, remember it for next time */
+ if (last_json_file_name != NULL)
+ pfree(last_json_file_name);
+ last_json_file_name = jsonfilename;
+ jsonfilename = NULL;
+ }
+ else if (!(Log_destination & LOG_DESTINATION_JSONLOG) &&
+ jsonlogFile != NULL)
+ {
+ /* JSONLOG was just turned off, so close the old file */
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ if (last_json_file_name != NULL)
+ pfree(last_json_file_name);
+ last_json_file_name = NULL;
+ }
if (filename)
pfree(filename);
if (csvfilename)
pfree(csvfilename);
+ if (jsonfilename)
+ pfree(jsonfilename);
update_metainfo_datafile();
@@ -1454,7 +1586,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1512,6 +1645,18 @@ update_metainfo_datafile(void)
return;
}
}
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index cd13111708..2689e37be9 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -80,6 +80,7 @@
#include "storage/proc.h"
#include "tcop/tcopprot.h"
#include "utils/guc.h"
+#include "utils/json.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
@@ -180,6 +181,7 @@ static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
static void write_csvlog(ErrorData *edata);
+static void write_jsonlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
@@ -2980,6 +2982,274 @@ write_csvlog(ErrorData *edata)
pfree(buf.data);
}
+/*
+ * appendJSONKeyValue
+ * Append to given StringInfo a comma followed by a JSON key and value.
+ * Both the key and value will be escaped as JSON string literals.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value)
+{
+ if (value == NULL)
+ return;
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ escape_json(buf, value);
+}
+
+/*
+ * appendJSONKeyValueFmt
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /*
+ * Allocate result buffer. Note that in frontend this maps to malloc
+ * with exit-on-error.
+ */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+ appendJSONKeyValue(buf, key, value);
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a signed integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%d", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as an unsigned integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsUInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%u", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a long (a JSON number).
+ */
+static void
+appendJSONKeyValueAsLong(StringInfo buf, const char *key, long value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%ld", value);
+}
+
+/*
+ * Write logs in json format.
+ */
+static void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ formatted_start_time[0] = '\0';
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and jsonlog messages.
+ */
+ if (formatted_log_time[0] == '\0')
+ setup_formatted_log_time();
+
+ /* First property does not use appendJSONKeyValue as it does not have comma prefix */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, formatted_log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name);
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueAsInt(&buf, "pid", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueAsLong(&buf, "line_num", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ if (formatted_start_time[0] == '\0')
+ setup_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", formatted_start_time);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity", (char *) error_severity(edata->elevel));
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code", unpack_sql_state(edata->sqlerrcode));
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log);
+ else if (edata->detail)
+ appendJSONKeyValue(&buf, "detail", edata->detail);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint);
+
+ /* Internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery);
+
+ /* If the internal query got printed, print internal pos, too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueAsUInt(&buf, "internal_position", edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (is_log_level_output(edata->elevel, log_min_error_statement) &&
+ debug_query_string != NULL &&
+ !edata->hide_stmt)
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueAsInt(&buf, "cursor_position", edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename);
+ appendJSONKeyValueAsInt(&buf, "file_line_num", edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name);
+
+ /* Error message */
+ appendJSONKeyValue(&buf, "message", edata->message);
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
+
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
@@ -3213,6 +3483,30 @@ send_message_to_server_log(ErrorData *edata)
pfree(buf.data);
}
}
+ /* Write to JSON log if enabled */
+ else if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ /*
+ * send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). First get back the space in the message buffer.
+ */
+ pfree(buf.data);
+ write_jsonlog(edata);
+ }
+ else
+ {
+ /*
+ * syslogger not up (yet), so just dump the message to stderr,
+ * unless we already did so above.
+ */
+ if (!(Log_destination & LOG_DESTINATION_STDERR) &&
+ whereToSendOutput != DestDebug)
+ write_console(buf.data, buf.len);
+ pfree(buf.data);
+ }
+ }
else
{
pfree(buf.data);
@@ -3256,7 +3550,7 @@ write_pipe_chunks(char *data, int len, int dest)
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
{
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'F' : 'f');
+ p.proto.is_last = ((dest == LOG_DESTINATION_CSVLOG || dest == LOG_DESTINATION_JSONLOG) ? 'F' : 'f');
p.proto.len = PIPE_MAX_PAYLOAD;
memcpy(p.proto.data, data, PIPE_MAX_PAYLOAD);
rc = write(fd, &p, PIPE_HEADER_SIZE + PIPE_MAX_PAYLOAD);
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 467b0fd6fe..ea4fa0c5c1 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -11691,6 +11691,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/bin/pg_ctl/t/006_jsonlog.pl b/src/bin/pg_ctl/t/006_jsonlog.pl
new file mode 100644
index 0000000000..e6b9979fd7
--- /dev/null
+++ b/src/bin/pg_ctl/t/006_jsonlog.pl
@@ -0,0 +1,98 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 4;
+use Time::HiRes qw(usleep);
+
+# Set up node with logging collector
+my $node = PostgresNode->new('primary');
+$node->init();
+$node->append_conf(
+ 'postgresql.conf', qq(
+logging_collector = on
+lc_messages = 'C'
+log_destination = 'jsonlog'
+));
+
+
+$node->start();
+
+# Verify that log output gets to the file
+
+$node->psql('postgres', 'SELECT 1/0');
+
+my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+
+note "current_logfiles = $current_logfiles";
+
+like(
+ $current_logfiles,
+ qr|^jsonlog log/postgresql-.*json$|,
+ 'current_logfiles is sane');
+
+my $lfname = $current_logfiles;
+$lfname =~ s/^jsonlog //;
+chomp $lfname;
+
+# might need to retry if logging collector process is slow...
+my $max_attempts = 100;
+
+my $first_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $first_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $first_logfile =~ m/division by zero/;
+ usleep(100_000);
+}
+
+like(
+ $first_logfile,
+ qr/"statement":"SELECT 1\/0",.*"message":"division by zero/,
+ 'found expected log file content');
+
+# Sleep 2 seconds and ask for log rotation; this should result in
+# output into a different log file name.
+sleep(2);
+$node->logrotate();
+
+# pg_ctl logrotate doesn't wait for rotation request to be completed.
+# Allow a bit of time for it to happen.
+my $new_current_logfiles;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $new_current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+ last if $new_current_logfiles ne $current_logfiles;
+ usleep(100_000);
+}
+
+note "now current_logfiles = $new_current_logfiles";
+
+like(
+ $new_current_logfiles,
+ qr|^jsonlog log/postgresql-.*json$|,
+ 'new current_logfiles is sane');
+
+$lfname = $new_current_logfiles;
+$lfname =~ s/^jsonlog //;
+chomp $lfname;
+
+# Verify that log output gets to this file, too
+
+$node->psql('postgres', 'fee fi fo fum');
+
+my $second_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $second_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $second_logfile =~ m/syntax error/;
+ usleep(100_000);
+}
+
+like(
+ $second_logfile,
+ qr/"statement":"fee fi fo fum",.*"message":"syntax error/,
+ 'found expected log file content in new log file');
+
+$node->stop();
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..c0c699f485 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
--
2.17.1
On Tue, Aug 31, 2021 at 11:34:56AM -0400, Sehrope Sarkuni wrote:
The second commit adds a TAP test for log_destination "csvlog". This was
done to both confirm that the previous change didn't break anything and as
a skeleton for the test in the next commit.
+note "Before sleep";
+usleep(100_000);
+note "Before rotate";
+$node->logrotate();
+note "After rotate";
+usleep(100_000);
Do you really need a rotation of the log files here? Wouldn't it be
better to grab the position of the current log file with a fixed log
file name, and then slurp the file from this position with your
expected output? That would make the test faster, as well.
The third commit adds the new log_destination "jsonlog". The output format
is one line per entry with the top level output being a JSON object keyed
with the log fields. Newlines in the output fields are escaped as \n so the
output file has exactly one line per log entry. It also includes a new test
for verifying the JSON output with some basic regex checks (similar to the
csvlog test).
+ * Write logs in json format.
+ */
+static void
+write_jsonlog(ErrorData *edata)
+{
Rather than making elog.c larger, I think that we should try to split
that into more files. Why not refactoring out the CSV part first?
You could just call that csvlog.c, then create a new jsonlog.c for the
meat of the patch.
The list of fields is not up to date. At quick glance, you are
missing:
- backend type.
- leader PID.
- query ID.
- Session start timestamp (?)
--
Michael
On Tue, Aug 31, 2021 at 8:43 PM Michael Paquier <michael@paquier.xyz> wrote:
On Tue, Aug 31, 2021 at 11:34:56AM -0400, Sehrope Sarkuni wrote:
The second commit adds a TAP test for log_destination "csvlog". This was
done to both confirm that the previous change didn't break anything andas
a skeleton for the test in the next commit.
+note "Before sleep"; +usleep(100_000); +note "Before rotate"; +$node->logrotate(); +note "After rotate"; +usleep(100_000);Do you really need a rotation of the log files here? Wouldn't it be
better to grab the position of the current log file with a fixed log
file name, and then slurp the file from this position with your
expected output? That would make the test faster, as well.
Yes, that was intentional. I used the log rotation tap test as a base and
kept that piece in there so it verifies that the csv log files are actually
rotated. Same for the json logs.
Rather than making elog.c larger, I think that we should try to split
that into more files. Why not refactoring out the CSV part first?
You could just call that csvlog.c, then create a new jsonlog.c for the
meat of the patch.
That's a good idea. I'll try that out.
The list of fields is not up to date. At quick glance, you are
missing:
- backend type.
- leader PID.
- query ID.
- Session start timestamp (?)
Thanks. I'll take a look.
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
Updated patch set is attached.
This version splits out the existing csvlog code into its own file and
centralizes the common helpers into a new elog-internal.h so that they're
only included by the actual write_xyz sources.
That makes the elog.c changes in the JSON logging patch minimal as all it's
really doing is invoking the new write_jsonlog(...) function.
It also adds those missing fields to the JSON logger output.
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
Attachments:
v2-0001-Adds-separate-dest-field-to-log-protocol-PipeProtoHe.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Adds-separate-dest-field-to-log-protocol-PipeProtoHe.patchDownload
From d5b3f5fe44e91d35aefdd570758d5b2a9e9c1a36 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Wed, 10 Jul 2019 10:02:31 -0400
Subject: [PATCH 1/4] Adds separate dest field to log protocol PipeProtoHeader
Adds a separate dest field to PipeProtoHeader to store the log destination
requested by the sending process. Also changes the is_last field to only
store whether the chunk is the last one for a message rather than also
including whether the destination is csvlog.
---
src/backend/postmaster/syslogger.c | 15 ++++++---------
src/backend/utils/error/elog.c | 4 +++-
src/include/postmaster/syslogger.h | 4 ++--
3 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index cad43bdef2..edd8f33204 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -878,7 +878,6 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
{
char *cursor = logbuffer;
int count = *bytes_in_logbuffer;
- int dest = LOG_DESTINATION_STDERR;
/* While we have enough for a header, process data... */
while (count >= (int) (offsetof(PipeProtoHeader, data) + 1))
@@ -891,8 +890,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
- (p.is_last == 't' || p.is_last == 'f' ||
- p.is_last == 'T' || p.is_last == 'F'))
+ (p.is_last == 't' || p.is_last == 'f') &&
+ (p.dest == LOG_DESTINATION_CSVLOG ||
+ p.dest == LOG_DESTINATION_STDERR))
{
List *buffer_list;
ListCell *cell;
@@ -906,9 +906,6 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
if (count < chunklen)
break;
- dest = (p.is_last == 'T' || p.is_last == 'F') ?
- LOG_DESTINATION_CSVLOG : LOG_DESTINATION_STDERR;
-
/* Locate any existing buffer for this source pid */
buffer_list = buffer_lists[p.pid % NBUFFER_LISTS];
foreach(cell, buffer_list)
@@ -924,7 +921,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
free_slot = buf;
}
- if (p.is_last == 'f' || p.is_last == 'F')
+ if (p.is_last == 'f')
{
/*
* Save a complete non-final chunk in a per-pid buffer
@@ -970,7 +967,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
appendBinaryStringInfo(str,
cursor + PIPE_HEADER_SIZE,
p.len);
- write_syslogger_file(str->data, str->len, dest);
+ write_syslogger_file(str->data, str->len, p.dest);
/* Mark the buffer unused, and reclaim string storage */
existing_slot->pid = 0;
pfree(str->data);
@@ -979,7 +976,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
{
/* The whole message was one chunk, evidently. */
write_syslogger_file(cursor + PIPE_HEADER_SIZE, p.len,
- dest);
+ p.dest);
}
}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a3e1c59a82..cd13111708 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3250,6 +3250,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.nuls[0] = p.proto.nuls[1] = '\0';
p.proto.pid = MyProcPid;
+ p.proto.dest = (int32) dest;
+ p.proto.is_last = 'f';
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
@@ -3264,7 +3266,7 @@ write_pipe_chunks(char *data, int len, int dest)
}
/* write the last chunk */
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'T' : 't');
+ p.proto.is_last = 't';
p.proto.len = len;
memcpy(p.proto.data, data, len);
rc = write(fd, &p, PIPE_HEADER_SIZE + len);
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index 1491eecb0f..41d026a474 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -46,8 +46,8 @@ typedef struct
char nuls[2]; /* always \0\0 */
uint16 len; /* size of this chunk (counts data only) */
int32 pid; /* writer's pid */
- char is_last; /* last chunk of message? 't' or 'f' ('T' or
- * 'F' for CSV case) */
+ int32 dest; /* log destination */
+ char is_last; /* last chunk of message? 't' or 'f'*/
char data[FLEXIBLE_ARRAY_MEMBER]; /* data payload starts here */
} PipeProtoHeader;
--
2.17.1
v2-0002-Add-TAP-test-for-csvlog.patchtext/x-patch; charset=US-ASCII; name=v2-0002-Add-TAP-test-for-csvlog.patchDownload
From dfb17c0b1804b9e54a287e6a058d02dd1be27ffb Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Tue, 31 Aug 2021 10:00:54 -0400
Subject: [PATCH 2/4] Add TAP test for csvlog
---
src/bin/pg_ctl/t/005_csvlog.pl | 118 +++++++++++++++++++++++++++++++++
1 file changed, 118 insertions(+)
create mode 100644 src/bin/pg_ctl/t/005_csvlog.pl
diff --git a/src/bin/pg_ctl/t/005_csvlog.pl b/src/bin/pg_ctl/t/005_csvlog.pl
new file mode 100644
index 0000000000..c6ab0ddbcc
--- /dev/null
+++ b/src/bin/pg_ctl/t/005_csvlog.pl
@@ -0,0 +1,118 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 4;
+use Time::HiRes qw(usleep);
+
+# Set up node with logging collector
+my $node = PostgresNode->new('primary');
+$node->init();
+$node->append_conf(
+ 'postgresql.conf', qq(
+logging_collector = on
+lc_messages = 'C'
+log_destination = 'csvlog'
+));
+
+$node->start();
+
+note "Before sleep";
+usleep(100_000);
+note "Before rotate";
+$node->logrotate();
+note "After rotate";
+usleep(100_000);
+note "After rotate sleep";
+
+# Verify that log output gets to the file
+
+$node->psql('postgres', 'SELECT 1/0');
+
+my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+
+for(my $tmp=0;$tmp < 10;$tmp++) {
+ my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+ note "current_logfiles = $current_logfiles";
+ usleep(100_000);
+}
+
+
+like(
+ $current_logfiles,
+ qr|^csvlog log/postgresql-.*$|,
+ 'current_logfiles is sane');
+
+my $lfname = $current_logfiles;
+$lfname =~ s/^csvlog //;
+chomp $lfname;
+
+note "current_logfiles = $current_logfiles";
+note "lfname = $lfname";
+
+# might need to retry if logging collector process is slow...
+my $max_attempts = 180 * 10;
+
+my $first_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ my $foo = $node->data_dir . '/' . $lfname;
+ note "will slurp: $foo";
+ $first_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $first_logfile =~ m/division by zero/;
+ usleep(100_000);
+}
+
+note "first_logfile = $first_logfile";
+# Our log entry should the error message and errant SQL
+like(
+ $first_logfile,
+ qr/division by zero.*"SELECT 1\/0"/,
+ 'found expected log file content');
+
+# Sleep 2 seconds and ask for log rotation; this should result in
+# output into a different log file name.
+sleep(2);
+$node->logrotate();
+
+# pg_ctl logrotate doesn't wait for rotation request to be completed.
+# Allow a bit of time for it to happen.
+my $new_current_logfiles;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $new_current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+ last if $new_current_logfiles ne $current_logfiles;
+ usleep(100_000);
+}
+
+note "now current_logfiles = $new_current_logfiles";
+
+like(
+ $new_current_logfiles,
+ qr|^csvlog log/postgresql-.*$|,
+ 'new current_logfiles is sane');
+
+$lfname = $new_current_logfiles;
+$lfname =~ s/^csvlog //;
+chomp $lfname;
+
+# Verify that log output gets to this file, too
+
+$node->psql('postgres', 'fee fi fo fum');
+
+my $second_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $second_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $second_logfile =~ m/syntax error/;
+ usleep(100_000);
+}
+
+like(
+ $second_logfile,
+ # Our log entry should have our bad string wrapped in quotes after the error
+ qr/syntax error.*,"fee fi fo fum"/,
+ 'found expected log file content in new log file');
+
+$node->stop();
--
2.17.1
v2-0003-Split-csv-handling-in-elog-c-into-separate-csvlog-c.patchtext/x-patch; charset=US-ASCII; name=v2-0003-Split-csv-handling-in-elog-c-into-separate-csvlog-c.patchDownload
From 1fadc727cc77d020e559b6a16db9db72d5678bf5 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Wed, 1 Sep 2021 09:06:15 -0400
Subject: [PATCH 3/4] Split csv handling in elog.c into separate csvlog.c
Split out csvlog to its own file and centralize common elog internals
and helpers into its own file as well.
---
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 270 ++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 313 ++----------------------------
src/include/utils/elog-internal.h | 78 ++++++++
4 files changed, 365 insertions(+), 297 deletions(-)
create mode 100644 src/backend/utils/error/csvlog.c
create mode 100644 src/include/utils/elog-internal.h
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..923b7e7d73
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,270 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog-internal.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ formatted_start_time[0] = '\0';
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and csvlog messages.
+ */
+ if (formatted_log_time[0] == '\0')
+ setup_formatted_log_time();
+
+ appendStringInfoString(&buf, formatted_log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ if (formatted_start_time[0] == '\0')
+ setup_formatted_start_time();
+ appendStringInfoString(&buf, formatted_start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ if (is_log_level_output(edata->elevel, log_min_error_statement) &&
+ debug_query_string != NULL &&
+ !edata->hide_stmt)
+ print_stmt = true;
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ write_syslogger(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index cd13111708..47d6677827 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -82,7 +82,7 @@
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
-
+#include "utils/elog-internal.h"
/* In this module, access gettext() via err_gettext() */
#undef _
@@ -155,9 +155,8 @@ static int recursion_depth = 0; /* to detect actual recursion */
static struct timeval saved_timeval;
static bool saved_timeval_set = false;
-#define FORMATTED_TS_LEN 128
-static char formatted_start_time[FORMATTED_TS_LEN];
-static char formatted_log_time[FORMATTED_TS_LEN];
+char formatted_start_time[FORMATTED_TS_LEN];
+char formatted_log_time[FORMATTED_TS_LEN];
/* Macro for checking errordata_stack_depth is reasonable */
@@ -175,52 +174,13 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
-
-/*
- * is_log_level_output -- is elevel logically >= log_min_level?
- *
- * We use this for tests that should consider LOG to sort out-of-order,
- * between ERROR and FATAL. Generally this is the right thing for testing
- * whether a message should go to the postmaster log, whereas a simple >=
- * test is correct for testing whether the message should go to the client.
- */
-static inline bool
-is_log_level_output(int elevel, int log_min_level)
-{
- if (elevel == LOG || elevel == LOG_SERVER_ONLY)
- {
- if (log_min_level == LOG || log_min_level <= ERROR)
- return true;
- }
- else if (elevel == WARNING_CLIENT_ONLY)
- {
- /* never sent to log, regardless of log_min_level */
- return false;
- }
- else if (log_min_level == LOG)
- {
- /* elevel != LOG */
- if (elevel >= FATAL)
- return true;
- }
- /* Neither is LOG */
- else if (elevel >= log_min_level)
- return true;
-
- return false;
-}
-
/*
* Policy-setting subroutines. These are fairly simple, but it seems wise
* to have the code in just one place.
@@ -2291,7 +2251,7 @@ write_console(const char *line, int len)
/*
* setup formatted_log_time, for consistent times between CSV and regular logs
*/
-static void
+void
setup_formatted_log_time(void)
{
pg_time_t stamp_time;
@@ -2323,7 +2283,7 @@ setup_formatted_log_time(void)
/*
* setup formatted_start_time
*/
-static void
+void
setup_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
@@ -2729,257 +2689,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-static void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
@@ -3273,6 +2982,16 @@ write_pipe_chunks(char *data, int len, int dest)
(void) rc;
}
+void
+write_syslogger(char *data, int len, int dest)
+{
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(data, len, dest);
+ else
+ write_pipe_chunks(data, len, dest);
+}
+
/*
* Append a text string to the error report being built for the client.
@@ -3483,7 +3202,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
diff --git a/src/include/utils/elog-internal.h b/src/include/utils/elog-internal.h
new file mode 100644
index 0000000000..ac08b6f12f
--- /dev/null
+++ b/src/include/utils/elog-internal.h
@@ -0,0 +1,78 @@
+/*-------------------------------------------------------------------------
+ *
+ * elog-internal.h
+ * POSTGRES error reporting/logging internal definitions.
+ *
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ * src/include/utils/elog-internal.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ELOG_INTERNAL_H
+#define ELOG_INTERNAL_H
+
+#include "postgres.h"
+
+#include "utils/elog.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "postmaster/bgworker.h"
+
+const char * error_severity(int elevel);
+void write_syslogger(char *data, int len, int dest);
+
+void write_csvlog(ErrorData *edata);
+
+/*
+ * is_log_level_output -- is elevel logically >= log_min_level?
+ *
+ * We use this for tests that should consider LOG to sort out-of-order,
+ * between ERROR and FATAL. Generally this is the right thing for testing
+ * whether a message should go to the postmaster log, whereas a simple >=
+ * test is correct for testing whether the message should go to the client.
+ */
+static inline bool
+is_log_level_output(int elevel, int log_min_level)
+{
+ if (elevel == LOG || elevel == LOG_SERVER_ONLY)
+ {
+ if (log_min_level == LOG || log_min_level <= ERROR)
+ return true;
+ }
+ else if (elevel == WARNING_CLIENT_ONLY)
+ {
+ /* never sent to log, regardless of log_min_level */
+ return false;
+ }
+ else if (log_min_level == LOG)
+ {
+ /* elevel != LOG */
+ if (elevel >= FATAL)
+ return true;
+ }
+ /* Neither is LOG */
+ else if (elevel >= log_min_level)
+ return true;
+
+ return false;
+}
+
+static inline const char *
+get_backend_type_for_log() {
+ if (MyProcPid == PostmasterPid)
+ return "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ return MyBgworkerEntry->bgw_type;
+ else
+ return GetBackendTypeDesc(MyBackendType);
+}
+
+#define FORMATTED_TS_LEN 128
+extern char formatted_start_time[FORMATTED_TS_LEN];
+extern char formatted_log_time[FORMATTED_TS_LEN];
+
+void setup_formatted_log_time(void);
+void setup_formatted_start_time(void);
+
+#endif
--
2.17.1
v2-0004-Add-jsonlog-log_destination-for-JSON-server-logs.patchtext/x-patch; charset=US-ASCII; name=v2-0004-Add-jsonlog-log_destination-for-JSON-server-logs.patchDownload
From 1805e8dbdfc3ea2f5460610d90ecc972e59d8e88 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Wed, 1 Sep 2021 13:49:27 -0400
Subject: [PATCH 4/4] Add jsonlog log_destination for JSON server logs
Adds option to write server log files as JSON. Each log entry is written as its
own line with any internal newlines escaped as \n. Other non-ASCII and special
characters are also escaped using standard JSON escape sequences.
JSON logging is enabled by setting the GUC log_destination to "jsonlog" and
defaults to a log file with a ".json" extension.
---
doc/src/sgml/config.sgml | 67 ++++++-
src/backend/postmaster/syslogger.c | 177 ++++++++++++++--
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 26 ++-
src/backend/utils/error/jsonlog.c | 312 +++++++++++++++++++++++++++++
src/backend/utils/misc/guc.c | 2 +
src/bin/pg_ctl/t/006_jsonlog.pl | 98 +++++++++
src/include/utils/elog-internal.h | 1 +
src/include/utils/elog.h | 1 +
10 files changed, 669 insertions(+), 23 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
create mode 100644 src/bin/pg_ctl/t/006_jsonlog.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 2c31c35a6b..fe4bcd61ef 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5926,7 +5926,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5944,6 +5945,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ CSV-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5955,13 +5964,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6101,6 +6111,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7433,6 +7450,50 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ Each log line is serialized as a JSON object with the following fields:
+<programlisting>
+ {
+ "timestamp": time stamp with milliseconds (string),
+ "user": user name (string),
+ "dbname": database name (string),
+ "pid": process ID (number),
+ "remote_host": client host (string)
+ "remote_port": port number (string),
+ "session_id": session ID (string),
+ "line_num": per-session line number (number),
+ "ps": current ps display (string),
+ "session_start": session start time (string),
+ "vxid": virtual transaction ID (string),
+ "txid": regular transaction ID (string),
+ "error_severity": error severity (string),
+ "state_code": SQLSTATE code (string),
+ "detail": error message detail (string),
+ "hint": hint (string),
+ "internal_query": internal query that led to the error (string),
+ "internal_position": cursor index into internal query (number),
+ "context": error context (string),
+ "statement": client supplied query string (string),
+ "cursor_position": cursor index into query string (string),
+ "func_name": error location function name (string),
+ "file_name": error location file name (string),
+ "file_line_num": error location file line number (number),
+ "application_name": client application name (string),
+ "message": error message (string)
+ }
+</programlisting>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ </sect2>
<sect2>
<title>Process Title</title>
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index edd8f33204..c8f6ef56fc 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -85,9 +85,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -274,6 +276,8 @@ SysLoggerMain(int argc, char *argv[])
last_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -360,6 +364,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -410,6 +422,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -419,7 +437,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -625,6 +643,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -722,6 +754,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -744,6 +781,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -771,14 +809,25 @@ syslogger_forkexec(void)
fileno(csvlogFile));
else
strcpy(csvfilenobuf, "-1");
+ if (jsonlogFile != NULL)
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ fileno(jsonlogFile));
+ else
+ strcpy(jsonfilenobuf, "-1");
#else /* WIN32 */
if (csvlogFile != NULL)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
(long) _get_osfhandle(_fileno(csvlogFile)));
else
strcpy(csvfilenobuf, "0");
+ if (jsonlogFile != NULL)
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%ld",
+ (long) _get_osfhandle(_fileno(jsonlogFile)));
+ else
+ strcpy(jsonfilenobuf, "0");
#endif /* WIN32 */
av[ac++] = csvfilenobuf;
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -796,8 +845,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -819,6 +868,12 @@ syslogger_parseArgs(int argc, char *argv[])
csvlogFile = fdopen(fd, "a");
setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
}
+ fd = atoi(*argv++);
+ if (fd != -1)
+ {
+ jsonlogFile = fdopen(fd, "a");
+ setvbuf(jsonlogFile, NULL, PG_IOLBF, 0);
+ }
#else /* WIN32 */
fd = atoi(*argv++);
if (fd != 0)
@@ -840,6 +895,16 @@ syslogger_parseArgs(int argc, char *argv[])
setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
}
}
+ fd = atoi(*argv++);
+ if (fd != 0)
+ {
+ fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
+ if (fd > 0)
+ {
+ jsonlogFile = fdopen(fd, "a");
+ setvbuf(jsonlogFile, NULL, PG_IOLBF, 0);
+ }
+ }
#endif /* WIN32 */
}
#endif /* EXEC_BACKEND */
@@ -892,6 +957,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
p.pid != 0 &&
(p.is_last == 't' || p.is_last == 'f') &&
(p.dest == LOG_DESTINATION_CSVLOG ||
+ p.dest == LOG_DESTINATION_JSONLOG ||
p.dest == LOG_DESTINATION_STDERR))
{
List *buffer_list;
@@ -1079,19 +1145,23 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
+ * If we're told to write to a structured log file, but it's not open, dump the data
+ * to syslogFile (which is always open) instead. This can happen if structured
* output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
+ * logFile. There are also race conditions during a parameter change
+ * whereby backends might send us structured output before we open the logFile or
+ * after we close it. Writing formatted output to the regular log
* file isn't great, but it beats dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1162,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1235,6 +1306,7 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
char *filename;
char *csvfilename = NULL;
+ char *jsonfilename = NULL;
pg_time_t fntime;
FILE *fh;
@@ -1252,6 +1324,9 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
filename = logfile_getname(fntime, NULL);
if (Log_destination & LOG_DESTINATION_CSVLOG)
csvfilename = logfile_getname(fntime, ".csv");
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ jsonfilename = logfile_getname(fntime, ".json");
+
/*
* Decide whether to overwrite or append. We can overwrite if (a)
@@ -1289,6 +1364,8 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
pfree(filename);
if (csvfilename)
pfree(csvfilename);
+ if (jsonfilename)
+ pfree(jsonfilename);
return;
}
@@ -1303,10 +1380,10 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
}
/*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
+ * Same as above, but for a structured file. Note that if LOG_DESTINATION_[STRUCTURED]LOG
+ * was just turned on, we might have to open logFile here though it was
* not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
+ * last_*_file_name will be NULL); that is consistent with the normal
* rules since it's not a time-based rotation.
*/
if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
@@ -1362,11 +1439,66 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
pfree(last_csv_file_name);
last_csv_file_name = NULL;
}
+ else if ((Log_destination & LOG_DESTINATION_JSONLOG) &&
+ (jsonlogFile == NULL ||
+ time_based_rotation || (size_rotation_for & LOG_DESTINATION_JSONLOG)))
+ {
+ if (Log_truncate_on_rotation && time_based_rotation &&
+ last_json_file_name != NULL &&
+ strcmp(jsonfilename, last_json_file_name) != 0)
+ fh = logfile_open(jsonfilename, "w", true);
+ else
+ fh = logfile_open(jsonfilename, "a", true);
+
+ if (!fh)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just
+ * keep using the old file till we manage to get a new one.
+ * Otherwise, assume something's wrong with Log_directory and stop
+ * trying to create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ if (filename)
+ pfree(filename);
+ if (jsonfilename)
+ pfree(jsonfilename);
+ return;
+ }
+
+ if (jsonlogFile != NULL)
+ fclose(jsonlogFile);
+ jsonlogFile = fh;
+
+ /* instead of pfree'ing filename, remember it for next time */
+ if (last_json_file_name != NULL)
+ pfree(last_json_file_name);
+ last_json_file_name = jsonfilename;
+ jsonfilename = NULL;
+ }
+ else if (!(Log_destination & LOG_DESTINATION_JSONLOG) &&
+ jsonlogFile != NULL)
+ {
+ /* JSONLOG was just turned off, so close the old file */
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ if (last_json_file_name != NULL)
+ pfree(last_json_file_name);
+ last_json_file_name = NULL;
+ }
if (filename)
pfree(filename);
if (csvfilename)
pfree(csvfilename);
+ if (jsonfilename)
+ pfree(jsonfilename);
update_metainfo_datafile();
@@ -1454,7 +1586,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1512,6 +1645,18 @@ update_metainfo_datafile(void)
return;
}
}
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 47d6677827..c259616a76 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2922,6 +2922,30 @@ send_message_to_server_log(ErrorData *edata)
pfree(buf.data);
}
}
+ /* Write to JSON log if enabled */
+ else if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ /*
+ * send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). First get back the space in the message buffer.
+ */
+ pfree(buf.data);
+ write_jsonlog(edata);
+ }
+ else
+ {
+ /*
+ * syslogger not up (yet), so just dump the message to stderr,
+ * unless we already did so above.
+ */
+ if (!(Log_destination & LOG_DESTINATION_STDERR) &&
+ whereToSendOutput != DestDebug)
+ write_console(buf.data, buf.len);
+ pfree(buf.data);
+ }
+ }
else
{
pfree(buf.data);
@@ -2965,7 +2989,7 @@ write_pipe_chunks(char *data, int len, int dest)
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
{
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'F' : 'f');
+ p.proto.is_last = ((dest == LOG_DESTINATION_CSVLOG || dest == LOG_DESTINATION_JSONLOG) ? 'F' : 'f');
p.proto.len = PIPE_MAX_PAYLOAD;
memcpy(p.proto.data, data, PIPE_MAX_PAYLOAD);
rc = write(fd, &p, PIPE_HEADER_SIZE + PIPE_MAX_PAYLOAD);
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..2777124977
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,312 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog-internal.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+/*
+ * appendJSONKeyValue
+ * Append to given StringInfo a comma followed by a JSON key and value.
+ * Both the key and value will be escaped as JSON string literals.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value)
+{
+ if (value == NULL)
+ return;
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ escape_json(buf, value);
+}
+
+/*
+ * appendJSONKeyValueFmt
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /*
+ * Allocate result buffer. Note that in frontend this maps to malloc
+ * with exit-on-error.
+ */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+ appendJSONKeyValue(buf, key, value);
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a signed integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%d", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as an unsigned integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsUInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%u", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a long (a JSON number).
+ */
+static void
+appendJSONKeyValueAsLong(StringInfo buf, const char *key, long value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%ld", value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ formatted_start_time[0] = '\0';
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and jsonlog messages.
+ */
+ if (formatted_log_time[0] == '\0')
+ setup_formatted_log_time();
+
+ /* First property does not use appendJSONKeyValue as it does not have comma prefix */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, formatted_log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueAsInt(&buf, "leader_pid", leader->pid);
+ }
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueAsInt(&buf, "pid", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueAsLong(&buf, "line_num", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ if (formatted_start_time[0] == '\0')
+ setup_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", formatted_start_time);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity", (char *) error_severity(edata->elevel));
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", "%lld", (long long) pgstat_get_my_query_id());
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code", unpack_sql_state(edata->sqlerrcode));
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log);
+ else if (edata->detail)
+ appendJSONKeyValue(&buf, "detail", edata->detail);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint);
+
+ /* Internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery);
+
+ /* If the internal query got printed, print internal pos, too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueAsUInt(&buf, "internal_position", edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (is_log_level_output(edata->elevel, log_min_error_statement) &&
+ debug_query_string != NULL &&
+ !edata->hide_stmt)
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueAsInt(&buf, "cursor_position", edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename);
+ appendJSONKeyValueAsInt(&buf, "file_line_num", edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log());
+
+ /* Error message */
+ appendJSONKeyValue(&buf, "message", edata->message);
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ write_syslogger(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 467b0fd6fe..ea4fa0c5c1 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -11691,6 +11691,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/bin/pg_ctl/t/006_jsonlog.pl b/src/bin/pg_ctl/t/006_jsonlog.pl
new file mode 100644
index 0000000000..e6b9979fd7
--- /dev/null
+++ b/src/bin/pg_ctl/t/006_jsonlog.pl
@@ -0,0 +1,98 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 4;
+use Time::HiRes qw(usleep);
+
+# Set up node with logging collector
+my $node = PostgresNode->new('primary');
+$node->init();
+$node->append_conf(
+ 'postgresql.conf', qq(
+logging_collector = on
+lc_messages = 'C'
+log_destination = 'jsonlog'
+));
+
+
+$node->start();
+
+# Verify that log output gets to the file
+
+$node->psql('postgres', 'SELECT 1/0');
+
+my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+
+note "current_logfiles = $current_logfiles";
+
+like(
+ $current_logfiles,
+ qr|^jsonlog log/postgresql-.*json$|,
+ 'current_logfiles is sane');
+
+my $lfname = $current_logfiles;
+$lfname =~ s/^jsonlog //;
+chomp $lfname;
+
+# might need to retry if logging collector process is slow...
+my $max_attempts = 100;
+
+my $first_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $first_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $first_logfile =~ m/division by zero/;
+ usleep(100_000);
+}
+
+like(
+ $first_logfile,
+ qr/"statement":"SELECT 1\/0",.*"message":"division by zero/,
+ 'found expected log file content');
+
+# Sleep 2 seconds and ask for log rotation; this should result in
+# output into a different log file name.
+sleep(2);
+$node->logrotate();
+
+# pg_ctl logrotate doesn't wait for rotation request to be completed.
+# Allow a bit of time for it to happen.
+my $new_current_logfiles;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $new_current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+ last if $new_current_logfiles ne $current_logfiles;
+ usleep(100_000);
+}
+
+note "now current_logfiles = $new_current_logfiles";
+
+like(
+ $new_current_logfiles,
+ qr|^jsonlog log/postgresql-.*json$|,
+ 'new current_logfiles is sane');
+
+$lfname = $new_current_logfiles;
+$lfname =~ s/^jsonlog //;
+chomp $lfname;
+
+# Verify that log output gets to this file, too
+
+$node->psql('postgres', 'fee fi fo fum');
+
+my $second_logfile;
+for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+{
+ $second_logfile = slurp_file($node->data_dir . '/' . $lfname);
+ last if $second_logfile =~ m/syntax error/;
+ usleep(100_000);
+}
+
+like(
+ $second_logfile,
+ qr/"statement":"fee fi fo fum",.*"message":"syntax error/,
+ 'found expected log file content in new log file');
+
+$node->stop();
diff --git a/src/include/utils/elog-internal.h b/src/include/utils/elog-internal.h
index ac08b6f12f..13c217a29d 100644
--- a/src/include/utils/elog-internal.h
+++ b/src/include/utils/elog-internal.h
@@ -23,6 +23,7 @@ const char * error_severity(int elevel);
void write_syslogger(char *data, int len, int dest);
void write_csvlog(ErrorData *edata);
+void write_jsonlog(ErrorData *edata);
/*
* is_log_level_output -- is elevel logically >= log_min_level?
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..c0c699f485 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
--
2.17.1
On Wed, Sep 01, 2021 at 04:39:43PM -0400, Sehrope Sarkuni wrote:
That makes the elog.c changes in the JSON logging patch minimal as all it's
really doing is invoking the new write_jsonlog(...) function.
Looking at 0001, to do things in order.
@@ -46,8 +46,8 @@ typedef struct char nuls[2]; /* always \0\0 */ uint16 len; /* size of this chunk (counts data only) */ int32 pid; /* writer's pid */ - char is_last; /* last chunk of message? 't' or 'f' ('T' or - * 'F' for CSV case) */ + int32 dest; /* log destination */ + char is_last; /* last chunk of message? 't' or 'f'*/ char data[FLEXIBLE_ARRAY_MEMBER]; /* data payload starts here */ } PipeProtoHeader;
Making PipeProtoHeader larger is not free, and that could penalize
workloads with a lot of short messages and many backends as the
syslogger relies on pipes with sync calls. Why not switching is_last
to bits8 flags instead? That should be enough for the addition of
JSON. 3 bits are enough at the end: one to know if it is the last
chunk of message, one for CSV and one for JSON.
--
Michael
On 9/8/21 2:58 AM, Michael Paquier wrote:
On Wed, Sep 01, 2021 at 04:39:43PM -0400, Sehrope Sarkuni wrote:
That makes the elog.c changes in the JSON logging patch minimal as all it's
really doing is invoking the new write_jsonlog(...) function.Looking at 0001, to do things in order.
@@ -46,8 +46,8 @@ typedef struct char nuls[2]; /* always \0\0 */ uint16 len; /* size of this chunk (counts data only) */ int32 pid; /* writer's pid */ - char is_last; /* last chunk of message? 't' or 'f' ('T' or - * 'F' for CSV case) */ + int32 dest; /* log destination */ + char is_last; /* last chunk of message? 't' or 'f'*/ char data[FLEXIBLE_ARRAY_MEMBER]; /* data payload starts here */ } PipeProtoHeader;Making PipeProtoHeader larger is not free, and that could penalize
workloads with a lot of short messages and many backends as the
syslogger relies on pipes with sync calls. Why not switching is_last
to bits8 flags instead? That should be enough for the addition of
JSON. 3 bits are enough at the end: one to know if it is the last
chunk of message, one for CSV and one for JSON.
Yeah. A very simple change would be to use two different values for json
(say 'y' and 'n'). A slightly more principled scheme might use the top
bit for the end marker and the bottom 3 bits for the dest type (so up to
8 types possible), with the rest available for future use.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Wed, Sep 08, 2021 at 08:46:44AM -0400, Andrew Dunstan wrote:
Yeah. A very simple change would be to use two different values for json
(say 'y' and 'n'). A slightly more principled scheme might use the top
bit for the end marker and the bottom 3 bits for the dest type (so up to
8 types possible), with the rest available for future use.
I was thinking to track stderr as a case where no bits are set in the
flags for the area of the destinations, but that's a bit crazy if we
have a lot of margin in what can be stored. I have looked at that and
finished with the attached which is an improvement IMO, especially
when it comes to the header validation.
FWIW, while looking at my own external module for the JSON logs, I
noticed that I used the chunk protocol when the log redirection is
enabled, but just enforced everything to be sent to stderr.
--
Michael
Attachments:
syslogger-flags.patchtext/plain; charset=us-asciiDownload
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index 1491eecb0f..c79dfbeba2 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -46,8 +46,7 @@ typedef struct
char nuls[2]; /* always \0\0 */
uint16 len; /* size of this chunk (counts data only) */
int32 pid; /* writer's pid */
- char is_last; /* last chunk of message? 't' or 'f' ('T' or
- * 'F' for CSV case) */
+ bits8 flags; /* bitmask of PIPE_PROTO_* */
char data[FLEXIBLE_ARRAY_MEMBER]; /* data payload starts here */
} PipeProtoHeader;
@@ -60,6 +59,11 @@ typedef union
#define PIPE_HEADER_SIZE offsetof(PipeProtoHeader, data)
#define PIPE_MAX_PAYLOAD ((int) (PIPE_CHUNK_SIZE - PIPE_HEADER_SIZE))
+/* flag bits for PipeProtoHeader->flags */
+#define PIPE_PROTO_IS_LAST 0x01 /* last chunk of message? */
+/* log destinations */
+#define PIPE_PROTO_DEST_STDERR 0x10
+#define PIPE_PROTO_DEST_CSVLOG 0x20
/* GUC options */
extern bool Logging_collector;
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index cad43bdef2..c00b2dc186 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -891,8 +891,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
- (p.is_last == 't' || p.is_last == 'f' ||
- p.is_last == 'T' || p.is_last == 'F'))
+ (p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG)) != 0)
{
List *buffer_list;
ListCell *cell;
@@ -906,8 +905,15 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
if (count < chunklen)
break;
- dest = (p.is_last == 'T' || p.is_last == 'F') ?
- LOG_DESTINATION_CSVLOG : LOG_DESTINATION_STDERR;
+ if ((p.flags & PIPE_PROTO_DEST_STDERR) != 0)
+ dest = LOG_DESTINATION_STDERR;
+ else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
+ dest = LOG_DESTINATION_CSVLOG;
+ else
+ {
+ /* this should never happen as of the header validation */
+ Assert(false);
+ }
/* Locate any existing buffer for this source pid */
buffer_list = buffer_lists[p.pid % NBUFFER_LISTS];
@@ -924,7 +930,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
free_slot = buf;
}
- if (p.is_last == 'f' || p.is_last == 'F')
+ if ((p.flags & PIPE_PROTO_IS_LAST) == 0)
{
/*
* Save a complete non-final chunk in a per-pid buffer
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 816b071afa..2af87ee3bd 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3250,11 +3250,16 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.nuls[0] = p.proto.nuls[1] = '\0';
p.proto.pid = MyProcPid;
+ p.proto.flags = 0;
+ if (dest == LOG_DESTINATION_STDERR)
+ p.proto.flags |= PIPE_PROTO_DEST_STDERR;
+ else if (dest == LOG_DESTINATION_CSVLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
{
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'F' : 'f');
+ /* no need to set PIPE_PROTO_IS_LAST yet */
p.proto.len = PIPE_MAX_PAYLOAD;
memcpy(p.proto.data, data, PIPE_MAX_PAYLOAD);
rc = write(fd, &p, PIPE_HEADER_SIZE + PIPE_MAX_PAYLOAD);
@@ -3264,7 +3269,7 @@ write_pipe_chunks(char *data, int len, int dest)
}
/* write the last chunk */
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'T' : 't');
+ p.proto.flags |= PIPE_PROTO_IS_LAST;
p.proto.len = len;
memcpy(p.proto.data, data, len);
rc = write(fd, &p, PIPE_HEADER_SIZE + len);
Fwiw I was shocked when I saw the t/f T/F kluge when I went to work on
jsonlogging. That's the kind of dead-end short-sighted hack that just
lays traps and barriers for future hackers to have to clean up before
they can do the work they want to do.
Please just put a "format" field (or "channel" field -- the logging
daemon doesn't really care what format) with a list of defined formats
that can easily be extended in the future. If you want to steal the
high bit for "is last" and only allow 128 values instead of 256 so be
it.
On Wed, Sep 08, 2021 at 10:58:51PM -0400, Greg Stark wrote:
Please just put a "format" field (or "channel" field -- the logging
daemon doesn't really care what format) with a list of defined formats
that can easily be extended in the future. If you want to steal the
high bit for "is last" and only allow 128 values instead of 256 so be
it.
Which is what I just posted here:
/messages/by-id/YTlunSciDRl1z7ik@paquier.xyz
Well, we could also do things so as we have two fields, as of
something like:
typedef struct
{
[...]
bits8 flags:4, format:4;
[...]
} PipeProtoHeader;
I am not sure if this is an improvement in readability though, and
that's less consistent with the recent practice we've been trying to
follow with bitmasks and flag-like options.
--
Michael
On Thu, Sep 09, 2021 at 11:17:01AM +0900, Michael Paquier wrote:
I was thinking to track stderr as a case where no bits are set in the
flags for the area of the destinations, but that's a bit crazy if we
have a lot of margin in what can be stored. I have looked at that and
finished with the attached which is an improvement IMO, especially
when it comes to the header validation.
One part that was a bit flacky after more consideration is that the
header validation would consider as correct the case where both stderr
and csvlog are set in the set of flags. I have finished by just using
pg_popcount() on one byte with a filter on the log destinations,
making the whole more robust. If there are any objections, please let
me know.
--
Michael
Attachments:
syslogger-flags-2.patchtext/plain; charset=us-asciiDownload
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index 1491eecb0f..c79dfbeba2 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -46,8 +46,7 @@ typedef struct
char nuls[2]; /* always \0\0 */
uint16 len; /* size of this chunk (counts data only) */
int32 pid; /* writer's pid */
- char is_last; /* last chunk of message? 't' or 'f' ('T' or
- * 'F' for CSV case) */
+ bits8 flags; /* bitmask of PIPE_PROTO_* */
char data[FLEXIBLE_ARRAY_MEMBER]; /* data payload starts here */
} PipeProtoHeader;
@@ -60,6 +59,11 @@ typedef union
#define PIPE_HEADER_SIZE offsetof(PipeProtoHeader, data)
#define PIPE_MAX_PAYLOAD ((int) (PIPE_CHUNK_SIZE - PIPE_HEADER_SIZE))
+/* flag bits for PipeProtoHeader->flags */
+#define PIPE_PROTO_IS_LAST 0x01 /* last chunk of message? */
+/* log destinations */
+#define PIPE_PROTO_DEST_STDERR 0x10
+#define PIPE_PROTO_DEST_CSVLOG 0x20
/* GUC options */
extern bool Logging_collector;
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index cad43bdef2..bca3883572 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -38,6 +38,7 @@
#include "nodes/pg_list.h"
#include "pgstat.h"
#include "pgtime.h"
+#include "port/pg_bitutils.h"
#include "postmaster/fork_process.h"
#include "postmaster/interrupt.h"
#include "postmaster/postmaster.h"
@@ -885,14 +886,15 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
{
PipeProtoHeader p;
int chunklen;
+ bits8 dest_flags;
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
- (p.is_last == 't' || p.is_last == 'f' ||
- p.is_last == 'T' || p.is_last == 'F'))
+ pg_popcount((char *) &dest_flags, 1) == 1)
{
List *buffer_list;
ListCell *cell;
@@ -906,8 +908,15 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
if (count < chunklen)
break;
- dest = (p.is_last == 'T' || p.is_last == 'F') ?
- LOG_DESTINATION_CSVLOG : LOG_DESTINATION_STDERR;
+ if ((p.flags & PIPE_PROTO_DEST_STDERR) != 0)
+ dest = LOG_DESTINATION_STDERR;
+ else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
+ dest = LOG_DESTINATION_CSVLOG;
+ else
+ {
+ /* this should never happen as of the header validation */
+ Assert(false);
+ }
/* Locate any existing buffer for this source pid */
buffer_list = buffer_lists[p.pid % NBUFFER_LISTS];
@@ -924,7 +933,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
free_slot = buf;
}
- if (p.is_last == 'f' || p.is_last == 'F')
+ if ((p.flags & PIPE_PROTO_IS_LAST) == 0)
{
/*
* Save a complete non-final chunk in a per-pid buffer
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 816b071afa..2af87ee3bd 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3250,11 +3250,16 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.nuls[0] = p.proto.nuls[1] = '\0';
p.proto.pid = MyProcPid;
+ p.proto.flags = 0;
+ if (dest == LOG_DESTINATION_STDERR)
+ p.proto.flags |= PIPE_PROTO_DEST_STDERR;
+ else if (dest == LOG_DESTINATION_CSVLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
{
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'F' : 'f');
+ /* no need to set PIPE_PROTO_IS_LAST yet */
p.proto.len = PIPE_MAX_PAYLOAD;
memcpy(p.proto.data, data, PIPE_MAX_PAYLOAD);
rc = write(fd, &p, PIPE_HEADER_SIZE + PIPE_MAX_PAYLOAD);
@@ -3264,7 +3269,7 @@ write_pipe_chunks(char *data, int len, int dest)
}
/* write the last chunk */
- p.proto.is_last = (dest == LOG_DESTINATION_CSVLOG ? 'T' : 't');
+ p.proto.flags |= PIPE_PROTO_IS_LAST;
p.proto.len = len;
memcpy(p.proto.data, data, len);
rc = write(fd, &p, PIPE_HEADER_SIZE + len);
On Wed, Sep 01, 2021 at 04:39:43PM -0400, Sehrope Sarkuni wrote:
This version splits out the existing csvlog code into its own file and
centralizes the common helpers into a new elog-internal.h so that they're
only included by the actual write_xyz sources.That makes the elog.c changes in the JSON logging patch minimal as all it's
really doing is invoking the new write_jsonlog(...) function.It also adds those missing fields to the JSON logger output.
Forking a bit this thread while looking at 0002 that adds new tests
for csvlog. While I agree that it would be useful to have more
coverage with the syslogger message chunk protocol in this area, I
think that having a separate test is a waste of resources. Creating a
new node is not cheap either, and this adds more wait phases, making
the tests take longer. It would be much better to extend
004_logrotate.pl and update it to use log_destination = 'stderr,
csvlog', to minimize the number of nodes we create as well as the
additional amount of time we'd spend for those tests. Plugging in
JSON into that would not be complicated either once we have in place a
set of small routines that limit the code duplication between the
checks for each log destination type.
--
Michael
On Fri, Sep 10, 2021 at 01:07:00PM +0900, Michael Paquier wrote:
Forking a bit this thread while looking at 0002 that adds new tests
for csvlog. While I agree that it would be useful to have more
coverage with the syslogger message chunk protocol in this area, I
think that having a separate test is a waste of resources. Creating a
new node is not cheap either, and this adds more wait phases, making
the tests take longer. It would be much better to extend
004_logrotate.pl and update it to use log_destination = 'stderr,
csvlog', to minimize the number of nodes we create as well as the
additional amount of time we'd spend for those tests. Plugging in
JSON into that would not be complicated either once we have in place a
set of small routines that limit the code duplication between the
checks for each log destination type.
And this part leads me to the attached, where the addition of the JSON
format would result in the addition of a couple of lines.
--
Michael
Attachments:
tap-csvlog.patchtext/plain; charset=us-asciiDownload
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index fa14b98c7d..aa0d64a4f7 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,15 +6,64 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 5;
+use Test::More tests => 10;
use Time::HiRes qw(usleep);
+# Extract the file name of a $format from the contents of
+# current_logfiles.
+sub fetch_file_name
+{
+ my $logfiles = shift;
+ my $format = shift;
+ my @lines = split(/\n/, $logfiles);
+ my $filename = undef;
+ foreach my $line (@lines)
+ {
+ if ($line =~ /$format (.*)$/gm)
+ {
+ $filename = $1;
+ }
+ }
+
+ return $filename;
+}
+
+# Check for a pattern in the logs associated to one format.
+sub check_log_pattern
+{
+ my $format = shift;
+ my $logfiles = shift;
+ my $pattern = shift;
+ my $node = shift;
+ my $lfname = fetch_file_name($logfiles, $format);
+
+ my $max_attempts = 180 * 10;
+
+ my $logcontents;
+ for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+ {
+ $logcontents = slurp_file($node->data_dir . '/' . $lfname);
+ last if $logcontents =~ m/$pattern/;
+ usleep(100_000);
+ }
+
+ like($logcontents, qr/$pattern/,
+ "found expected log file content for $format");
+
+ # While we're at it, test pg_current_logfile() function
+ is( $node->safe_psql('postgres', "SELECT pg_current_logfile('$format')"),
+ $lfname,
+ "pg_current_logfile() gives correct answer with $format");
+ return;
+}
+
# Set up node with logging collector
my $node = PostgresNode->new('primary');
$node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
+log_destination = 'stderr, csvlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -44,26 +93,12 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
- qr|^stderr log/postgresql-.*log$|,
+ qr|^stderr log/postgresql-.*log
+csvlog log/postgresql-.*csv$|,
'current_logfiles is sane');
-my $lfname = $current_logfiles;
-$lfname =~ s/^stderr //;
-chomp $lfname;
-
-my $first_logfile;
-for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
-{
- $first_logfile = slurp_file($node->data_dir . '/' . $lfname);
- last if $first_logfile =~ m/division by zero/;
- usleep(100_000);
-}
-
-like($first_logfile, qr/division by zero/, 'found expected log file content');
-
-# While we're at it, test pg_current_logfile() function
-is($node->safe_psql('postgres', "SELECT pg_current_logfile('stderr')"),
- $lfname, 'pg_current_logfile() gives correct answer');
+check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
+check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -84,28 +119,14 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
- qr|^stderr log/postgresql-.*log$|,
+ qr|^stderr log/postgresql-.*log
+csvlog log/postgresql-.*csv$|,
'new current_logfiles is sane');
-$lfname = $new_current_logfiles;
-$lfname =~ s/^stderr //;
-chomp $lfname;
-
# Verify that log output gets to this file, too
-
$node->psql('postgres', 'fee fi fo fum');
-my $second_logfile;
-for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
-{
- $second_logfile = slurp_file($node->data_dir . '/' . $lfname);
- last if $second_logfile =~ m/syntax error/;
- usleep(100_000);
-}
-
-like(
- $second_logfile,
- qr/syntax error/,
- 'found expected log file content in new log file');
+check_log_pattern('stderr', $new_current_logfiles, 'syntax error', $node);
+check_log_pattern('csvlog', $new_current_logfiles, 'syntax error', $node);
$node->stop();
On Fri, Sep 10, 2021 at 03:56:18PM +0900, Michael Paquier wrote:
And this part leads me to the attached, where the addition of the JSON
format would result in the addition of a couple of lines.
Okay, I have worked through the first half of the patch set, and
applied the improved versions of 0001 (refactoring of the chunk
protocol) and 0002 (addition of the tests for csvlog).
I have not looked in details at 0003 and 0004 yet. Still, here are
some comments after a quick scan.
+ * elog-internal.h
I'd rather avoid the hyphen, and use elog_internal.h.
+#define FORMATTED_TS_LEN 128
+extern char formatted_start_time[FORMATTED_TS_LEN];
+extern char formatted_log_time[FORMATTED_TS_LEN];
+
+void setup_formatted_log_time(void);
+void setup_formatted_start_time(void);
We could just use a static buffer in each one of those routines, and
return back a pointer to the caller.
+ else if ((Log_destination & LOG_DESTINATION_JSONLOG) &&
+ (jsonlogFile == NULL ||
+ time_based_rotation || (size_rotation_for & LOG_DESTINATION_JSONLOG)))
[...]
+ /* Write to JSON log if enabled */
+ else if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
Those bits in 0004 are wrong. They should be two "if" clauses. This
is leading to issues when setting log_destination to multiple values
with jsonlog in the set of values and logging_connector = on, and the
logs are not getting redirected properly to the .json file. We would
get the data for the .log and .csv files though. This issue also
happens with the original patch set applied on top of e757080. I
think that we should also be more careful with the way we free
StringInfoData in send_message_to_server_log(), or we are going to
finish with unwelcome leaks.
The same coding pattern is now repeated three times in logfile_rotate():
- Check if a logging type is enabled.
- Optionally open new file, with logfile_open().
- Business with ENFILE and EMFILE.
- pfree() and save of the static FILE* ane file name for each type.
I think that we have enough material for a wrapper routine that does
this work, where we pass down LOG_DESTINATION_* and pointers to the
static FILE* and the static last file names. That would have avoided
the bug I found above.
The rebased patch set, that has reworked the tests to be in line with
HEAD, also fails. They compile.
Sehrope, could you adjust that?
--
Michael
Attachments:
v3-0001-Split-csv-handling-in-elog.c-into-separate-csvlog.patchtext/plain; charset=us-asciiDownload
From a08e9df54c5960225055a0c999090dad8cf839be Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Wed, 1 Sep 2021 09:06:15 -0400
Subject: [PATCH v3 1/2] Split csv handling in elog.c into separate csvlog.c
Split out csvlog to its own file and centralize common elog internals
and helpers into its own file as well.
---
src/include/utils/elog-internal.h | 78 ++++++++
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 270 ++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 313 ++----------------------------
4 files changed, 365 insertions(+), 297 deletions(-)
create mode 100644 src/include/utils/elog-internal.h
create mode 100644 src/backend/utils/error/csvlog.c
diff --git a/src/include/utils/elog-internal.h b/src/include/utils/elog-internal.h
new file mode 100644
index 0000000000..ac08b6f12f
--- /dev/null
+++ b/src/include/utils/elog-internal.h
@@ -0,0 +1,78 @@
+/*-------------------------------------------------------------------------
+ *
+ * elog-internal.h
+ * POSTGRES error reporting/logging internal definitions.
+ *
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ * src/include/utils/elog-internal.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ELOG_INTERNAL_H
+#define ELOG_INTERNAL_H
+
+#include "postgres.h"
+
+#include "utils/elog.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "postmaster/bgworker.h"
+
+const char * error_severity(int elevel);
+void write_syslogger(char *data, int len, int dest);
+
+void write_csvlog(ErrorData *edata);
+
+/*
+ * is_log_level_output -- is elevel logically >= log_min_level?
+ *
+ * We use this for tests that should consider LOG to sort out-of-order,
+ * between ERROR and FATAL. Generally this is the right thing for testing
+ * whether a message should go to the postmaster log, whereas a simple >=
+ * test is correct for testing whether the message should go to the client.
+ */
+static inline bool
+is_log_level_output(int elevel, int log_min_level)
+{
+ if (elevel == LOG || elevel == LOG_SERVER_ONLY)
+ {
+ if (log_min_level == LOG || log_min_level <= ERROR)
+ return true;
+ }
+ else if (elevel == WARNING_CLIENT_ONLY)
+ {
+ /* never sent to log, regardless of log_min_level */
+ return false;
+ }
+ else if (log_min_level == LOG)
+ {
+ /* elevel != LOG */
+ if (elevel >= FATAL)
+ return true;
+ }
+ /* Neither is LOG */
+ else if (elevel >= log_min_level)
+ return true;
+
+ return false;
+}
+
+static inline const char *
+get_backend_type_for_log() {
+ if (MyProcPid == PostmasterPid)
+ return "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ return MyBgworkerEntry->bgw_type;
+ else
+ return GetBackendTypeDesc(MyBackendType);
+}
+
+#define FORMATTED_TS_LEN 128
+extern char formatted_start_time[FORMATTED_TS_LEN];
+extern char formatted_log_time[FORMATTED_TS_LEN];
+
+void setup_formatted_log_time(void);
+void setup_formatted_start_time(void);
+
+#endif
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..923b7e7d73
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,270 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog-internal.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ formatted_start_time[0] = '\0';
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and csvlog messages.
+ */
+ if (formatted_log_time[0] == '\0')
+ setup_formatted_log_time();
+
+ appendStringInfoString(&buf, formatted_log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ if (formatted_start_time[0] == '\0')
+ setup_formatted_start_time();
+ appendStringInfoString(&buf, formatted_start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ if (is_log_level_output(edata->elevel, log_min_error_statement) &&
+ debug_query_string != NULL &&
+ !edata->hide_stmt)
+ print_stmt = true;
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ write_syslogger(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 2af87ee3bd..68fb167976 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -82,7 +82,7 @@
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
-
+#include "utils/elog-internal.h"
/* In this module, access gettext() via err_gettext() */
#undef _
@@ -155,9 +155,8 @@ static int recursion_depth = 0; /* to detect actual recursion */
static struct timeval saved_timeval;
static bool saved_timeval_set = false;
-#define FORMATTED_TS_LEN 128
-static char formatted_start_time[FORMATTED_TS_LEN];
-static char formatted_log_time[FORMATTED_TS_LEN];
+char formatted_start_time[FORMATTED_TS_LEN];
+char formatted_log_time[FORMATTED_TS_LEN];
/* Macro for checking errordata_stack_depth is reasonable */
@@ -175,52 +174,13 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
-
-/*
- * is_log_level_output -- is elevel logically >= log_min_level?
- *
- * We use this for tests that should consider LOG to sort out-of-order,
- * between ERROR and FATAL. Generally this is the right thing for testing
- * whether a message should go to the postmaster log, whereas a simple >=
- * test is correct for testing whether the message should go to the client.
- */
-static inline bool
-is_log_level_output(int elevel, int log_min_level)
-{
- if (elevel == LOG || elevel == LOG_SERVER_ONLY)
- {
- if (log_min_level == LOG || log_min_level <= ERROR)
- return true;
- }
- else if (elevel == WARNING_CLIENT_ONLY)
- {
- /* never sent to log, regardless of log_min_level */
- return false;
- }
- else if (log_min_level == LOG)
- {
- /* elevel != LOG */
- if (elevel >= FATAL)
- return true;
- }
- /* Neither is LOG */
- else if (elevel >= log_min_level)
- return true;
-
- return false;
-}
-
/*
* Policy-setting subroutines. These are fairly simple, but it seems wise
* to have the code in just one place.
@@ -2291,7 +2251,7 @@ write_console(const char *line, int len)
/*
* setup formatted_log_time, for consistent times between CSV and regular logs
*/
-static void
+void
setup_formatted_log_time(void)
{
pg_time_t stamp_time;
@@ -2323,7 +2283,7 @@ setup_formatted_log_time(void)
/*
* setup formatted_start_time
*/
-static void
+void
setup_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
@@ -2729,257 +2689,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-static void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
@@ -3276,6 +2985,16 @@ write_pipe_chunks(char *data, int len, int dest)
(void) rc;
}
+void
+write_syslogger(char *data, int len, int dest)
+{
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(data, len, dest);
+ else
+ write_pipe_chunks(data, len, dest);
+}
+
/*
* Append a text string to the error report being built for the client.
@@ -3475,7 +3194,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
--
2.33.0
v3-0002-Add-jsonlog-log_destination-for-JSON-server-logs.patchtext/plain; charset=us-asciiDownload
From 823aab1f809a47f1a525a6ad8ae0366fd07328b6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 13 Sep 2021 10:41:55 +0900
Subject: [PATCH v3 2/2] Add jsonlog log_destination for JSON server logs
Adds option to write server log files as JSON. Each log entry is written as its
own line with any internal newlines escaped as \n. Other non-ASCII and special
characters are also escaped using standard JSON escape sequences.
JSON logging is enabled by setting the GUC log_destination to "jsonlog" and
defaults to a log file with a ".json" extension.
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog-internal.h | 1 +
src/include/utils/elog.h | 1 +
src/backend/postmaster/syslogger.c | 182 +++++++++++++++--
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 26 +++
src/backend/utils/error/jsonlog.c | 312 +++++++++++++++++++++++++++++
src/backend/utils/misc/guc.c | 2 +
src/bin/pg_ctl/t/004_logrotate.pl | 12 +-
doc/src/sgml/config.sgml | 67 ++++++-
11 files changed, 585 insertions(+), 27 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index c79dfbeba2..18448b76e5 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog-internal.h b/src/include/utils/elog-internal.h
index ac08b6f12f..13c217a29d 100644
--- a/src/include/utils/elog-internal.h
+++ b/src/include/utils/elog-internal.h
@@ -23,6 +23,7 @@ const char * error_severity(int elevel);
void write_syslogger(char *data, int len, int dest);
void write_csvlog(ErrorData *edata);
+void write_jsonlog(ErrorData *edata);
/*
* is_log_level_output -- is elevel logically >= log_min_level?
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..c0c699f485 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index bca3883572..c0ce072900 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -275,6 +277,8 @@ SysLoggerMain(int argc, char *argv[])
last_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -361,6 +365,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -411,6 +423,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -420,7 +438,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -626,6 +644,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -723,6 +755,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -745,6 +782,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -772,14 +810,25 @@ syslogger_forkexec(void)
fileno(csvlogFile));
else
strcpy(csvfilenobuf, "-1");
+ if (jsonlogFile != NULL)
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ fileno(jsonlogFile));
+ else
+ strcpy(jsonfilenobuf, "-1");
#else /* WIN32 */
if (csvlogFile != NULL)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
(long) _get_osfhandle(_fileno(csvlogFile)));
else
strcpy(csvfilenobuf, "0");
+ if (jsonlogFile != NULL)
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%ld",
+ (long) _get_osfhandle(_fileno(jsonlogFile)));
+ else
+ strcpy(jsonfilenobuf, "0");
#endif /* WIN32 */
av[ac++] = csvfilenobuf;
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -797,8 +846,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -820,6 +869,12 @@ syslogger_parseArgs(int argc, char *argv[])
csvlogFile = fdopen(fd, "a");
setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
}
+ fd = atoi(*argv++);
+ if (fd != -1)
+ {
+ jsonlogFile = fdopen(fd, "a");
+ setvbuf(jsonlogFile, NULL, PG_IOLBF, 0);
+ }
#else /* WIN32 */
fd = atoi(*argv++);
if (fd != 0)
@@ -841,6 +896,16 @@ syslogger_parseArgs(int argc, char *argv[])
setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
}
}
+ fd = atoi(*argv++);
+ if (fd != 0)
+ {
+ fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
+ if (fd > 0)
+ {
+ jsonlogFile = fdopen(fd, "a");
+ setvbuf(jsonlogFile, NULL, PG_IOLBF, 0);
+ }
+ }
#endif /* WIN32 */
}
#endif /* EXEC_BACKEND */
@@ -890,7 +955,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -912,6 +979,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1091,19 +1160,23 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
+ * If we're told to write to a structured log file, but it's not open, dump the data
+ * to syslogFile (which is always open) instead. This can happen if structured
* output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
+ * logFile. There are also race conditions during a parameter change
+ * whereby backends might send us structured output before we open the logFile or
+ * after we close it. Writing formatted output to the regular log
* file isn't great, but it beats dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1174,7 +1247,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1247,6 +1321,7 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
char *filename;
char *csvfilename = NULL;
+ char *jsonfilename = NULL;
pg_time_t fntime;
FILE *fh;
@@ -1264,6 +1339,9 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
filename = logfile_getname(fntime, NULL);
if (Log_destination & LOG_DESTINATION_CSVLOG)
csvfilename = logfile_getname(fntime, ".csv");
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ jsonfilename = logfile_getname(fntime, ".json");
+
/*
* Decide whether to overwrite or append. We can overwrite if (a)
@@ -1301,6 +1379,8 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
pfree(filename);
if (csvfilename)
pfree(csvfilename);
+ if (jsonfilename)
+ pfree(jsonfilename);
return;
}
@@ -1315,10 +1395,10 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
}
/*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
+ * Same as above, but for a structured file. Note that if LOG_DESTINATION_[STRUCTURED]LOG
+ * was just turned on, we might have to open logFile here though it was
* not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
+ * last_*_file_name will be NULL); that is consistent with the normal
* rules since it's not a time-based rotation.
*/
if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
@@ -1374,11 +1454,66 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
pfree(last_csv_file_name);
last_csv_file_name = NULL;
}
+ else if ((Log_destination & LOG_DESTINATION_JSONLOG) &&
+ (jsonlogFile == NULL ||
+ time_based_rotation || (size_rotation_for & LOG_DESTINATION_JSONLOG)))
+ {
+ if (Log_truncate_on_rotation && time_based_rotation &&
+ last_json_file_name != NULL &&
+ strcmp(jsonfilename, last_json_file_name) != 0)
+ fh = logfile_open(jsonfilename, "w", true);
+ else
+ fh = logfile_open(jsonfilename, "a", true);
+
+ if (!fh)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just
+ * keep using the old file till we manage to get a new one.
+ * Otherwise, assume something's wrong with Log_directory and stop
+ * trying to create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ if (filename)
+ pfree(filename);
+ if (jsonfilename)
+ pfree(jsonfilename);
+ return;
+ }
+
+ if (jsonlogFile != NULL)
+ fclose(jsonlogFile);
+ jsonlogFile = fh;
+
+ /* instead of pfree'ing filename, remember it for next time */
+ if (last_json_file_name != NULL)
+ pfree(last_json_file_name);
+ last_json_file_name = jsonfilename;
+ jsonfilename = NULL;
+ }
+ else if (!(Log_destination & LOG_DESTINATION_JSONLOG) &&
+ jsonlogFile != NULL)
+ {
+ /* JSONLOG was just turned off, so close the old file */
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ if (last_json_file_name != NULL)
+ pfree(last_json_file_name);
+ last_json_file_name = NULL;
+ }
if (filename)
pfree(filename);
if (csvfilename)
pfree(csvfilename);
+ if (jsonfilename)
+ pfree(jsonfilename);
update_metainfo_datafile();
@@ -1466,7 +1601,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1524,6 +1660,18 @@ update_metainfo_datafile(void)
return;
}
}
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 68fb167976..064fbf7e61 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2922,6 +2922,30 @@ send_message_to_server_log(ErrorData *edata)
pfree(buf.data);
}
}
+ /* Write to JSON log if enabled */
+ else if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ /*
+ * send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). First get back the space in the message buffer.
+ */
+ pfree(buf.data);
+ write_jsonlog(edata);
+ }
+ else
+ {
+ /*
+ * syslogger not up (yet), so just dump the message to stderr,
+ * unless we already did so above.
+ */
+ if (!(Log_destination & LOG_DESTINATION_STDERR) &&
+ whereToSendOutput != DestDebug)
+ write_console(buf.data, buf.len);
+ pfree(buf.data);
+ }
+ }
else
{
pfree(buf.data);
@@ -2964,6 +2988,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..2777124977
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,312 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog-internal.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+/*
+ * appendJSONKeyValue
+ * Append to given StringInfo a comma followed by a JSON key and value.
+ * Both the key and value will be escaped as JSON string literals.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value)
+{
+ if (value == NULL)
+ return;
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ escape_json(buf, value);
+}
+
+/*
+ * appendJSONKeyValueFmt
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /*
+ * Allocate result buffer. Note that in frontend this maps to malloc
+ * with exit-on-error.
+ */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+ appendJSONKeyValue(buf, key, value);
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a signed integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%d", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as an unsigned integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsUInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%u", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a long (a JSON number).
+ */
+static void
+appendJSONKeyValueAsLong(StringInfo buf, const char *key, long value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%ld", value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ formatted_start_time[0] = '\0';
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and jsonlog messages.
+ */
+ if (formatted_log_time[0] == '\0')
+ setup_formatted_log_time();
+
+ /* First property does not use appendJSONKeyValue as it does not have comma prefix */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, formatted_log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueAsInt(&buf, "leader_pid", leader->pid);
+ }
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueAsInt(&buf, "pid", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueAsLong(&buf, "line_num", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ if (formatted_start_time[0] == '\0')
+ setup_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", formatted_start_time);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity", (char *) error_severity(edata->elevel));
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", "%lld", (long long) pgstat_get_my_query_id());
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code", unpack_sql_state(edata->sqlerrcode));
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log);
+ else if (edata->detail)
+ appendJSONKeyValue(&buf, "detail", edata->detail);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint);
+
+ /* Internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery);
+
+ /* If the internal query got printed, print internal pos, too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueAsUInt(&buf, "internal_position", edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (is_log_level_output(edata->elevel, log_min_error_statement) &&
+ debug_query_string != NULL &&
+ !edata->hide_stmt)
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueAsInt(&buf, "cursor_position", edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename);
+ appendJSONKeyValueAsInt(&buf, "file_line_num", edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log());
+
+ /* Error message */
+ appendJSONKeyValue(&buf, "message", edata->message);
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ write_syslogger(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 23236fa4c3..e38a312b53 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -11702,6 +11702,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index aa0d64a4f7..c6f03f1047 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 10;
+use Test::More tests => 15;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -63,7 +63,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -94,11 +94,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -120,7 +122,8 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
@@ -128,5 +131,6 @@ $node->psql('postgres', 'fee fi fo fum');
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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ef0e2a7746..c1544cab26 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5926,7 +5926,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5944,6 +5945,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ CSV-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5955,13 +5964,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6101,6 +6111,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7433,6 +7450,50 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ Each log line is serialized as a JSON object with the following fields:
+<programlisting>
+ {
+ "timestamp": time stamp with milliseconds (string),
+ "user": user name (string),
+ "dbname": database name (string),
+ "pid": process ID (number),
+ "remote_host": client host (string)
+ "remote_port": port number (string),
+ "session_id": session ID (string),
+ "line_num": per-session line number (number),
+ "ps": current ps display (string),
+ "session_start": session start time (string),
+ "vxid": virtual transaction ID (string),
+ "txid": regular transaction ID (string),
+ "error_severity": error severity (string),
+ "state_code": SQLSTATE code (string),
+ "detail": error message detail (string),
+ "hint": hint (string),
+ "internal_query": internal query that led to the error (string),
+ "internal_position": cursor index into internal query (number),
+ "context": error context (string),
+ "statement": client supplied query string (string),
+ "cursor_position": cursor index into query string (string),
+ "func_name": error location function name (string),
+ "file_name": error location file name (string),
+ "file_line_num": error location file line number (number),
+ "application_name": client application name (string),
+ "message": error message (string)
+ }
+</programlisting>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.33.0
On Sun, Sep 12, 2021 at 10:22 PM Michael Paquier <michael@paquier.xyz>
wrote:
On Fri, Sep 10, 2021 at 03:56:18PM +0900, Michael Paquier wrote:
And this part leads me to the attached, where the addition of the JSON
format would result in the addition of a couple of lines.Okay, I have worked through the first half of the patch set, and
applied the improved versions of 0001 (refactoring of the chunk
protocol) and 0002 (addition of the tests for csvlog).
Thanks. I finally got a chance to look through those changes. I like it.
The popcount and pulling out the flags are much easier to follow than the
old hard coded letters.
I have not looked in details at 0003 and 0004 yet. Still, here are
some comments after a quick scan.+ * elog-internal.h
I'd rather avoid the hyphen, and use elog_internal.h.+#define FORMATTED_TS_LEN 128 +extern char formatted_start_time[FORMATTED_TS_LEN]; +extern char formatted_log_time[FORMATTED_TS_LEN]; + +void setup_formatted_log_time(void); +void setup_formatted_start_time(void); We could just use a static buffer in each one of those routines, and return back a pointer to the caller.
+1
+ else if ((Log_destination & LOG_DESTINATION_JSONLOG) && + (jsonlogFile == NULL || + time_based_rotation || (size_rotation_for & LOG_DESTINATION_JSONLOG))) [...] + /* Write to JSON log if enabled */ + else if (Log_destination & LOG_DESTINATION_JSONLOG) + { Those bits in 0004 are wrong. They should be two "if" clauses. This is leading to issues when setting log_destination to multiple values with jsonlog in the set of values and logging_connector = on, and the logs are not getting redirected properly to the .json file. We would get the data for the .log and .csv files though. This issue also happens with the original patch set applied on top of e757080. I think that we should also be more careful with the way we free StringInfoData in send_message_to_server_log(), or we are going to finish with unwelcome leaks.
Good catch. Staring at that piece again, that's tricky as it tries to
aggressively free the buffer before calling write_cvslog(...). Which can't
just be duplicated for additional destinations.
I think we need to pull up the negative case (i.e. syslogger not available)
before the other destinations and if it matches, free and exit early.
Otherwise, free the buffer and call whatever destination routines are
enabled.
The same coding pattern is now repeated three times in logfile_rotate():
- Check if a logging type is enabled.
- Optionally open new file, with logfile_open().
- Business with ENFILE and EMFILE.
- pfree() and save of the static FILE* ane file name for each type.
I think that we have enough material for a wrapper routine that does
this work, where we pass down LOG_DESTINATION_* and pointers to the
static FILE* and the static last file names. That would have avoided
the bug I found above.
I started on a bit of this as well. There's so much overlap already between
the syslog_ and csvlog code that I'm going to put that together first. Then
the json addition should just fall into a couple of call sites.
I'm thinking of adding an internal struct to house the FILE* and file
names. Then all the opening, closing, and log rotation code can pass
pointers to those and centralize the pfree() and NULL checks.
The rebased patch set, that has reworked the tests to be in line with
HEAD, also fails. They compile.Sehrope, could you adjust that?
Yes I'm looking to sync those up and address the above later this week.
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
On Mon, Sep 13, 2021 at 11:56:52PM -0400, Sehrope Sarkuni wrote:
Good catch. Staring at that piece again, that's tricky as it tries to
aggressively free the buffer before calling write_cvslog(...). Which can't
just be duplicated for additional destinations.I think we need to pull up the negative case (i.e. syslogger not available)
before the other destinations and if it matches, free and exit early.
Otherwise, free the buffer and call whatever destination routines are
enabled.
Yes, I got a similar impression.
I started on a bit of this as well. There's so much overlap already between
the syslog_ and csvlog code that I'm going to put that together first. Then
the json addition should just fall into a couple of call sites.I'm thinking of adding an internal struct to house the FILE* and file
names. Then all the opening, closing, and log rotation code can pass
pointers to those and centralize the pfree() and NULL checks.
Agreed on both points (using a structure and doing the refactoring as
a first patch).
--
Michael
Attached three patches refactor the syslogger handling of file based
destinations a bit ... and then a lot.
First patch adds a new constant to represent both file based destinations.
This should make it easier to ensure additional destinations are handled in
"For all file destinations..." situations (e.g. when we add the jsonlog
destination).
Second patch refactors the file descriptor serialization and re-opening in
EXEC_BACKEND forking. Previously the code was duplicated for both stderr
and csvlog. Again, this should simplify adding new destinations as they'd
just invoke the same helper. There's an existing comment about not handling
failed opens in syslogger_parseArgs(...) and this patch doesn't fix that,
but it does provide a single location to do so in the future.
The third patch adds a new internal (to syslogger.c) structure,
FileLogDestination, for file based log destinations and modifies the
existing handling for syslogFile and csvlogFile to defer to a bunch of
helper functions. It also renames "syslogFile" to "stderr_file_info".
Working through this patch, it was initially confusing that the stderr log
file was named "syslogFile", the C file is named syslogger.c, and there's
an entirely separate syslog (the message logging API) destination. I think
this clears that up a bit.
The patches pass check-world on Linux. I haven't tested it on Windows but
it does pass check-world with EXEC_BACKEND defined to try out the forking
code paths. Definitely needs some review to ensure it's functionally
correct for the different log files.
I haven't tried splitting the csvlog code out or adding the new jsonlog
atop this yet. There's enough changes here that I'd like to get this
settled first.
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
Attachments:
v4-0001-Add-constant-for-list-of-log-destinations-that-use-f.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Add-constant-for-list-of-log-destinations-that-use-f.patchDownload
From bd5a4ff0435c721de3e7eb9b9207d9e96d79baf4 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Thu, 16 Sep 2021 14:43:31 -0400
Subject: [PATCH 1/3] Add constant for list of log destinations that use files
---
src/backend/postmaster/syslogger.c | 5 ++---
src/include/utils/elog.h | 3 +++
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index bca3883572..bc546af7ff 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -420,7 +420,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATIONS_WITH_FILES;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -1465,8 +1465,7 @@ update_metainfo_datafile(void)
FILE *fh;
mode_t oumask;
- if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ if (!(Log_destination & LOG_DESTINATIONS_WITH_FILES))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..bea8b93da6 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -437,6 +437,9 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+/* Log destinations with file handles */
+#define LOG_DESTINATIONS_WITH_FILES (LOG_DESTINATION_CSVLOG | LOG_DESTINATION_STDERR)
+
/* Other exported functions */
extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
--
2.17.1
v4-0002-Split-out-syslogger-EXEC_BACKEND-fd-serialization-an.patchtext/x-patch; charset=US-ASCII; name=v4-0002-Split-out-syslogger-EXEC_BACKEND-fd-serialization-an.patchDownload
From 653ea6865ce81de89bec4fc41f3c0be2933e6b2b Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Thu, 16 Sep 2021 15:57:00 -0400
Subject: [PATCH 2/3] Split out syslogger EXEC_BACKEND fd serialization and
opening into helper functions
---
src/backend/postmaster/syslogger.c | 109 ++++++++++++-----------------
1 file changed, 44 insertions(+), 65 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index bc546af7ff..4f0477794e 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -730,9 +730,23 @@ SysLogger_Start(void)
return 0;
}
-
#ifdef EXEC_BACKEND
+static long syslogger_get_fileno(FILE *file)
+{
+#ifndef WIN32
+ if (file != NULL)
+ return (long) fileno(file);
+ else
+ return -1;
+#else /* WIN32 */
+ if (file != NULL)
+ return (long) _get_osfhandle(_fileno(file));
+ else
+ return 0;
+#endif /* WIN32 */
+}
+
/*
* syslogger_forkexec() -
*
@@ -751,34 +765,9 @@ syslogger_forkexec(void)
av[ac++] = NULL; /* filled in by postmaster_forkexec */
/* static variables (those not passed by write_backend_variables) */
-#ifndef WIN32
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%d",
- fileno(syslogFile));
- else
- strcpy(filenobuf, "-1");
-#else /* WIN32 */
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%ld",
- (long) _get_osfhandle(_fileno(syslogFile)));
- else
- strcpy(filenobuf, "0");
-#endif /* WIN32 */
+ snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(syslogFile));
av[ac++] = filenobuf;
-
-#ifndef WIN32
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
- fileno(csvlogFile));
- else
- strcpy(csvfilenobuf, "-1");
-#else /* WIN32 */
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
- (long) _get_osfhandle(_fileno(csvlogFile)));
- else
- strcpy(csvfilenobuf, "0");
-#endif /* WIN32 */
+ snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlogFile));
av[ac++] = csvfilenobuf;
av[ac] = NULL;
@@ -787,6 +776,31 @@ syslogger_forkexec(void)
return postmaster_forkexec(ac, av);
}
+static FILE* syslogger_fdopen(int fd)
+{
+ FILE *file = NULL;
+
+#ifndef WIN32
+ if (fd != -1)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+#else /* WIN32 */
+ if (fd != 0)
+ {
+ fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
+ if (fd > 0)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+ }
+#endif /* WIN32 */
+
+ return file;
+}
+
/*
* syslogger_parseArgs() -
*
@@ -795,8 +809,6 @@ syslogger_forkexec(void)
static void
syslogger_parseArgs(int argc, char *argv[])
{
- int fd;
-
Assert(argc == 5);
argv += 3;
@@ -807,41 +819,8 @@ syslogger_parseArgs(int argc, char *argv[])
* fails there's not a lot we can do to report the problem anyway. As
* coded, we'll just crash on a null pointer dereference after failure...
*/
-#ifndef WIN32
- fd = atoi(*argv++);
- if (fd != -1)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
- fd = atoi(*argv++);
- if (fd != -1)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
-#else /* WIN32 */
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
- }
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
- }
-#endif /* WIN32 */
+ syslogFile = syslogger_fdopen(atoi(*argv++));
+ csvlogFile = syslogger_fdopen(atoi(*argv++));
}
#endif /* EXEC_BACKEND */
--
2.17.1
v4-0003-Refactor-syslogger-to-consolidate-common-tasks-for-f.patchtext/x-patch; charset=US-ASCII; name=v4-0003-Refactor-syslogger-to-consolidate-common-tasks-for-f.patchDownload
From 99eb9e6b69a75fec3356696d10910c4cf89dae57 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Thu, 16 Sep 2021 16:14:04 -0400
Subject: [PATCH 3/3] Refactor syslogger to consolidate common tasks for file
based destinations
---
src/backend/postmaster/syslogger.c | 415 ++++++++++++++---------------
1 file changed, 199 insertions(+), 216 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 4f0477794e..0f5d3b7862 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -84,11 +84,28 @@ extern bool redirection_done;
static pg_time_t next_rotation_time;
static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
-static FILE *syslogFile = NULL;
-static FILE *csvlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
-static char *last_file_name = NULL;
-static char *last_csv_file_name = NULL;
+
+typedef struct
+{
+ FILE *file;
+ char *last_file_name;
+ const int dest;
+ const char* name;
+ const char* suffix;
+} FileLogDestination;
+
+static FileLogDestination stderr_file_info = {NULL, NULL, LOG_DESTINATION_STDERR, "stderr", NULL};
+static FileLogDestination csvlog_file_info = {NULL, NULL, LOG_DESTINATION_CSVLOG, "csvlog", ".csv"};
+
+static inline void file_log_dest_initial_open(FileLogDestination *info);
+static inline void file_log_dest_initial_set_last_file_name(FileLogDestination *info);
+static inline void file_log_dest_check_rotate_for_open(FileLogDestination *info);
+static inline void file_log_dest_check_rotate_for_size(FileLogDestination *info, int *p_size_rotation_for);
+static inline void file_log_dest_close(FileLogDestination *info);
+static inline bool file_log_dest_should_rotate_for_size(FileLogDestination *info);
+static inline bool file_log_dest_rotate(FileLogDestination *info, pg_time_t fntime, bool time_based_rotation, int size_rotation_for);
+static bool inline file_log_dest_write_metadata(FileLogDestination *info, FILE *metadata_file);
/*
* Buffers for saving partial messages from different backends.
@@ -272,9 +289,8 @@ SysLoggerMain(int argc, char *argv[])
* time because passing down just the pg_time_t is a lot cheaper than
* passing a whole file path in the EXEC_BACKEND case.
*/
- last_file_name = logfile_getname(first_syslogger_file_time, NULL);
- if (csvlogFile != NULL)
- last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ file_log_dest_initial_set_last_file_name(&stderr_file_info);
+ file_log_dest_initial_set_last_file_name(&csvlog_file_info);
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -353,13 +369,7 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
}
- /*
- * Force a rotation if CSVLOG output was just turned on or off and
- * we need to open or close csvlogFile accordingly.
- */
- if (((Log_destination & LOG_DESTINATION_CSVLOG) != 0) !=
- (csvlogFile != NULL))
- rotation_requested = true;
+ file_log_dest_check_rotate_for_open(&csvlog_file_info);
/*
* If rotation time parameter changed, reset next rotation time,
@@ -399,18 +409,8 @@ SysLoggerMain(int argc, char *argv[])
if (!rotation_requested && Log_RotationSize > 0 && !rotation_disabled)
{
- /* Do a rotation if file is too big */
- if (ftell(syslogFile) >= Log_RotationSize * 1024L)
- {
- rotation_requested = true;
- size_rotation_for |= LOG_DESTINATION_STDERR;
- }
- if (csvlogFile != NULL &&
- ftell(csvlogFile) >= Log_RotationSize * 1024L)
- {
- rotation_requested = true;
- size_rotation_for |= LOG_DESTINATION_CSVLOG;
- }
+ file_log_dest_check_rotate_for_size(&stderr_file_info, &size_rotation_for);
+ file_log_dest_check_rotate_for_size(&csvlog_file_info, &size_rotation_for);
}
if (rotation_requested)
@@ -541,7 +541,6 @@ int
SysLogger_Start(void)
{
pid_t sysloggerPid;
- char *filename;
if (!Logging_collector)
return 0;
@@ -606,25 +605,10 @@ SysLogger_Start(void)
*/
first_syslogger_file_time = time(NULL);
- filename = logfile_getname(first_syslogger_file_time, NULL);
-
- syslogFile = logfile_open(filename, "a", false);
-
- pfree(filename);
-
- /*
- * Likewise for the initial CSV log file, if that's enabled. (Note that
- * we open syslogFile even when only CSV output is nominally enabled,
- * since some code paths will write to syslogFile anyway.)
- */
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- {
- filename = logfile_getname(first_syslogger_file_time, ".csv");
-
- csvlogFile = logfile_open(filename, "a", false);
-
- pfree(filename);
- }
+ /* stderr file will always be opened */
+ file_log_dest_initial_open(&stderr_file_info);
+ /* other files are opened if destination is enabled */
+ file_log_dest_initial_open(&csvlog_file_info);
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
@@ -716,13 +700,8 @@ SysLogger_Start(void)
}
/* postmaster will never write the file(s); close 'em */
- fclose(syslogFile);
- syslogFile = NULL;
- if (csvlogFile != NULL)
- {
- fclose(csvlogFile);
- csvlogFile = NULL;
- }
+ file_log_dest_close(&stderr_file_info);
+ file_log_dest_close(&csvlog_file_info);
return (int) sysloggerPid;
}
@@ -765,9 +744,9 @@ syslogger_forkexec(void)
av[ac++] = NULL; /* filled in by postmaster_forkexec */
/* static variables (those not passed by write_backend_variables) */
- snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(syslogFile));
+ snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(stderr_file_info->file));
av[ac++] = filenobuf;
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlogFile));
+ snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlog_file_info->file));
av[ac++] = csvfilenobuf;
av[ac] = NULL;
@@ -819,8 +798,8 @@ syslogger_parseArgs(int argc, char *argv[])
* fails there's not a lot we can do to report the problem anyway. As
* coded, we'll just crash on a null pointer dereference after failure...
*/
- syslogFile = syslogger_fdopen(atoi(*argv++));
- csvlogFile = syslogger_fdopen(atoi(*argv++));
+ stderr_file_info->file = syslogger_fdopen(atoi(*argv++));
+ csvlog_file_info->file = syslogger_fdopen(atoi(*argv++));
}
#endif /* EXEC_BACKEND */
@@ -1050,6 +1029,26 @@ flush_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
*bytes_in_logbuffer = 0;
}
+static inline FILE* get_syslogger_file(int dest)
+{
+ /*
+ * If we're told to write to a logfile, but it's not open, dump the data
+ * to stderr (which is always open) instead. This can happen if the
+ * output is enabled after postmaster start and we've been unable to open
+ * outputs's file. There are also race conditions during a parameter change
+ * whereby backends might send us the output before we open the log file or
+ * after we close it. Writing formatted output to the regular log
+ * file isn't great, but it beats dropping log output on the floor.
+ *
+ * Think not to improve this by trying to open a logfile on-the-fly. Any
+ * failure in that would lead to recursion.
+ */
+ if (dest == LOG_DESTINATION_CSVLOG && csvlog_file_info.file != NULL) {
+ return csvlog_file_info.file;
+ }
+ return stderr_file_info.file;
+}
+
/* --------------------------------
* logfile routines
@@ -1067,24 +1066,8 @@ void
write_syslogger_file(const char *buffer, int count, int destination)
{
int rc;
- FILE *logfile;
- /*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
- *
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
- * failure in that would lead to recursion.
- */
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
-
- rc = fwrite(buffer, 1, count, logfile);
+ rc = fwrite(buffer, 1, count, get_syslogger_file(destination));
/*
* Try to report any failure. We mustn't use ereport because it would
@@ -1152,8 +1135,8 @@ pipeThread(void *arg)
*/
if (Log_RotationSize > 0)
{
- if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ if (file_log_dest_should_rotate_for_size(&stderr_file_info) ||
+ file_log_dest_should_rotate_for_size(&csvlog_file_info))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1224,10 +1207,7 @@ logfile_open(const char *filename, const char *mode, bool allow_errors)
static void
logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
- char *filename;
- char *csvfilename = NULL;
pg_time_t fntime;
- FILE *fh;
rotation_requested = false;
@@ -1240,124 +1220,15 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
fntime = next_rotation_time;
else
fntime = time(NULL);
- filename = logfile_getname(fntime, NULL);
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- csvfilename = logfile_getname(fntime, ".csv");
-
- /*
- * Decide whether to overwrite or append. We can overwrite if (a)
- * Log_truncate_on_rotation is set, (b) the rotation was triggered by
- * elapsed time and not something else, and (c) the computed file name is
- * different from what we were previously logging into.
- *
- * Note: last_file_name should never be NULL here, but if it is, append.
- */
- if (time_based_rotation || (size_rotation_for & LOG_DESTINATION_STDERR))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_file_name != NULL &&
- strcmp(filename, last_file_name) != 0)
- fh = logfile_open(filename, "w", true);
- else
- fh = logfile_open(filename, "a", true);
-
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- fclose(syslogFile);
- syslogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_file_name != NULL)
- pfree(last_file_name);
- last_file_name = filename;
- filename = NULL;
- }
-
- /*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
- * not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
- * rules since it's not a time-based rotation.
- */
- if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
- (csvlogFile == NULL ||
- time_based_rotation || (size_rotation_for & LOG_DESTINATION_CSVLOG)))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_csv_file_name != NULL &&
- strcmp(csvfilename, last_csv_file_name) != 0)
- fh = logfile_open(csvfilename, "w", true);
- else
- fh = logfile_open(csvfilename, "a", true);
-
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- if (csvlogFile != NULL)
- fclose(csvlogFile);
- csvlogFile = fh;
- /* instead of pfree'ing filename, remember it for next time */
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = csvfilename;
- csvfilename = NULL;
- }
- else if (!(Log_destination & LOG_DESTINATION_CSVLOG) &&
- csvlogFile != NULL)
- {
- /* CSVLOG was just turned off, so close the old file */
- fclose(csvlogFile);
- csvlogFile = NULL;
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = NULL;
+ if (!file_log_dest_rotate(&stderr_file_info, fntime, time_based_rotation, size_rotation_for) ||
+ !file_log_dest_rotate(&csvlog_file_info, fntime, time_based_rotation, size_rotation_for)) {
+ /* We failed to open a new log file so try again later */
+ return;
}
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
+ if (!(Log_destination & LOG_DESTINATION_CSVLOG))
+ file_log_dest_close(&csvlog_file_info);
update_metainfo_datafile();
@@ -1477,31 +1348,13 @@ update_metainfo_datafile(void)
return;
}
- if (last_file_name && (Log_destination & LOG_DESTINATION_STDERR))
+ if (!file_log_dest_write_metadata(&stderr_file_info, fh) ||
+ !file_log_dest_write_metadata(&csvlog_file_info, fh))
{
- if (fprintf(fh, "stderr %s\n", last_file_name) < 0)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not write file \"%s\": %m",
- LOG_METAINFO_DATAFILE_TMP)));
- fclose(fh);
- return;
- }
+ fclose(fh);
+ return;
}
- if (last_csv_file_name && (Log_destination & LOG_DESTINATION_CSVLOG))
- {
- if (fprintf(fh, "csvlog %s\n", last_csv_file_name) < 0)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not write file \"%s\": %m",
- LOG_METAINFO_DATAFILE_TMP)));
- fclose(fh);
- return;
- }
- }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
@@ -1551,3 +1404,133 @@ sigUsr1Handler(SIGNAL_ARGS)
errno = save_errno;
}
+
+static inline void file_log_dest_initial_open(FileLogDestination *info)
+{
+ /* We always open stderr. Otherwise only open if enabled. */
+ if (info->dest == LOG_DESTINATION_STDERR || (Log_destination & info->dest))
+ {
+ char *filename = logfile_getname(first_syslogger_file_time, info->suffix);
+ info->file = logfile_open(filename, "a", false);
+ pfree(filename);
+ }
+}
+
+static inline void file_log_dest_initial_set_last_file_name(FileLogDestination *info)
+{
+ if (info->file != NULL)
+ info->last_file_name = logfile_getname(first_syslogger_file_time, info->suffix);
+}
+
+static inline void file_log_dest_check_rotate_for_open(FileLogDestination *info)
+{
+ /*
+ * Force a rotation if a destination was just turned on or off and
+ * we need to open or close its file accordingly.
+ */
+ if (((Log_destination & info->dest) != 0) != (info->file != NULL))
+ rotation_requested = true;
+}
+
+static inline void file_log_dest_check_rotate_for_size(FileLogDestination *info, int *p_size_rotation_for)
+{
+ if (info->file == NULL) {
+ return;
+ }
+ /* Do a rotation if file is too big */
+ if (ftell(info->file) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ *p_size_rotation_for |= info->dest;
+ }
+}
+
+static inline void file_log_dest_close(FileLogDestination *info)
+{
+ if (info->file != NULL)
+ {
+ fclose(info->file);
+ info->file = NULL;
+ }
+ if (info->last_file_name != NULL)
+ {
+ pfree(info->last_file_name);
+ info->last_file_name = NULL;
+ }
+}
+
+static inline bool file_log_dest_should_rotate_for_size(FileLogDestination *info)
+{
+ if (info->file == NULL)
+ return false;
+
+ return ftell(info->file) >= Log_RotationSize * 1024L;
+}
+
+static inline bool file_log_dest_rotate(FileLogDestination *info, pg_time_t fntime, bool time_based_rotation, int size_rotation_for)
+{
+ /*
+ * Decide whether to overwrite or append. We can overwrite if (a)
+ * Log_truncate_on_rotation is set, (b) the rotation was triggered by
+ * elapsed time and not something else, and (c) the computed file name is
+ * different from what we were previously logging into.
+ *
+ * Note: last_file_name should never be NULL here, but if it is, append.
+ */
+ if (time_based_rotation || (size_rotation_for & info->dest))
+ {
+ FILE *file = NULL;
+ char *filename = logfile_getname(fntime, info->suffix);
+
+ if (Log_truncate_on_rotation
+ && time_based_rotation
+ && info->last_file_name != NULL
+ && strcmp(filename, info->last_file_name) != 0)
+ file = logfile_open(filename, "w", true);
+ else
+ file = logfile_open(filename, "a", true);
+
+ if (file == NULL)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just
+ * keep using the old file till we manage to get a new one.
+ * Otherwise, assume something's wrong with Log_directory and stop
+ * trying to create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ if (filename)
+ pfree(filename);
+ return false;
+ }
+
+ file_log_dest_close(info);
+ info->file = file;
+ /* instead of pfree'ing filename, remember it for next time */
+ info->last_file_name = filename;
+ }
+
+ return true;
+}
+
+static bool inline file_log_dest_write_metadata(FileLogDestination *info, FILE *metadata_file)
+{
+ if (info->last_file_name != NULL && (Log_destination & info->dest))
+ {
+ if (fprintf(metadata_file, "%s %s\n", info->name, info->last_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ return false;
+ }
+ }
+ return true;
+}
--
2.17.1
The previous patches failed on the cfbot Appveyor Windows build. That also
makes me question whether I did the EXEC_BACKEND testing correctly as it
should have caught that compile error. I'm going to look into that.
In the meantime the attached should correct that part of the build.
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
Show quoted text
Attachments:
v5-0001-Add-constant-for-list-of-log-destinations-that-use-f.patchtext/x-patch; charset=US-ASCII; name=v5-0001-Add-constant-for-list-of-log-destinations-that-use-f.patchDownload
From 0f7978566b7770465060cd497a8dd4102b313878 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Thu, 16 Sep 2021 14:43:31 -0400
Subject: [PATCH 1/3] Add constant for list of log destinations that use files
---
src/backend/postmaster/syslogger.c | 5 ++---
src/include/utils/elog.h | 3 +++
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index bca3883572..bc546af7ff 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -420,7 +420,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATIONS_WITH_FILES;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -1465,8 +1465,7 @@ update_metainfo_datafile(void)
FILE *fh;
mode_t oumask;
- if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ if (!(Log_destination & LOG_DESTINATIONS_WITH_FILES))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..bea8b93da6 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -437,6 +437,9 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+/* Log destinations with file handles */
+#define LOG_DESTINATIONS_WITH_FILES (LOG_DESTINATION_CSVLOG | LOG_DESTINATION_STDERR)
+
/* Other exported functions */
extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
--
2.25.1
v5-0002-Split-out-syslogger-EXEC_BACKEND-fd-serialization-an.patchtext/x-patch; charset=US-ASCII; name=v5-0002-Split-out-syslogger-EXEC_BACKEND-fd-serialization-an.patchDownload
From a22760648dad3832869b8c67886826e7cd306e52 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Thu, 16 Sep 2021 15:57:00 -0400
Subject: [PATCH 2/3] Split out syslogger EXEC_BACKEND fd serialization and
opening into helper functions
---
src/backend/postmaster/syslogger.c | 109 ++++++++++++-----------------
1 file changed, 44 insertions(+), 65 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index bc546af7ff..4f0477794e 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -730,9 +730,23 @@ SysLogger_Start(void)
return 0;
}
-
#ifdef EXEC_BACKEND
+static long syslogger_get_fileno(FILE *file)
+{
+#ifndef WIN32
+ if (file != NULL)
+ return (long) fileno(file);
+ else
+ return -1;
+#else /* WIN32 */
+ if (file != NULL)
+ return (long) _get_osfhandle(_fileno(file));
+ else
+ return 0;
+#endif /* WIN32 */
+}
+
/*
* syslogger_forkexec() -
*
@@ -751,34 +765,9 @@ syslogger_forkexec(void)
av[ac++] = NULL; /* filled in by postmaster_forkexec */
/* static variables (those not passed by write_backend_variables) */
-#ifndef WIN32
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%d",
- fileno(syslogFile));
- else
- strcpy(filenobuf, "-1");
-#else /* WIN32 */
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%ld",
- (long) _get_osfhandle(_fileno(syslogFile)));
- else
- strcpy(filenobuf, "0");
-#endif /* WIN32 */
+ snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(syslogFile));
av[ac++] = filenobuf;
-
-#ifndef WIN32
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
- fileno(csvlogFile));
- else
- strcpy(csvfilenobuf, "-1");
-#else /* WIN32 */
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
- (long) _get_osfhandle(_fileno(csvlogFile)));
- else
- strcpy(csvfilenobuf, "0");
-#endif /* WIN32 */
+ snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlogFile));
av[ac++] = csvfilenobuf;
av[ac] = NULL;
@@ -787,6 +776,31 @@ syslogger_forkexec(void)
return postmaster_forkexec(ac, av);
}
+static FILE* syslogger_fdopen(int fd)
+{
+ FILE *file = NULL;
+
+#ifndef WIN32
+ if (fd != -1)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+#else /* WIN32 */
+ if (fd != 0)
+ {
+ fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
+ if (fd > 0)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+ }
+#endif /* WIN32 */
+
+ return file;
+}
+
/*
* syslogger_parseArgs() -
*
@@ -795,8 +809,6 @@ syslogger_forkexec(void)
static void
syslogger_parseArgs(int argc, char *argv[])
{
- int fd;
-
Assert(argc == 5);
argv += 3;
@@ -807,41 +819,8 @@ syslogger_parseArgs(int argc, char *argv[])
* fails there's not a lot we can do to report the problem anyway. As
* coded, we'll just crash on a null pointer dereference after failure...
*/
-#ifndef WIN32
- fd = atoi(*argv++);
- if (fd != -1)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
- fd = atoi(*argv++);
- if (fd != -1)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
-#else /* WIN32 */
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
- }
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
- }
-#endif /* WIN32 */
+ syslogFile = syslogger_fdopen(atoi(*argv++));
+ csvlogFile = syslogger_fdopen(atoi(*argv++));
}
#endif /* EXEC_BACKEND */
--
2.25.1
v5-0003-Refactor-syslogger-to-consolidate-common-tasks-for-f.patchtext/x-patch; charset=US-ASCII; name=v5-0003-Refactor-syslogger-to-consolidate-common-tasks-for-f.patchDownload
From b4d56b497ab9412ed5a28bf5422b0905aeb4ad9f Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Thu, 16 Sep 2021 16:14:04 -0400
Subject: [PATCH 3/3] Refactor syslogger to consolidate common tasks for file
based destinations
---
src/backend/postmaster/syslogger.c | 415 ++++++++++++++---------------
1 file changed, 199 insertions(+), 216 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 4f0477794e..720066bdca 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -84,11 +84,28 @@ extern bool redirection_done;
static pg_time_t next_rotation_time;
static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
-static FILE *syslogFile = NULL;
-static FILE *csvlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
-static char *last_file_name = NULL;
-static char *last_csv_file_name = NULL;
+
+typedef struct
+{
+ FILE *file;
+ char *last_file_name;
+ const int dest;
+ const char* name;
+ const char* suffix;
+} FileLogDestination;
+
+static FileLogDestination stderr_file_info = {NULL, NULL, LOG_DESTINATION_STDERR, "stderr", NULL};
+static FileLogDestination csvlog_file_info = {NULL, NULL, LOG_DESTINATION_CSVLOG, "csvlog", ".csv"};
+
+static inline void file_log_dest_initial_open(FileLogDestination *info);
+static inline void file_log_dest_initial_set_last_file_name(FileLogDestination *info);
+static inline void file_log_dest_check_rotate_for_open(FileLogDestination *info);
+static inline void file_log_dest_check_rotate_for_size(FileLogDestination *info, int *p_size_rotation_for);
+static inline void file_log_dest_close(FileLogDestination *info);
+static inline bool file_log_dest_should_rotate_for_size(FileLogDestination *info);
+static inline bool file_log_dest_rotate(FileLogDestination *info, pg_time_t fntime, bool time_based_rotation, int size_rotation_for);
+static bool inline file_log_dest_write_metadata(FileLogDestination *info, FILE *metadata_file);
/*
* Buffers for saving partial messages from different backends.
@@ -272,9 +289,8 @@ SysLoggerMain(int argc, char *argv[])
* time because passing down just the pg_time_t is a lot cheaper than
* passing a whole file path in the EXEC_BACKEND case.
*/
- last_file_name = logfile_getname(first_syslogger_file_time, NULL);
- if (csvlogFile != NULL)
- last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ file_log_dest_initial_set_last_file_name(&stderr_file_info);
+ file_log_dest_initial_set_last_file_name(&csvlog_file_info);
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -353,13 +369,7 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
}
- /*
- * Force a rotation if CSVLOG output was just turned on or off and
- * we need to open or close csvlogFile accordingly.
- */
- if (((Log_destination & LOG_DESTINATION_CSVLOG) != 0) !=
- (csvlogFile != NULL))
- rotation_requested = true;
+ file_log_dest_check_rotate_for_open(&csvlog_file_info);
/*
* If rotation time parameter changed, reset next rotation time,
@@ -399,18 +409,8 @@ SysLoggerMain(int argc, char *argv[])
if (!rotation_requested && Log_RotationSize > 0 && !rotation_disabled)
{
- /* Do a rotation if file is too big */
- if (ftell(syslogFile) >= Log_RotationSize * 1024L)
- {
- rotation_requested = true;
- size_rotation_for |= LOG_DESTINATION_STDERR;
- }
- if (csvlogFile != NULL &&
- ftell(csvlogFile) >= Log_RotationSize * 1024L)
- {
- rotation_requested = true;
- size_rotation_for |= LOG_DESTINATION_CSVLOG;
- }
+ file_log_dest_check_rotate_for_size(&stderr_file_info, &size_rotation_for);
+ file_log_dest_check_rotate_for_size(&csvlog_file_info, &size_rotation_for);
}
if (rotation_requested)
@@ -541,7 +541,6 @@ int
SysLogger_Start(void)
{
pid_t sysloggerPid;
- char *filename;
if (!Logging_collector)
return 0;
@@ -606,25 +605,10 @@ SysLogger_Start(void)
*/
first_syslogger_file_time = time(NULL);
- filename = logfile_getname(first_syslogger_file_time, NULL);
-
- syslogFile = logfile_open(filename, "a", false);
-
- pfree(filename);
-
- /*
- * Likewise for the initial CSV log file, if that's enabled. (Note that
- * we open syslogFile even when only CSV output is nominally enabled,
- * since some code paths will write to syslogFile anyway.)
- */
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- {
- filename = logfile_getname(first_syslogger_file_time, ".csv");
-
- csvlogFile = logfile_open(filename, "a", false);
-
- pfree(filename);
- }
+ /* stderr file will always be opened */
+ file_log_dest_initial_open(&stderr_file_info);
+ /* other files are opened if destination is enabled */
+ file_log_dest_initial_open(&csvlog_file_info);
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
@@ -716,13 +700,8 @@ SysLogger_Start(void)
}
/* postmaster will never write the file(s); close 'em */
- fclose(syslogFile);
- syslogFile = NULL;
- if (csvlogFile != NULL)
- {
- fclose(csvlogFile);
- csvlogFile = NULL;
- }
+ file_log_dest_close(&stderr_file_info);
+ file_log_dest_close(&csvlog_file_info);
return (int) sysloggerPid;
}
@@ -765,9 +744,9 @@ syslogger_forkexec(void)
av[ac++] = NULL; /* filled in by postmaster_forkexec */
/* static variables (those not passed by write_backend_variables) */
- snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(syslogFile));
+ snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(stderr_file_info.file));
av[ac++] = filenobuf;
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlogFile));
+ snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlog_file_info.file));
av[ac++] = csvfilenobuf;
av[ac] = NULL;
@@ -819,8 +798,8 @@ syslogger_parseArgs(int argc, char *argv[])
* fails there's not a lot we can do to report the problem anyway. As
* coded, we'll just crash on a null pointer dereference after failure...
*/
- syslogFile = syslogger_fdopen(atoi(*argv++));
- csvlogFile = syslogger_fdopen(atoi(*argv++));
+ stderr_file_info.file = syslogger_fdopen(atoi(*argv++));
+ csvlog_file_info.file = syslogger_fdopen(atoi(*argv++));
}
#endif /* EXEC_BACKEND */
@@ -1050,6 +1029,26 @@ flush_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
*bytes_in_logbuffer = 0;
}
+static inline FILE* get_syslogger_file(int dest)
+{
+ /*
+ * If we're told to write to a logfile, but it's not open, dump the data
+ * to stderr (which is always open) instead. This can happen if the
+ * output is enabled after postmaster start and we've been unable to open
+ * outputs's file. There are also race conditions during a parameter change
+ * whereby backends might send us the output before we open the log file or
+ * after we close it. Writing formatted output to the regular log
+ * file isn't great, but it beats dropping log output on the floor.
+ *
+ * Think not to improve this by trying to open a logfile on-the-fly. Any
+ * failure in that would lead to recursion.
+ */
+ if (dest == LOG_DESTINATION_CSVLOG && csvlog_file_info.file != NULL) {
+ return csvlog_file_info.file;
+ }
+ return stderr_file_info.file;
+}
+
/* --------------------------------
* logfile routines
@@ -1067,24 +1066,8 @@ void
write_syslogger_file(const char *buffer, int count, int destination)
{
int rc;
- FILE *logfile;
- /*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
- *
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
- * failure in that would lead to recursion.
- */
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
-
- rc = fwrite(buffer, 1, count, logfile);
+ rc = fwrite(buffer, 1, count, get_syslogger_file(destination));
/*
* Try to report any failure. We mustn't use ereport because it would
@@ -1152,8 +1135,8 @@ pipeThread(void *arg)
*/
if (Log_RotationSize > 0)
{
- if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ if (file_log_dest_should_rotate_for_size(&stderr_file_info) ||
+ file_log_dest_should_rotate_for_size(&csvlog_file_info))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1224,10 +1207,7 @@ logfile_open(const char *filename, const char *mode, bool allow_errors)
static void
logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
- char *filename;
- char *csvfilename = NULL;
pg_time_t fntime;
- FILE *fh;
rotation_requested = false;
@@ -1240,124 +1220,15 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
fntime = next_rotation_time;
else
fntime = time(NULL);
- filename = logfile_getname(fntime, NULL);
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- csvfilename = logfile_getname(fntime, ".csv");
-
- /*
- * Decide whether to overwrite or append. We can overwrite if (a)
- * Log_truncate_on_rotation is set, (b) the rotation was triggered by
- * elapsed time and not something else, and (c) the computed file name is
- * different from what we were previously logging into.
- *
- * Note: last_file_name should never be NULL here, but if it is, append.
- */
- if (time_based_rotation || (size_rotation_for & LOG_DESTINATION_STDERR))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_file_name != NULL &&
- strcmp(filename, last_file_name) != 0)
- fh = logfile_open(filename, "w", true);
- else
- fh = logfile_open(filename, "a", true);
-
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- fclose(syslogFile);
- syslogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_file_name != NULL)
- pfree(last_file_name);
- last_file_name = filename;
- filename = NULL;
- }
-
- /*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
- * not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
- * rules since it's not a time-based rotation.
- */
- if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
- (csvlogFile == NULL ||
- time_based_rotation || (size_rotation_for & LOG_DESTINATION_CSVLOG)))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_csv_file_name != NULL &&
- strcmp(csvfilename, last_csv_file_name) != 0)
- fh = logfile_open(csvfilename, "w", true);
- else
- fh = logfile_open(csvfilename, "a", true);
-
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- if (csvlogFile != NULL)
- fclose(csvlogFile);
- csvlogFile = fh;
- /* instead of pfree'ing filename, remember it for next time */
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = csvfilename;
- csvfilename = NULL;
- }
- else if (!(Log_destination & LOG_DESTINATION_CSVLOG) &&
- csvlogFile != NULL)
- {
- /* CSVLOG was just turned off, so close the old file */
- fclose(csvlogFile);
- csvlogFile = NULL;
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = NULL;
+ if (!file_log_dest_rotate(&stderr_file_info, fntime, time_based_rotation, size_rotation_for) ||
+ !file_log_dest_rotate(&csvlog_file_info, fntime, time_based_rotation, size_rotation_for)) {
+ /* We failed to open a new log file so try again later */
+ return;
}
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
+ if (!(Log_destination & LOG_DESTINATION_CSVLOG))
+ file_log_dest_close(&csvlog_file_info);
update_metainfo_datafile();
@@ -1477,31 +1348,13 @@ update_metainfo_datafile(void)
return;
}
- if (last_file_name && (Log_destination & LOG_DESTINATION_STDERR))
+ if (!file_log_dest_write_metadata(&stderr_file_info, fh) ||
+ !file_log_dest_write_metadata(&csvlog_file_info, fh))
{
- if (fprintf(fh, "stderr %s\n", last_file_name) < 0)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not write file \"%s\": %m",
- LOG_METAINFO_DATAFILE_TMP)));
- fclose(fh);
- return;
- }
+ fclose(fh);
+ return;
}
- if (last_csv_file_name && (Log_destination & LOG_DESTINATION_CSVLOG))
- {
- if (fprintf(fh, "csvlog %s\n", last_csv_file_name) < 0)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not write file \"%s\": %m",
- LOG_METAINFO_DATAFILE_TMP)));
- fclose(fh);
- return;
- }
- }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
@@ -1551,3 +1404,133 @@ sigUsr1Handler(SIGNAL_ARGS)
errno = save_errno;
}
+
+static inline void file_log_dest_initial_open(FileLogDestination *info)
+{
+ /* We always open stderr. Otherwise only open if enabled. */
+ if (info->dest == LOG_DESTINATION_STDERR || (Log_destination & info->dest))
+ {
+ char *filename = logfile_getname(first_syslogger_file_time, info->suffix);
+ info->file = logfile_open(filename, "a", false);
+ pfree(filename);
+ }
+}
+
+static inline void file_log_dest_initial_set_last_file_name(FileLogDestination *info)
+{
+ if (info->file != NULL)
+ info->last_file_name = logfile_getname(first_syslogger_file_time, info->suffix);
+}
+
+static inline void file_log_dest_check_rotate_for_open(FileLogDestination *info)
+{
+ /*
+ * Force a rotation if a destination was just turned on or off and
+ * we need to open or close its file accordingly.
+ */
+ if (((Log_destination & info->dest) != 0) != (info->file != NULL))
+ rotation_requested = true;
+}
+
+static inline void file_log_dest_check_rotate_for_size(FileLogDestination *info, int *p_size_rotation_for)
+{
+ if (info->file == NULL) {
+ return;
+ }
+ /* Do a rotation if file is too big */
+ if (ftell(info->file) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ *p_size_rotation_for |= info->dest;
+ }
+}
+
+static inline void file_log_dest_close(FileLogDestination *info)
+{
+ if (info->file != NULL)
+ {
+ fclose(info->file);
+ info->file = NULL;
+ }
+ if (info->last_file_name != NULL)
+ {
+ pfree(info->last_file_name);
+ info->last_file_name = NULL;
+ }
+}
+
+static inline bool file_log_dest_should_rotate_for_size(FileLogDestination *info)
+{
+ if (info->file == NULL)
+ return false;
+
+ return ftell(info->file) >= Log_RotationSize * 1024L;
+}
+
+static inline bool file_log_dest_rotate(FileLogDestination *info, pg_time_t fntime, bool time_based_rotation, int size_rotation_for)
+{
+ /*
+ * Decide whether to overwrite or append. We can overwrite if (a)
+ * Log_truncate_on_rotation is set, (b) the rotation was triggered by
+ * elapsed time and not something else, and (c) the computed file name is
+ * different from what we were previously logging into.
+ *
+ * Note: last_file_name should never be NULL here, but if it is, append.
+ */
+ if (time_based_rotation || (size_rotation_for & info->dest))
+ {
+ FILE *file = NULL;
+ char *filename = logfile_getname(fntime, info->suffix);
+
+ if (Log_truncate_on_rotation
+ && time_based_rotation
+ && info->last_file_name != NULL
+ && strcmp(filename, info->last_file_name) != 0)
+ file = logfile_open(filename, "w", true);
+ else
+ file = logfile_open(filename, "a", true);
+
+ if (file == NULL)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just
+ * keep using the old file till we manage to get a new one.
+ * Otherwise, assume something's wrong with Log_directory and stop
+ * trying to create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ if (filename)
+ pfree(filename);
+ return false;
+ }
+
+ file_log_dest_close(info);
+ info->file = file;
+ /* instead of pfree'ing filename, remember it for next time */
+ info->last_file_name = filename;
+ }
+
+ return true;
+}
+
+static bool inline file_log_dest_write_metadata(FileLogDestination *info, FILE *metadata_file)
+{
+ if (info->last_file_name != NULL && (Log_destination & info->dest))
+ {
+ if (fprintf(metadata_file, "%s %s\n", info->name, info->last_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ return false;
+ }
+ }
+ return true;
+}
--
2.25.1
On Thu, Sep 16, 2021 at 05:27:20PM -0400, Sehrope Sarkuni wrote:
Attached three patches refactor the syslogger handling of file based
destinations a bit ... and then a lot.First patch adds a new constant to represent both file based destinations.
This should make it easier to ensure additional destinations are handled in
"For all file destinations..." situations (e.g. when we add the jsonlog
destination).Second patch refactors the file descriptor serialization and re-opening in
EXEC_BACKEND forking. Previously the code was duplicated for both stderr
and csvlog. Again, this should simplify adding new destinations as they'd
just invoke the same helper. There's an existing comment about not handling
failed opens in syslogger_parseArgs(...) and this patch doesn't fix that,
but it does provide a single location to do so in the future.The third patch adds a new internal (to syslogger.c) structure,
FileLogDestination, for file based log destinations and modifies the
existing handling for syslogFile and csvlogFile to defer to a bunch of
helper functions. It also renames "syslogFile" to "stderr_file_info".
Working through this patch, it was initially confusing that the stderr log
file was named "syslogFile", the C file is named syslogger.c, and there's
an entirely separate syslog (the message logging API) destination. I think
this clears that up a bit.The patches pass check-world on Linux. I haven't tested it on Windows but
it does pass check-world with EXEC_BACKEND defined to try out the forking
code paths. Definitely needs some review to ensure it's functionally
correct for the different log files.
Compilation with EXEC_BACKEND on Linux should work, so no need to
bother with Windows when it comes to that. There is a buildfarm
machine testing this configuration, for example.
I haven't tried splitting the csvlog code out or adding the new jsonlog
atop this yet. There's enough changes here that I'd like to get this
settled first.
Makes sense.
I am not really impressed by 0001 and the introduction of
LOG_DESTINATIONS_WITH_FILES. So I would leave that out and keep the
list of destinations listed instead. And we are talking about two
places here, only within syslogger.c.
0002 looks like an improvement, but I think that 0003 makes the
readability of the code worse with the introduction of eight static
routines to handle all those cases.
file_log_dest_should_rotate_for_size(), file_log_dest_close(),
file_log_dest_check_rotate_for_size(), get_syslogger_file() don't
bring major improvements. On the contrary,
file_log_dest_write_metadata and file_log_dest_rotate seem to reduce a
bit the noise.
--
Michael
On Thu, Sep 16, 2021 at 9:36 PM Michael Paquier <michael@paquier.xyz> wrote:
I am not really impressed by 0001 and the introduction of
LOG_DESTINATIONS_WITH_FILES. So I would leave that out and keep the
list of destinations listed instead. And we are talking about two
places here, only within syslogger.c.
I've taken that out for now. The idea was to simplify the additions when
jsonlog is added but can circle back to that later if it makes sense.
0002 looks like an improvement,
Nice. That's left unchanged (renamed to 0001 now).
but I think that 0003 makes the
readability of the code worse with the introduction of eight static
routines to handle all those cases.file_log_dest_should_rotate_for_size(), file_log_dest_close(),
file_log_dest_check_rotate_for_size(), get_syslogger_file() don't
bring major improvements. On the contrary,
file_log_dest_write_metadata and file_log_dest_rotate seem to reduce a
bit the noise.
It was helpful to split them out while working on the patch but I see your
point upon re-reading through the result.
Attached version (renamed to 002) adds only three static functions for
checking rotation size, performing the actual rotation, and closing. Also
one other to handle generating the logfile names with a suffix to handle
the pfree-ing (wrapping logfile_open(...)).
The rest of the changes happen in place using the new structures.
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
Attachments:
v6-0001-Split-out-syslogger-EXEC_BACKEND-fd-serialization-an.patchtext/x-patch; charset=US-ASCII; name=v6-0001-Split-out-syslogger-EXEC_BACKEND-fd-serialization-an.patchDownload
From c85e15ffc6a8e310c10be4c85580980d04846bf2 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Thu, 16 Sep 2021 15:57:00 -0400
Subject: [PATCH 1/2] Split out syslogger EXEC_BACKEND fd serialization and
opening into helper functions
---
src/backend/postmaster/syslogger.c | 109 ++++++++++++-----------------
1 file changed, 44 insertions(+), 65 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index bca3883572..48b4c48a18 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -730,9 +730,23 @@ SysLogger_Start(void)
return 0;
}
-
#ifdef EXEC_BACKEND
+static long syslogger_get_fileno(FILE *file)
+{
+#ifndef WIN32
+ if (file != NULL)
+ return (long) fileno(file);
+ else
+ return -1;
+#else /* WIN32 */
+ if (file != NULL)
+ return (long) _get_osfhandle(_fileno(file));
+ else
+ return 0;
+#endif /* WIN32 */
+}
+
/*
* syslogger_forkexec() -
*
@@ -751,34 +765,9 @@ syslogger_forkexec(void)
av[ac++] = NULL; /* filled in by postmaster_forkexec */
/* static variables (those not passed by write_backend_variables) */
-#ifndef WIN32
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%d",
- fileno(syslogFile));
- else
- strcpy(filenobuf, "-1");
-#else /* WIN32 */
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%ld",
- (long) _get_osfhandle(_fileno(syslogFile)));
- else
- strcpy(filenobuf, "0");
-#endif /* WIN32 */
+ snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(syslogFile));
av[ac++] = filenobuf;
-
-#ifndef WIN32
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
- fileno(csvlogFile));
- else
- strcpy(csvfilenobuf, "-1");
-#else /* WIN32 */
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
- (long) _get_osfhandle(_fileno(csvlogFile)));
- else
- strcpy(csvfilenobuf, "0");
-#endif /* WIN32 */
+ snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlogFile));
av[ac++] = csvfilenobuf;
av[ac] = NULL;
@@ -787,6 +776,31 @@ syslogger_forkexec(void)
return postmaster_forkexec(ac, av);
}
+static FILE* syslogger_fdopen(int fd)
+{
+ FILE *file = NULL;
+
+#ifndef WIN32
+ if (fd != -1)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+#else /* WIN32 */
+ if (fd != 0)
+ {
+ fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
+ if (fd > 0)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+ }
+#endif /* WIN32 */
+
+ return file;
+}
+
/*
* syslogger_parseArgs() -
*
@@ -795,8 +809,6 @@ syslogger_forkexec(void)
static void
syslogger_parseArgs(int argc, char *argv[])
{
- int fd;
-
Assert(argc == 5);
argv += 3;
@@ -807,41 +819,8 @@ syslogger_parseArgs(int argc, char *argv[])
* fails there's not a lot we can do to report the problem anyway. As
* coded, we'll just crash on a null pointer dereference after failure...
*/
-#ifndef WIN32
- fd = atoi(*argv++);
- if (fd != -1)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
- fd = atoi(*argv++);
- if (fd != -1)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
-#else /* WIN32 */
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
- }
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
- }
-#endif /* WIN32 */
+ syslogFile = syslogger_fdopen(atoi(*argv++));
+ csvlogFile = syslogger_fdopen(atoi(*argv++));
}
#endif /* EXEC_BACKEND */
--
2.17.1
v6-0002-Refactor-syslogger-to-consolidate-common-tasks-for-f.patchtext/x-patch; charset=US-ASCII; name=v6-0002-Refactor-syslogger-to-consolidate-common-tasks-for-f.patchDownload
From f4836bdebdae6008de3be589e253ed64f686b2f6 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Fri, 17 Sep 2021 16:14:38 -0400
Subject: [PATCH 2/2] Refactor syslogger to consolidate common tasks for file
based destinations
---
src/backend/postmaster/syslogger.c | 345 ++++++++++++++---------------
1 file changed, 164 insertions(+), 181 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 48b4c48a18..2c12305992 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -84,11 +84,23 @@ extern bool redirection_done;
static pg_time_t next_rotation_time;
static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
-static FILE *syslogFile = NULL;
-static FILE *csvlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
-static char *last_file_name = NULL;
-static char *last_csv_file_name = NULL;
+
+typedef struct
+{
+ FILE *file;
+ char *last_file_name;
+ const int dest;
+ const char* name;
+ const char* suffix;
+} FileLogDestination;
+
+static FileLogDestination stderr_file_info = {NULL, NULL, LOG_DESTINATION_STDERR, "stderr", NULL};
+static FileLogDestination csvlog_file_info = {NULL, NULL, LOG_DESTINATION_CSVLOG, "csvlog", ".csv"};
+
+static inline bool file_log_dest_should_rotate_for_size(const FileLogDestination *info);
+static bool file_log_dest_rotate(FileLogDestination *info, pg_time_t fntime, bool time_based_rotation, int size_rotation_for);
+static inline void file_log_dest_close(FileLogDestination *info);
/*
* Buffers for saving partial messages from different backends.
@@ -138,6 +150,8 @@ static void process_pipe_input(char *logbuffer, int *bytes_in_logbuffer);
static void flush_pipe_input(char *logbuffer, int *bytes_in_logbuffer);
static FILE *logfile_open(const char *filename, const char *mode,
bool allow_errors);
+static FILE *logfile_open_suffix(const char *filename, const char *mode,
+ bool allow_errors);
#ifdef WIN32
static unsigned int __stdcall pipeThread(void *arg);
@@ -272,9 +286,9 @@ SysLoggerMain(int argc, char *argv[])
* time because passing down just the pg_time_t is a lot cheaper than
* passing a whole file path in the EXEC_BACKEND case.
*/
- last_file_name = logfile_getname(first_syslogger_file_time, NULL);
- if (csvlogFile != NULL)
- last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ stderr_file_info.last_file_name = logfile_getname(first_syslogger_file_time, stderr_file_info.suffix);
+ if (csvlog_file_info.file != NULL)
+ csvlog_file_info.last_file_name = logfile_getname(first_syslogger_file_time, csvlog_file_info.suffix);
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -358,7 +372,7 @@ SysLoggerMain(int argc, char *argv[])
* we need to open or close csvlogFile accordingly.
*/
if (((Log_destination & LOG_DESTINATION_CSVLOG) != 0) !=
- (csvlogFile != NULL))
+ (csvlog_file_info.file != NULL))
rotation_requested = true;
/*
@@ -400,13 +414,12 @@ SysLoggerMain(int argc, char *argv[])
if (!rotation_requested && Log_RotationSize > 0 && !rotation_disabled)
{
/* Do a rotation if file is too big */
- if (ftell(syslogFile) >= Log_RotationSize * 1024L)
+ if (file_log_dest_should_rotate_for_size(&stderr_file_info))
{
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_STDERR;
}
- if (csvlogFile != NULL &&
- ftell(csvlogFile) >= Log_RotationSize * 1024L)
+ if (file_log_dest_should_rotate_for_size(&csvlog_file_info))
{
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
@@ -541,7 +554,6 @@ int
SysLogger_Start(void)
{
pid_t sysloggerPid;
- char *filename;
if (!Logging_collector)
return 0;
@@ -606,25 +618,14 @@ SysLogger_Start(void)
*/
first_syslogger_file_time = time(NULL);
- filename = logfile_getname(first_syslogger_file_time, NULL);
-
- syslogFile = logfile_open(filename, "a", false);
-
- pfree(filename);
-
/*
- * Likewise for the initial CSV log file, if that's enabled. (Note that
- * we open syslogFile even when only CSV output is nominally enabled,
- * since some code paths will write to syslogFile anyway.)
+ * stderr file will always be opened since some code paths will write there
+ * even if only other log destinations are enabled.
*/
+ stderr_file_info.file = logfile_open_suffix(stderr_file_info.suffix, "a", false);
+ /* Likewise for the initial CSV log file, if that's enabled. */
if (Log_destination & LOG_DESTINATION_CSVLOG)
- {
- filename = logfile_getname(first_syslogger_file_time, ".csv");
-
- csvlogFile = logfile_open(filename, "a", false);
-
- pfree(filename);
- }
+ csvlog_file_info.file = logfile_open_suffix(csvlog_file_info.suffix, "a", false);
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
@@ -716,13 +717,8 @@ SysLogger_Start(void)
}
/* postmaster will never write the file(s); close 'em */
- fclose(syslogFile);
- syslogFile = NULL;
- if (csvlogFile != NULL)
- {
- fclose(csvlogFile);
- csvlogFile = NULL;
- }
+ file_log_dest_close(&stderr_file_info);
+ file_log_dest_close(&csvlog_file_info);
return (int) sysloggerPid;
}
@@ -765,9 +761,9 @@ syslogger_forkexec(void)
av[ac++] = NULL; /* filled in by postmaster_forkexec */
/* static variables (those not passed by write_backend_variables) */
- snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(syslogFile));
+ snprintf(filenobuf, sizeof(filenobuf), "%ld", syslogger_get_fileno(stderr_file_info.file));
av[ac++] = filenobuf;
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlogFile));
+ snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld", syslogger_get_fileno(csvlog_file_info.file));
av[ac++] = csvfilenobuf;
av[ac] = NULL;
@@ -819,8 +815,8 @@ syslogger_parseArgs(int argc, char *argv[])
* fails there's not a lot we can do to report the problem anyway. As
* coded, we'll just crash on a null pointer dereference after failure...
*/
- syslogFile = syslogger_fdopen(atoi(*argv++));
- csvlogFile = syslogger_fdopen(atoi(*argv++));
+ stderr_file_info.file = syslogger_fdopen(atoi(*argv++));
+ csvlog_file_info.file = syslogger_fdopen(atoi(*argv++));
}
#endif /* EXEC_BACKEND */
@@ -1081,8 +1077,10 @@ write_syslogger_file(const char *buffer, int count, int destination)
* Think not to improve this by trying to open csvlogFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if (destination == LOG_DESTINATION_CSVLOG && csvlog_file_info.file != NULL)
+ logfile = csvlog_file_info.file;
+ else
+ logfile = stderr_file_info.file;
rc = fwrite(buffer, 1, count, logfile);
@@ -1152,8 +1150,8 @@ pipeThread(void *arg)
*/
if (Log_RotationSize > 0)
{
- if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ if (file_log_dest_should_rotate_for_size(&stderr_file_info) ||
+ file_log_dest_should_rotate_for_size(&csvlog_file_info))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1218,16 +1216,29 @@ logfile_open(const char *filename, const char *mode, bool allow_errors)
return fh;
}
+/*
+ * Generate a logfile name from a given suffix and then open it.
+ */
+static FILE *
+logfile_open_suffix(const char *suffix, const char *mode, bool allow_errors)
+{
+ FILE *file;
+ char *filename;
+
+ filename = logfile_getname(first_syslogger_file_time, suffix);
+ file = logfile_open(filename, "a", allow_errors);
+
+ pfree(filename);
+ return file;
+}
+
/*
* perform logfile rotation
*/
static void
logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
- char *filename;
- char *csvfilename = NULL;
pg_time_t fntime;
- FILE *fh;
rotation_requested = false;
@@ -1240,125 +1251,20 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
fntime = next_rotation_time;
else
fntime = time(NULL);
- filename = logfile_getname(fntime, NULL);
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- csvfilename = logfile_getname(fntime, ".csv");
-
- /*
- * Decide whether to overwrite or append. We can overwrite if (a)
- * Log_truncate_on_rotation is set, (b) the rotation was triggered by
- * elapsed time and not something else, and (c) the computed file name is
- * different from what we were previously logging into.
- *
- * Note: last_file_name should never be NULL here, but if it is, append.
- */
- if (time_based_rotation || (size_rotation_for & LOG_DESTINATION_STDERR))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_file_name != NULL &&
- strcmp(filename, last_file_name) != 0)
- fh = logfile_open(filename, "w", true);
- else
- fh = logfile_open(filename, "a", true);
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- fclose(syslogFile);
- syslogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_file_name != NULL)
- pfree(last_file_name);
- last_file_name = filename;
- filename = NULL;
- }
-
- /*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
- * not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
- * rules since it's not a time-based rotation.
- */
- if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
- (csvlogFile == NULL ||
- time_based_rotation || (size_rotation_for & LOG_DESTINATION_CSVLOG)))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_csv_file_name != NULL &&
- strcmp(csvfilename, last_csv_file_name) != 0)
- fh = logfile_open(csvfilename, "w", true);
- else
- fh = logfile_open(csvfilename, "a", true);
-
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
+ if (!file_log_dest_rotate(&stderr_file_info, fntime, time_based_rotation, size_rotation_for))
+ /* error rotating stderr so try again later */
+ return;
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
+ if (Log_destination & LOG_DESTINATION_CSVLOG) {
+ if (!file_log_dest_rotate(&csvlog_file_info, fntime, time_based_rotation, size_rotation_for))
+ /* error rotating csvlog so try again later */
return;
- }
-
- if (csvlogFile != NULL)
- fclose(csvlogFile);
- csvlogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = csvfilename;
- csvfilename = NULL;
- }
- else if (!(Log_destination & LOG_DESTINATION_CSVLOG) &&
- csvlogFile != NULL)
- {
+ } else if (csvlog_file_info.file != NULL) {
/* CSVLOG was just turned off, so close the old file */
- fclose(csvlogFile);
- csvlogFile = NULL;
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = NULL;
+ file_log_dest_close(&csvlog_file_info);
}
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
-
update_metainfo_datafile();
set_next_rotation_time();
@@ -1430,6 +1336,22 @@ set_next_rotation_time(void)
next_rotation_time = now;
}
+static bool inline write_metainfo_line(FILE *metadata_file, const FileLogDestination *info)
+{
+ if (info->last_file_name != NULL && (Log_destination & info->dest))
+ {
+ if (fprintf(metadata_file, "%s %s\n", info->name, info->last_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ return false;
+ }
+ }
+ return true;
+}
+
/*
* Store the name of the file(s) where the log collector, when enabled, writes
* log messages. Useful for finding the name(s) of the current log file(s)
@@ -1478,31 +1400,13 @@ update_metainfo_datafile(void)
return;
}
- if (last_file_name && (Log_destination & LOG_DESTINATION_STDERR))
+ if (!write_metainfo_line(fh, &stderr_file_info) ||
+ !write_metainfo_line(fh, &csvlog_file_info))
{
- if (fprintf(fh, "stderr %s\n", last_file_name) < 0)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not write file \"%s\": %m",
- LOG_METAINFO_DATAFILE_TMP)));
- fclose(fh);
- return;
- }
+ fclose(fh);
+ return;
}
- if (last_csv_file_name && (Log_destination & LOG_DESTINATION_CSVLOG))
- {
- if (fprintf(fh, "csvlog %s\n", last_csv_file_name) < 0)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not write file \"%s\": %m",
- LOG_METAINFO_DATAFILE_TMP)));
- fclose(fh);
- return;
- }
- }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
@@ -1552,3 +1456,82 @@ sigUsr1Handler(SIGNAL_ARGS)
errno = save_errno;
}
+
+static inline bool file_log_dest_should_rotate_for_size(const FileLogDestination *info)
+{
+ if (info->file == NULL)
+ return false;
+
+ return ftell(info->file) >= Log_RotationSize * 1024L;
+}
+
+/*
+ * Checks if a log file should be rotated and if so rotate it.
+ *
+ * If the file is to be rotated and a new log file cannot be opened then return
+ * false. Otherwise return true regardless of whether it was rotated.
+ */
+static bool file_log_dest_rotate(FileLogDestination *info, pg_time_t fntime, bool time_based_rotation, int size_rotation_for)
+{
+ /*
+ * Decide whether to overwrite or append. We can overwrite if (a)
+ * Log_truncate_on_rotation is set, (b) the rotation was triggered by
+ * elapsed time and not something else, and (c) the computed file name is
+ * different from what we were previously logging into.
+ *
+ * Note: last_file_name should never be NULL here, but if it is, append.
+ */
+ if (time_based_rotation || (size_rotation_for & info->dest))
+ {
+ FILE *file = NULL;
+ char *filename = logfile_getname(fntime, info->suffix);
+
+ if (Log_truncate_on_rotation
+ && time_based_rotation
+ && info->last_file_name != NULL
+ && strcmp(filename, info->last_file_name) != 0)
+ file = logfile_open(filename, "w", true);
+ else
+ file = logfile_open(filename, "a", true);
+
+ if (file == NULL)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just
+ * keep using the old file till we manage to get a new one.
+ * Otherwise, assume something's wrong with Log_directory and stop
+ * trying to create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ pfree(filename);
+ return false;
+ }
+
+ file_log_dest_close(info);
+ info->file = file;
+ /* instead of pfree'ing filename, remember it for next time */
+ info->last_file_name = filename;
+ }
+
+ return true;
+}
+
+static inline void file_log_dest_close(FileLogDestination *info)
+{
+ if (info->file != NULL)
+ {
+ fclose(info->file);
+ info->file = NULL;
+ }
+ if (info->last_file_name != NULL)
+ {
+ pfree(info->last_file_name);
+ info->last_file_name = NULL;
+ }
+}
--
2.17.1
On Fri, Sep 17, 2021 at 04:36:57PM -0400, Sehrope Sarkuni wrote:
It was helpful to split them out while working on the patch but I see your
point upon re-reading through the result.Attached version (renamed to 002) adds only three static functions for
checking rotation size, performing the actual rotation, and closing. Also
one other to handle generating the logfile names with a suffix to handle
the pfree-ing (wrapping logfile_open(...)).The rest of the changes happen in place using the new structures.
I have looked at that in details, and found that the introduction of
FileLogDestination makes the code harder to follow, and that the
introduction of the file extension, the destination name and the
expected target destination LOG_DESTINATION_* had a limited impact
because they are used in few places. The last two useful pieces are
the FILE* handle and the last file name for current_logfiles.
Attached are updated patches. The logic of 0001 to refactor the fd
fetch/save logic when forking the syslogger in EXEC_BACKEND builds is
unchanged. I have tweaked the patch with more comments and different
routine names though. Patch 0002 refactors the main point that
introduced FileLogDestination by refactoring the per-destination file
rotation, not forgetting the fact that the file last name and handle
for stderr can never be cleaned up even if LOG_DESTINATION_STDERR is
disabled. Grepping after LOG_DESTINATION_CSVLOG in the code tree, I'd
be fine to live with this level of abstraction as each per-destination
change are grouped with each other so they are hard to miss.
0001 is in a rather commitable shape, and I have made the code
consistent with HEAD. However, I think that its handling of
_get_osfhandle() is clunky for 64-bit compilations as long is 32b in
WIN32 but intptr_t is platform-dependent as it could be 32b or 64b, so
atoi() would overflow if the handle is larger than INT_MAX for 64b
builds:
https://docs.microsoft.com/en-us/cpp/c-runtime-library/standard-types
This problem deserves a different thread.
It would be good for 0002 if an extra pair of eyes looks at it. While
on it, I have renamed the existing last_file_name to
last_sys_file_name in 0002 to make the naming more consistent with
syslogFile. It is independent of 0001, so it could be done first as
well.
--
Michael
Attachments:
v7-0001-Refactor-fd-handling-when-forking-syslogger-in-EX.patchtext/x-diff; charset=us-asciiDownload
From 57bcaf49bd10ec3e6afb14f3ec384cb88a65efe6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 28 Sep 2021 12:28:21 +0900
Subject: [PATCH v7 1/2] Refactor fd handling when forking syslogger in
EXEC_BACKEND build
---
src/backend/postmaster/syslogger.c | 122 +++++++++++++++--------------
1 file changed, 62 insertions(+), 60 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index bca3883572..ddb47e3cb0 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -130,6 +130,8 @@ static volatile sig_atomic_t rotation_requested = false;
/* Local subroutines */
#ifdef EXEC_BACKEND
+static long syslogger_fdget(FILE *file);
+static FILE *syslogger_fdopen(int fd);
static pid_t syslogger_forkexec(void);
static void syslogger_parseArgs(int argc, char *argv[]);
#endif
@@ -733,6 +735,60 @@ SysLogger_Start(void)
#ifdef EXEC_BACKEND
+/*
+ * syslogger_fdget() -
+ *
+ * Utility wrapper to grab the file descriptor of an opened error output
+ * file. Used when building the command to fork the logging collector.
+ */
+static long
+syslogger_fdget(FILE *file)
+{
+#ifndef WIN32
+ if (file != NULL)
+ return (long) fileno(file);
+ else
+ return -1;
+#else /* WIN32 */
+ if (file != NULL)
+ return (long) _get_osfhandle(_fileno(file));
+ else
+ return 0;
+#endif /* WIN32 */
+}
+
+/*
+ * syslogger_fdopen() -
+ *
+ * Utility wrapper to re-open an error output file, using the given file
+ * descriptor. Used when parsing arguments in a forked logging collector.
+ */
+static FILE *
+syslogger_fdopen(int fd)
+{
+ FILE *file = NULL;
+
+#ifndef WIN32
+ if (fd != -1)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+#else /* WIN32 */
+ if (fd != 0)
+ {
+ fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
+ if (fd > 0)
+ {
+ file = fdopen(fd, "a");
+ setvbuf(file, NULL, PG_IOLBF, 0);
+ }
+ }
+#endif /* WIN32 */
+
+ return file;
+}
+
/*
* syslogger_forkexec() -
*
@@ -751,34 +807,11 @@ syslogger_forkexec(void)
av[ac++] = NULL; /* filled in by postmaster_forkexec */
/* static variables (those not passed by write_backend_variables) */
-#ifndef WIN32
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%d",
- fileno(syslogFile));
- else
- strcpy(filenobuf, "-1");
-#else /* WIN32 */
- if (syslogFile != NULL)
- snprintf(filenobuf, sizeof(filenobuf), "%ld",
- (long) _get_osfhandle(_fileno(syslogFile)));
- else
- strcpy(filenobuf, "0");
-#endif /* WIN32 */
+ snprintf(filenobuf, sizeof(filenobuf), "%ld",
+ syslogger_fdget(syslogFile));
av[ac++] = filenobuf;
-
-#ifndef WIN32
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
- fileno(csvlogFile));
- else
- strcpy(csvfilenobuf, "-1");
-#else /* WIN32 */
- if (csvlogFile != NULL)
- snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
- (long) _get_osfhandle(_fileno(csvlogFile)));
- else
- strcpy(csvfilenobuf, "0");
-#endif /* WIN32 */
+ snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%ld",
+ syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
av[ac] = NULL;
@@ -807,41 +840,10 @@ syslogger_parseArgs(int argc, char *argv[])
* fails there's not a lot we can do to report the problem anyway. As
* coded, we'll just crash on a null pointer dereference after failure...
*/
-#ifndef WIN32
fd = atoi(*argv++);
- if (fd != -1)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
+ syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
- if (fd != -1)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
-#else /* WIN32 */
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- syslogFile = fdopen(fd, "a");
- setvbuf(syslogFile, NULL, PG_IOLBF, 0);
- }
- }
- fd = atoi(*argv++);
- if (fd != 0)
- {
- fd = _open_osfhandle(fd, _O_APPEND | _O_TEXT);
- if (fd > 0)
- {
- csvlogFile = fdopen(fd, "a");
- setvbuf(csvlogFile, NULL, PG_IOLBF, 0);
- }
- }
-#endif /* WIN32 */
+ csvlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
--
2.33.0
v7-0002-Refactor-per-destination-file-rotation-in-syslogg.patchtext/x-diff; charset=us-asciiDownload
From 88d43ad72b252e9efd2c9b7f5ba84d786ad78471 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 28 Sep 2021 11:48:18 +0900
Subject: [PATCH v7 2/2] Refactor per-destination file rotation in syslogger
---
src/backend/postmaster/syslogger.c | 242 ++++++++++++++---------------
1 file changed, 119 insertions(+), 123 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index ddb47e3cb0..2c13e7ef9f 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -87,7 +87,7 @@ static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
-static char *last_file_name = NULL;
+static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
/*
@@ -274,7 +274,7 @@ SysLoggerMain(int argc, char *argv[])
* time because passing down just the pg_time_t is a lot cheaper than
* passing a whole file path in the EXEC_BACKEND case.
*/
- last_file_name = logfile_getname(first_syslogger_file_time, NULL);
+ last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
@@ -1241,16 +1241,118 @@ logfile_open(const char *filename, const char *mode, bool allow_errors)
return fh;
}
+/*
+ * Do logfile rotation for a single destination, as specified by target_dest.
+ * The information stored in *last_file_name and *logFile is updated on a
+ * successful file rotation.
+ *
+ * Returns false if the rotation has been stopped, and true to move on to
+ * the processing of other formats.
+ */
+static bool
+logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
+ pg_time_t fntime, int target_dest,
+ char **last_file_name, FILE **logFile)
+{
+ char *logFileExt;
+ char *filename;
+ FILE *fh;
+
+ /*
+ * If the target destination was just turned off, close the previous
+ * file and unregister its data. This cannot happen for stderr as
+ * syslogFile is assumed to be always opened even if
+ * LOG_DESTINATION_STDERR is disabled.
+ */
+ if ((Log_destination & target_dest) == 0 &&
+ target_dest != LOG_DESTINATION_STDERR)
+ {
+ if (*logFile != NULL)
+ fclose(*logFile);
+ *logFile = NULL;
+ if (*last_file_name != NULL)
+ pfree(*last_file_name);
+ *last_file_name = NULL;
+ return true;
+ }
+
+ /*
+ * Leave if it is not time for a rotation or if the target destination
+ * has no need to do a rotation.
+ */
+ if (!time_based_rotation &&
+ (size_rotation_for & target_dest) == 0)
+ {
+ return true;
+ }
+
+ /* file extension depends on the destination type */
+ if (target_dest == LOG_DESTINATION_STDERR)
+ logFileExt = NULL;
+ else if (target_dest == LOG_DESTINATION_CSVLOG)
+ logFileExt = ".csv";
+ else
+ {
+ /* cannot happen */
+ Assert(false);
+ }
+
+ /* build the new file name */
+ filename = logfile_getname(fntime, logFileExt);
+
+ /*
+ * Decide whether to overwrite or append. We can overwrite if (a)
+ * Log_truncate_on_rotation is set, (b) the rotation was triggered by
+ * elapsed time and not something else, and (c) the computed file name is
+ * different from what we were previously logging into.
+ */
+ if (Log_truncate_on_rotation && time_based_rotation &&
+ *last_file_name != NULL &&
+ strcmp(filename, *last_file_name) != 0)
+ fh = logfile_open(filename, "w", true);
+ else
+ fh = logfile_open(filename, "a", true);
+
+ if (!fh)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just keep
+ * using the old file till we manage to get a new one. Otherwise,
+ * assume something's wrong with Log_directory and stop trying to
+ * create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ if (filename)
+ pfree(filename);
+ return false;
+ }
+
+ /* fill in the new information */
+ if (*logFile != NULL)
+ fclose(*logFile);
+ *logFile = fh;
+
+ /* instead of pfree'ing filename, remember it for next time */
+ if (*last_file_name != NULL)
+ pfree(*last_file_name);
+ *last_file_name = filename;
+
+ return true;
+}
+
/*
* perform logfile rotation
*/
static void
logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
- char *filename;
- char *csvfilename = NULL;
pg_time_t fntime;
- FILE *fh;
rotation_requested = false;
@@ -1263,124 +1365,18 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
fntime = next_rotation_time;
else
fntime = time(NULL);
- filename = logfile_getname(fntime, NULL);
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- csvfilename = logfile_getname(fntime, ".csv");
- /*
- * Decide whether to overwrite or append. We can overwrite if (a)
- * Log_truncate_on_rotation is set, (b) the rotation was triggered by
- * elapsed time and not something else, and (c) the computed file name is
- * different from what we were previously logging into.
- *
- * Note: last_file_name should never be NULL here, but if it is, append.
- */
- if (time_based_rotation || (size_rotation_for & LOG_DESTINATION_STDERR))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_file_name != NULL &&
- strcmp(filename, last_file_name) != 0)
- fh = logfile_open(filename, "w", true);
- else
- fh = logfile_open(filename, "a", true);
+ /* file rotation for stderr */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_STDERR, &last_sys_file_name,
+ &syslogFile))
+ return;
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- fclose(syslogFile);
- syslogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_file_name != NULL)
- pfree(last_file_name);
- last_file_name = filename;
- filename = NULL;
- }
-
- /*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
- * not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
- * rules since it's not a time-based rotation.
- */
- if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
- (csvlogFile == NULL ||
- time_based_rotation || (size_rotation_for & LOG_DESTINATION_CSVLOG)))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_csv_file_name != NULL &&
- strcmp(csvfilename, last_csv_file_name) != 0)
- fh = logfile_open(csvfilename, "w", true);
- else
- fh = logfile_open(csvfilename, "a", true);
-
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- if (csvlogFile != NULL)
- fclose(csvlogFile);
- csvlogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = csvfilename;
- csvfilename = NULL;
- }
- else if (!(Log_destination & LOG_DESTINATION_CSVLOG) &&
- csvlogFile != NULL)
- {
- /* CSVLOG was just turned off, so close the old file */
- fclose(csvlogFile);
- csvlogFile = NULL;
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = NULL;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
+ /* file rotation for csvlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_CSVLOG, &last_csv_file_name,
+ &csvlogFile))
+ return;
update_metainfo_datafile();
@@ -1501,9 +1497,9 @@ update_metainfo_datafile(void)
return;
}
- if (last_file_name && (Log_destination & LOG_DESTINATION_STDERR))
+ if (last_sys_file_name && (Log_destination & LOG_DESTINATION_STDERR))
{
- if (fprintf(fh, "stderr %s\n", last_file_name) < 0)
+ if (fprintf(fh, "stderr %s\n", last_sys_file_name) < 0)
{
ereport(LOG,
(errcode_for_file_access(),
--
2.33.0
On Tue, Sep 28, 2021 at 12:30:10PM +0900, Michael Paquier wrote:
0001 is in a rather commitable shape, and I have made the code
consistent with HEAD. However, I think that its handling of
_get_osfhandle() is clunky for 64-bit compilations as long is 32b in
WIN32 but intptr_t is platform-dependent as it could be 32b or 64b, so
atoi() would overflow if the handle is larger than INT_MAX for 64b
builds:
https://docs.microsoft.com/en-us/cpp/c-runtime-library/standard-types
This problem deserves a different thread.
This happens to not be a problem as only 32 bits are significant for
handles for both Win32 and Win64. This also means that we should be
able to remove the use for "long" in this code, making the routines
more symmetric. I have done more tests with Win32 and Win64, and
applied it. I don't have MinGW environments at hand, but looking at
the upstream code that should not matter. The buildfarm will let
us know soon enough if there is a problem thanks to the TAP tests of
pg_ctl.
--
Michael
On Wed, Sep 29, 2021 at 11:02:10AM +0900, Michael Paquier wrote:
This happens to not be a problem as only 32 bits are significant for
handles for both Win32 and Win64. This also means that we should be
able to remove the use for "long" in this code, making the routines
more symmetric. I have done more tests with Win32 and Win64, and
applied it. I don't have MinGW environments at hand, but looking at
the upstream code that should not matter. The buildfarm will let
us know soon enough if there is a problem thanks to the TAP tests of
pg_ctl.
So, I have been looking at the rest of the patch set for the last
couple of days, and I think that I have spotted all the code paths
that need to be smarter when it comes to multiple file-based log
destinations. Attached is a new patch set:
- 0001 does some refactoring of the file rotation in syslogger.c,
that's the same patch as previously posted.
- 0002 is more refactoring of elog.c, adding routines for the start
timestamp, log timestamp, the backend type and an extra one to check
if a query can be logged or not.
- 0003 is a change to send_message_to_server_log() to be smarter
regarding the fallback to stderr if a csvlog (or a jsonlog!) entry
cannot be logged because the redirection is not ready yet. The code
of HEAD processes first stderr, then csvlog, with csvlog moving back
to stderr if not done yet. That's a bit strange, because for example
on WIN32 we would lose any csvlog entry for a service. I propose here
to do csvlog first, and fallback to stderr so as it gets done in one
code path instead of two. I have spent quite a bit of time thinking
about the best way to handle the case of multiple file log
destinations here because we don't want to log multiple times to
stderr if csvlog and jsonlog are both enabled. And I think that this
is the simplest thing we could do.
- 0004 moves the CSV-specific code into its own file. This include
some refactoring of elog.c that should be moved to 0002, as this
requires more routines of elog.c to be published:
-- write_pipe_chunks()
-- error_severity()
- 0005 is the main meat, that introduces JSON as log_destination.
This compiles and passes all my tests, but I have not really done an
in-depth review of this code yet.
0002 and 0004 could be more polished and most of their pieces had
better be squashed together. 0003, though, would improve the case of
WIN32 where only csvlog is enabled so as log entries are properly
redirected to the event logs if the redirection is not done yet. I'd
like to move on with 0001 and 0003 as independent pieces.
Sehrope, any thoughts?
--
Michael
Attachments:
v4-0001-Refactor-per-destination-file-rotation-in-syslogg.patchtext/x-diff; charset=us-asciiDownload
From 4641f8535c2be4b75285ac34d87e883bec250e48 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 28 Sep 2021 11:48:18 +0900
Subject: [PATCH v4 1/5] Refactor per-destination file rotation in syslogger
---
src/backend/postmaster/syslogger.c | 242 ++++++++++++++---------------
1 file changed, 119 insertions(+), 123 deletions(-)
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index c5f9c5202d..4a019db7f4 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -87,7 +87,7 @@ static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
-static char *last_file_name = NULL;
+static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
/*
@@ -274,7 +274,7 @@ SysLoggerMain(int argc, char *argv[])
* time because passing down just the pg_time_t is a lot cheaper than
* passing a whole file path in the EXEC_BACKEND case.
*/
- last_file_name = logfile_getname(first_syslogger_file_time, NULL);
+ last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
@@ -1241,16 +1241,118 @@ logfile_open(const char *filename, const char *mode, bool allow_errors)
return fh;
}
+/*
+ * Do logfile rotation for a single destination, as specified by target_dest.
+ * The information stored in *last_file_name and *logFile is updated on a
+ * successful file rotation.
+ *
+ * Returns false if the rotation has been stopped, and true to move on to
+ * the processing of other formats.
+ */
+static bool
+logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
+ pg_time_t fntime, int target_dest,
+ char **last_file_name, FILE **logFile)
+{
+ char *logFileExt;
+ char *filename;
+ FILE *fh;
+
+ /*
+ * If the target destination was just turned off, close the previous
+ * file and unregister its data. This cannot happen for stderr as
+ * syslogFile is assumed to be always opened even if
+ * LOG_DESTINATION_STDERR is disabled.
+ */
+ if ((Log_destination & target_dest) == 0 &&
+ target_dest != LOG_DESTINATION_STDERR)
+ {
+ if (*logFile != NULL)
+ fclose(*logFile);
+ *logFile = NULL;
+ if (*last_file_name != NULL)
+ pfree(*last_file_name);
+ *last_file_name = NULL;
+ return true;
+ }
+
+ /*
+ * Leave if it is not time for a rotation or if the target destination
+ * has no need to do a rotation.
+ */
+ if (!time_based_rotation &&
+ (size_rotation_for & target_dest) == 0)
+ {
+ return true;
+ }
+
+ /* file extension depends on the destination type */
+ if (target_dest == LOG_DESTINATION_STDERR)
+ logFileExt = NULL;
+ else if (target_dest == LOG_DESTINATION_CSVLOG)
+ logFileExt = ".csv";
+ else
+ {
+ /* cannot happen */
+ Assert(false);
+ }
+
+ /* build the new file name */
+ filename = logfile_getname(fntime, logFileExt);
+
+ /*
+ * Decide whether to overwrite or append. We can overwrite if (a)
+ * Log_truncate_on_rotation is set, (b) the rotation was triggered by
+ * elapsed time and not something else, and (c) the computed file name is
+ * different from what we were previously logging into.
+ */
+ if (Log_truncate_on_rotation && time_based_rotation &&
+ *last_file_name != NULL &&
+ strcmp(filename, *last_file_name) != 0)
+ fh = logfile_open(filename, "w", true);
+ else
+ fh = logfile_open(filename, "a", true);
+
+ if (!fh)
+ {
+ /*
+ * ENFILE/EMFILE are not too surprising on a busy system; just keep
+ * using the old file till we manage to get a new one. Otherwise,
+ * assume something's wrong with Log_directory and stop trying to
+ * create files.
+ */
+ if (errno != ENFILE && errno != EMFILE)
+ {
+ ereport(LOG,
+ (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
+ rotation_disabled = true;
+ }
+
+ if (filename)
+ pfree(filename);
+ return false;
+ }
+
+ /* fill in the new information */
+ if (*logFile != NULL)
+ fclose(*logFile);
+ *logFile = fh;
+
+ /* instead of pfree'ing filename, remember it for next time */
+ if (*last_file_name != NULL)
+ pfree(*last_file_name);
+ *last_file_name = filename;
+
+ return true;
+}
+
/*
* perform logfile rotation
*/
static void
logfile_rotate(bool time_based_rotation, int size_rotation_for)
{
- char *filename;
- char *csvfilename = NULL;
pg_time_t fntime;
- FILE *fh;
rotation_requested = false;
@@ -1263,124 +1365,18 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
fntime = next_rotation_time;
else
fntime = time(NULL);
- filename = logfile_getname(fntime, NULL);
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- csvfilename = logfile_getname(fntime, ".csv");
- /*
- * Decide whether to overwrite or append. We can overwrite if (a)
- * Log_truncate_on_rotation is set, (b) the rotation was triggered by
- * elapsed time and not something else, and (c) the computed file name is
- * different from what we were previously logging into.
- *
- * Note: last_file_name should never be NULL here, but if it is, append.
- */
- if (time_based_rotation || (size_rotation_for & LOG_DESTINATION_STDERR))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_file_name != NULL &&
- strcmp(filename, last_file_name) != 0)
- fh = logfile_open(filename, "w", true);
- else
- fh = logfile_open(filename, "a", true);
+ /* file rotation for stderr */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_STDERR, &last_sys_file_name,
+ &syslogFile))
+ return;
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- fclose(syslogFile);
- syslogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_file_name != NULL)
- pfree(last_file_name);
- last_file_name = filename;
- filename = NULL;
- }
-
- /*
- * Same as above, but for csv file. Note that if LOG_DESTINATION_CSVLOG
- * was just turned on, we might have to open csvlogFile here though it was
- * not open before. In such a case we'll append not overwrite (since
- * last_csv_file_name will be NULL); that is consistent with the normal
- * rules since it's not a time-based rotation.
- */
- if ((Log_destination & LOG_DESTINATION_CSVLOG) &&
- (csvlogFile == NULL ||
- time_based_rotation || (size_rotation_for & LOG_DESTINATION_CSVLOG)))
- {
- if (Log_truncate_on_rotation && time_based_rotation &&
- last_csv_file_name != NULL &&
- strcmp(csvfilename, last_csv_file_name) != 0)
- fh = logfile_open(csvfilename, "w", true);
- else
- fh = logfile_open(csvfilename, "a", true);
-
- if (!fh)
- {
- /*
- * ENFILE/EMFILE are not too surprising on a busy system; just
- * keep using the old file till we manage to get a new one.
- * Otherwise, assume something's wrong with Log_directory and stop
- * trying to create files.
- */
- if (errno != ENFILE && errno != EMFILE)
- {
- ereport(LOG,
- (errmsg("disabling automatic rotation (use SIGHUP to re-enable)")));
- rotation_disabled = true;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
- return;
- }
-
- if (csvlogFile != NULL)
- fclose(csvlogFile);
- csvlogFile = fh;
-
- /* instead of pfree'ing filename, remember it for next time */
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = csvfilename;
- csvfilename = NULL;
- }
- else if (!(Log_destination & LOG_DESTINATION_CSVLOG) &&
- csvlogFile != NULL)
- {
- /* CSVLOG was just turned off, so close the old file */
- fclose(csvlogFile);
- csvlogFile = NULL;
- if (last_csv_file_name != NULL)
- pfree(last_csv_file_name);
- last_csv_file_name = NULL;
- }
-
- if (filename)
- pfree(filename);
- if (csvfilename)
- pfree(csvfilename);
+ /* file rotation for csvlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_CSVLOG, &last_csv_file_name,
+ &csvlogFile))
+ return;
update_metainfo_datafile();
@@ -1501,9 +1497,9 @@ update_metainfo_datafile(void)
return;
}
- if (last_file_name && (Log_destination & LOG_DESTINATION_STDERR))
+ if (last_sys_file_name && (Log_destination & LOG_DESTINATION_STDERR))
{
- if (fprintf(fh, "stderr %s\n", last_file_name) < 0)
+ if (fprintf(fh, "stderr %s\n", last_sys_file_name) < 0)
{
ereport(LOG,
(errcode_for_file_access(),
--
2.33.0
v4-0002-Some-refactoring-of-elog-specific-routines.patchtext/x-diff; charset=us-asciiDownload
From 83a5bd50da25d6a99e5804b0bd268b3a9e86644e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 11:19:46 +0900
Subject: [PATCH v4 2/5] Some refactoring of elog-specific routines
This refactors out the following things in elog.c, for ease of use
across multiple log destinations:
- start_timestamp
- log_timestamp
- decide if query can be logged
- backend type
This will be reused by jsonlog.
---
src/include/utils/elog.h | 4 +
src/backend/utils/error/elog.c | 141 +++++++++++++++++++++------------
2 files changed, 94 insertions(+), 51 deletions(-)
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..bc6f362b6a 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -441,6 +441,10 @@ extern bool syslog_split_messages;
extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+extern char *get_formatted_start_time(void);
+extern char *get_formatted_log_time(void);
+extern bool check_log_of_query(ErrorData *edata);
+extern const char *get_backend_type_for_log(void);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 2af87ee3bd..3d89937322 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -175,8 +175,6 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
static void write_csvlog(ErrorData *edata);
@@ -2289,14 +2287,23 @@ write_console(const char *line, int len)
}
/*
- * setup formatted_log_time, for consistent times between CSV and regular logs
+ * get_formatted_log_time -- compute and get the log timestamp.
+ *
+ * The timestamp is computed if not set yet, so as it is kept consistent
+ * among all the log destinations that require it to be consistent. Note
+ * that the computed timestamp is returned in a static buffer, not
+ * palloc()'d.
*/
-static void
-setup_formatted_log_time(void)
+char *
+get_formatted_log_time(void)
{
pg_time_t stamp_time;
char msbuf[13];
+ /* leave if already computed */
+ if (formatted_log_time[0] != '\0')
+ return formatted_log_time;
+
if (!saved_timeval_set)
{
gettimeofday(&saved_timeval, NULL);
@@ -2318,16 +2325,25 @@ setup_formatted_log_time(void)
/* 'paste' milliseconds into place... */
sprintf(msbuf, ".%03d", (int) (saved_timeval.tv_usec / 1000));
memcpy(formatted_log_time + 19, msbuf, 4);
+
+ return formatted_log_time;
}
/*
- * setup formatted_start_time
+ * get_formatted_start_time -- compute and get the start timestamp.
+ *
+ * The timestamp is computed if not set yet. Note that the computed
+ * timestamp is returned in a static buffer, not palloc()'d.
*/
-static void
-setup_formatted_start_time(void)
+char *
+get_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
+ /* leave if already computed */
+ if (formatted_start_time[0] != '\0')
+ return formatted_start_time;
+
/*
* Note: we expect that guc.c will ensure that log_timezone is set up (at
* least with a minimal GMT value) before Log_line_prefix can become
@@ -2336,6 +2352,49 @@ setup_formatted_start_time(void)
pg_strftime(formatted_start_time, FORMATTED_TS_LEN,
"%Y-%m-%d %H:%M:%S %Z",
pg_localtime(&stamp_time, log_timezone));
+
+ return formatted_start_time;
+}
+
+/*
+ * check_log_of_query -- check if a query can be logged
+ */
+bool
+check_log_of_query(ErrorData *edata)
+{
+ /* log required? */
+ if (!is_log_level_output(edata->elevel, log_min_error_statement))
+ return false;
+
+ /* query log wanted? */
+ if (edata->hide_stmt)
+ return false;
+
+ /* query string available? */
+ if (debug_query_string == NULL)
+ return false;
+
+ return true;
+}
+
+/*
+ * get_backend_type_for_log -- backend type for log entries
+ *
+ * Returns a pointer to a static buffer, not palloc()'d.
+ */
+const char *
+get_backend_type_for_log(void)
+{
+ const char *backend_type_str;
+
+ if (MyProcPid == PostmasterPid)
+ backend_type_str = "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ backend_type_str = MyBgworkerEntry->bgw_type;
+ else
+ backend_type_str = GetBackendTypeDesc(MyBackendType);
+
+ return backend_type_str;
}
/*
@@ -2466,14 +2525,7 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
break;
case 'b':
{
- const char *backend_type_str;
-
- if (MyProcPid == PostmasterPid)
- backend_type_str = "postmaster";
- else if (MyBackendType == B_BG_WORKER)
- backend_type_str = MyBgworkerEntry->bgw_type;
- else
- backend_type_str = GetBackendTypeDesc(MyBackendType);
+ const char *backend_type_str = get_backend_type_for_log();
if (padding != 0)
appendStringInfo(buf, "%*s", padding, backend_type_str);
@@ -2561,7 +2613,10 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
appendStringInfo(buf, "%ld", log_line_number);
break;
case 'm':
- setup_formatted_log_time();
+ /* force a log timestamp reset */
+ formatted_log_time[0] = '\0';
+ (void) get_formatted_log_time();
+
if (padding != 0)
appendStringInfo(buf, "%*s", padding, formatted_log_time);
else
@@ -2602,12 +2657,14 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
break;
case 's':
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- if (padding != 0)
- appendStringInfo(buf, "%*s", padding, formatted_start_time);
- else
- appendStringInfoString(buf, formatted_start_time);
+ {
+ char *start_time = get_formatted_start_time();
+
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, start_time);
+ else
+ appendStringInfoString(buf, start_time);
+ }
break;
case 'i':
if (MyProcPort)
@@ -2763,6 +2820,8 @@ write_csvlog(ErrorData *edata)
{
StringInfoData buf;
bool print_stmt = false;
+ char *start_time;
+ char *log_time;
/* static counter for line numbers */
static long log_line_number = 0;
@@ -2785,17 +2844,9 @@ write_csvlog(ErrorData *edata)
initStringInfo(&buf);
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
appendStringInfoChar(&buf, ',');
/* username */
@@ -2853,9 +2904,8 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
appendStringInfoChar(&buf, ',');
/* Virtual transaction id */
@@ -2906,10 +2956,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
+ print_stmt = check_log_of_query(edata);
if (print_stmt)
appendCSVLiteral(&buf, debug_query_string);
appendStringInfoChar(&buf, ',');
@@ -2943,13 +2990,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
+ appendCSVLiteral(&buf, get_backend_type_for_log());
appendStringInfoChar(&buf, ',');
/* leader PID */
@@ -3100,9 +3141,7 @@ send_message_to_server_log(ErrorData *edata)
/*
* If the user wants the query that generated this error logged, do it.
*/
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
+ if (check_log_of_query(edata))
{
log_line_prefix(&buf, edata);
appendStringInfoString(&buf, _("STATEMENT: "));
--
2.33.0
v4-0003-Refactor-fallback-to-stderr-for-some-cases-with-c.patchtext/x-diff; charset=us-asciiDownload
From b3b57f3bc30cedd59b199c2fa0178d4f7cdd1703 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 14:24:59 +0900
Subject: [PATCH v4 3/5] Refactor fallback to stderr for some cases with csvlog
send_message_to_server_log() would force a redirection of a log entry to
stderr in some cases, like the syslogger not being available yet. If
this happens, csvlog falls back to stderr to log something. The code
was organized so as stderr was happening before csvlog, with csvlog
calling checking for the same conditions as the stderr code path for a
second time, with a log handling rather buggy for Windows.
Instead, move the csvlog handling to be before stderr, tracking down if
it is necessary to log something to stderr after. The reduces the
handling of stderr to a single code path, and fixes one issue with the
case of a Windows service, where stderr is not available, where we would
lose log entries that should have been redirected from csvlog to
stderr.
---
src/backend/utils/error/elog.c | 56 ++++++++++++++++------------------
1 file changed, 26 insertions(+), 30 deletions(-)
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 3d89937322..01a317ea18 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3049,6 +3049,7 @@ static void
send_message_to_server_log(ErrorData *edata)
{
StringInfoData buf;
+ bool fallback_to_stderr = false;
initStringInfo(&buf);
@@ -3198,8 +3199,29 @@ send_message_to_server_log(ErrorData *edata)
}
#endif /* WIN32 */
- /* Write to stderr, if enabled */
- if ((Log_destination & LOG_DESTINATION_STDERR) || whereToSendOutput == DestDebug)
+ /* Write to CSV log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_CSVLOG) != 0)
+ {
+ /*
+ * Send CSV data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written
+ * to stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_csvlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
+ /*
+ * Write to stderr, if enabled or if required because of a previous
+ * limitation.
+ */
+ if ((Log_destination & LOG_DESTINATION_STDERR) ||
+ whereToSendOutput == DestDebug ||
+ fallback_to_stderr)
{
/*
* Use the chunking protocol if we know the syslogger should be
@@ -3228,34 +3250,8 @@ send_message_to_server_log(ErrorData *edata)
if (MyBackendType == B_LOGGER)
write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_STDERR);
- /* Write to CSV log if enabled */
- if (Log_destination & LOG_DESTINATION_CSVLOG)
- {
- if (redirection_done || MyBackendType == B_LOGGER)
- {
- /*
- * send CSV data if it's safe to do so (syslogger doesn't need the
- * pipe). First get back the space in the message buffer.
- */
- pfree(buf.data);
- write_csvlog(edata);
- }
- else
- {
- /*
- * syslogger not up (yet), so just dump the message to stderr,
- * unless we already did so above.
- */
- if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- whereToSendOutput != DestDebug)
- write_console(buf.data, buf.len);
- pfree(buf.data);
- }
- }
- else
- {
- pfree(buf.data);
- }
+ /* No more need of the message formatted for stderr */
+ pfree(buf.data);
}
/*
--
2.33.0
v4-0004-Refactor-CSV-specific-code-into-its-own-file.patchtext/x-diff; charset=us-asciiDownload
From 5d5eeed08377d5da0203b249b2c4042353207578 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:22:24 +0900
Subject: [PATCH v4 4/5] Refactor CSV-specific code into its own file
This requires more routines of elog.c to be published.
---
src/include/utils/elog.h | 4 +
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 268 +++++++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 251 ++---------------------------
4 files changed, 284 insertions(+), 240 deletions(-)
create mode 100644 src/backend/utils/error/csvlog.c
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index bc6f362b6a..ab08e7fac8 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -441,10 +441,14 @@ extern bool syslog_split_messages;
extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+extern void reset_formatted_start_time(void);
extern char *get_formatted_start_time(void);
extern char *get_formatted_log_time(void);
extern bool check_log_of_query(ErrorData *edata);
extern const char *get_backend_type_for_log(void);
+extern const char *error_severity(int elevel);
+extern void write_csvlog(ErrorData *edata);
+extern void write_pipe_chunks(char *data, int len, int dest);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..443a91e168
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,268 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of Californi
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * write_csvlog -- Generate and write CSV log entry
+ *
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ print_stmt = check_log_of_query(edata);
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 01a317ea18..f99a173e15 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -177,11 +177,8 @@ static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *st
static void write_console(const char *line, int len);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
-static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
@@ -2329,6 +2326,15 @@ get_formatted_log_time(void)
return formatted_log_time;
}
+/*
+ * reset_formatted_start_time -- reset the start timestamp
+ */
+void
+reset_formatted_start_time(void)
+{
+ formatted_start_time[0] = '\0';
+}
+
/*
* get_formatted_start_time -- compute and get the start timestamp.
*
@@ -2786,241 +2792,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-static void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
- char *start_time;
- char *log_time;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /* timestamp with milliseconds */
- log_time = get_formatted_log_time();
- appendStringInfoString(&buf, log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- start_time = get_formatted_start_time();
- appendStringInfoString(&buf, start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- print_stmt = check_log_of_query(edata);
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- appendCSVLiteral(&buf, get_backend_type_for_log());
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
@@ -3274,7 +3045,7 @@ send_message_to_server_log(ErrorData *edata)
* warning from ignoring write()'s result, so do a little dance with casting
* rc to void to shut up the compiler.
*/
-static void
+void
write_pipe_chunks(char *data, int len, int dest)
{
PipeProtoChunk p;
@@ -3510,7 +3281,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
--
2.33.0
v4-0005-JSON-logging.patchtext/x-diff; charset=us-asciiDownload
From 77687eb0fa733cc6990265d1918b79a82a185e24 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:59:44 +0900
Subject: [PATCH v4 5/5] JSON logging
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog.h | 2 +
src/backend/postmaster/syslogger.c | 107 ++++++++--
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 18 ++
src/backend/utils/error/jsonlog.c | 323 +++++++++++++++++++++++++++++
src/backend/utils/misc/guc.c | 2 +
src/bin/pg_ctl/t/004_logrotate.pl | 12 +-
doc/src/sgml/config.sgml | 67 +++++-
10 files changed, 514 insertions(+), 26 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index c79dfbeba2..18448b76e5 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index ab08e7fac8..87b3398ca6 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
@@ -448,6 +449,7 @@ extern bool check_log_of_query(ErrorData *edata);
extern const char *get_backend_type_for_log(void);
extern const char *error_severity(int elevel);
extern void write_csvlog(ErrorData *edata);
+extern void write_jsonlog(ErrorData *edata);
extern void write_pipe_chunks(char *data, int len, int dest);
#ifdef HAVE_SYSLOG
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 4a019db7f4..9d5de35a44 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -277,6 +279,8 @@ SysLoggerMain(int argc, char *argv[])
last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -363,6 +367,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -413,6 +425,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -422,7 +440,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -628,6 +646,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -725,6 +757,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -801,6 +838,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -813,6 +851,9 @@ syslogger_forkexec(void)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ syslogger_fdget(jsonlogFile));
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -830,8 +871,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -844,6 +885,8 @@ syslogger_parseArgs(int argc, char *argv[])
syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
csvlogFile = syslogger_fdopen(fd);
+ fd = atoi(*argv++);
+ jsonlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
@@ -892,7 +935,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -914,6 +959,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1093,19 +1140,24 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
+ * If we're told to write to a structured log file, but it's not open,
+ * dump the data to syslogFile (which is always open) instead. This can
+ * happen if structured output is enabled after postmaster start and
+ * we've been unable to open logFile. There are also race conditions
+ * during a parameter change whereby backends might send us structured
+ * output before we open the logFile or after we close it. Writing
+ * formatted output to the regular log file isn't great, but it beats
+ * dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1176,7 +1228,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1291,6 +1344,8 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
logFileExt = NULL;
else if (target_dest == LOG_DESTINATION_CSVLOG)
logFileExt = ".csv";
+ else if (target_dest == LOG_DESTINATION_JSONLOG)
+ logFileExt = ".json";
else
{
/* cannot happen */
@@ -1378,6 +1433,12 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
&csvlogFile))
return;
+ /* file rotation for csvlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_JSONLOG, &last_json_file_name,
+ &jsonlogFile))
+ return;
+
update_metainfo_datafile();
set_next_rotation_time();
@@ -1464,7 +1525,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1522,6 +1584,19 @@ update_metainfo_datafile(void)
return;
}
}
+
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index f99a173e15..c96bb3d527 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2986,6 +2986,22 @@ send_message_to_server_log(ErrorData *edata)
fallback_to_stderr = true;
}
+ /* Write to JSON log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_JSONLOG) != 0)
+ {
+ /*
+ * Send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written
+ * to stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_jsonlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
/*
* Write to stderr, if enabled or if required because of a previous
* limitation.
@@ -3061,6 +3077,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..f92ee64a47
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,323 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+static void appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ const char *fmt,...) pg_attribute_printf(3, 4);
+
+/*
+ * appendJSONKeyValue
+ * Append to given StringInfo a comma followed by a JSON key and value.
+ * Both the key and value will be escaped as JSON string literals.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value)
+{
+ if (value == NULL)
+ return;
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ escape_json(buf, value);
+}
+
+/*
+ * appendJSONKeyValueFmt
+ *
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /*
+ * Allocate result buffer. Note that in frontend this maps to malloc
+ * with exit-on-error.
+ */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+ appendJSONKeyValue(buf, key, value);
+
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a signed integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%d", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as an unsigned integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsUInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%u", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a long (a JSON number).
+ */
+static void
+appendJSONKeyValueAsLong(StringInfo buf, const char *key, long value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%ld", value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and jsonlog messages.
+ */
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+
+ /*
+ * First property does not use appendJSONKeyValue as it does not have
+ * comma prefix.
+ */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueAsInt(&buf, "leader_pid", leader->pid);
+ }
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueAsInt(&buf, "pid", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueAsLong(&buf, "line_num", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", start_time);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity", (char *) error_severity(edata->elevel));
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", "%lld", (long long) pgstat_get_my_query_id());
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code", unpack_sql_state(edata->sqlerrcode));
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log);
+ else if (edata->detail)
+ appendJSONKeyValue(&buf, "detail", edata->detail);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint);
+
+ /* Internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery);
+
+ /* If the internal query got printed, print internal pos, too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueAsUInt(&buf, "internal_position", edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (check_log_of_query(edata))
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueAsInt(&buf, "cursor_position", edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename);
+ appendJSONKeyValueAsInt(&buf, "file_line_num", edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log());
+
+ /* Error message */
+ appendJSONKeyValue(&buf, "message", edata->message);
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d2ce4a8450..0b79753ec3 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -11714,6 +11714,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index aa0d64a4f7..caa5740254 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 10;
+use Test::More tests => 14;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -63,7 +63,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -94,11 +94,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -120,7 +122,8 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
@@ -128,5 +131,6 @@ $node->psql('postgres', 'fee fi fo fum');
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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0a8e35c59f..7723e11cf7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5926,7 +5926,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5944,6 +5945,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ CSV-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5955,13 +5964,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6101,6 +6111,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7433,6 +7450,50 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ Each log line is serialized as a JSON object with the following fields:
+<programlisting>
+ {
+ "timestamp": time stamp with milliseconds (string),
+ "user": user name (string),
+ "dbname": database name (string),
+ "pid": process ID (number),
+ "remote_host": client host (string)
+ "remote_port": port number (string),
+ "session_id": session ID (string),
+ "line_num": per-session line number (number),
+ "ps": current ps display (string),
+ "session_start": session start time (string),
+ "vxid": virtual transaction ID (string),
+ "txid": regular transaction ID (string),
+ "error_severity": error severity (string),
+ "state_code": SQLSTATE code (string),
+ "detail": error message detail (string),
+ "hint": hint (string),
+ "internal_query": internal query that led to the error (string),
+ "internal_position": cursor index into internal query (number),
+ "context": error context (string),
+ "statement": client supplied query string (string),
+ "cursor_position": cursor index into query string (string),
+ "func_name": error location function name (string),
+ "file_name": error location file name (string),
+ "file_line_num": error location file line number (number),
+ "application_name": client application name (string),
+ "message": error message (string)
+ }
+</programlisting>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.33.0
On 10/5/21, 12:22 AM, "Michael Paquier" <michael@paquier.xyz> wrote:
- 0001 does some refactoring of the file rotation in syslogger.c,
that's the same patch as previously posted.
My compiler is unhappy with 5c6e33f:
syslogger.c: In function ‘logfile_rotate_dest’:
syslogger.c:1302:11: warning: ‘logFileExt’ may be used uninitialized in this function [-Wmaybe-uninitialized]
filename = logfile_getname(fntime, logFileExt);
~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The attached patch seems to fix it.
Nathan
Attachments:
fix_warning.patchapplication/octet-stream; name=fix_warning.patchDownload
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 34b16ba3bb..d1f56b95a4 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -1258,7 +1258,7 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
pg_time_t fntime, int target_dest,
char **last_file_name, FILE **logFile)
{
- char *logFileExt;
+ char *logFileExt = NULL;
char *filename;
FILE *fh;
On Thu, Oct 07, 2021 at 06:00:08AM +0000, Bossart, Nathan wrote:
The attached patch seems to fix it.
Thanks, sorry about that. I was able to see that once I compiled
without assertions.
--
Michael
On Tue, Oct 05, 2021 at 04:18:17PM +0900, Michael Paquier wrote:
0002 and 0004 could be more polished and most of their pieces had
better be squashed together. 0003, though, would improve the case of
WIN32 where only csvlog is enabled so as log entries are properly
redirected to the event logs if the redirection is not done yet. I'd
like to move on with 0001 and 0003 as independent pieces.
0001 and 0003 have been applied independently, attached is a rebased
version.
--
Michael
Attachments:
v5-0001-Some-refactoring-of-elog-specific-routines.patchtext/x-diff; charset=us-asciiDownload
From 70317f6603923347d95e22404fdd53842c6fc1a5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 11:19:46 +0900
Subject: [PATCH v5 1/3] Some refactoring of elog-specific routines
This refactors out the following things in elog.c, for ease of use
across multiple log destinations:
- start_timestamp
- log_timestamp
- decide if query can be logged
- backend type
This will be reused by jsonlog.
---
src/include/utils/elog.h | 4 +
src/backend/utils/error/elog.c | 141 +++++++++++++++++++++------------
2 files changed, 94 insertions(+), 51 deletions(-)
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..bc6f362b6a 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -441,6 +441,10 @@ extern bool syslog_split_messages;
extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+extern char *get_formatted_start_time(void);
+extern char *get_formatted_log_time(void);
+extern bool check_log_of_query(ErrorData *edata);
+extern const char *get_backend_type_for_log(void);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index f33729513a..87e911ad28 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -175,8 +175,6 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
static void write_csvlog(ErrorData *edata);
@@ -2289,14 +2287,23 @@ write_console(const char *line, int len)
}
/*
- * setup formatted_log_time, for consistent times between CSV and regular logs
+ * get_formatted_log_time -- compute and get the log timestamp.
+ *
+ * The timestamp is computed if not set yet, so as it is kept consistent
+ * among all the log destinations that require it to be consistent. Note
+ * that the computed timestamp is returned in a static buffer, not
+ * palloc()'d.
*/
-static void
-setup_formatted_log_time(void)
+char *
+get_formatted_log_time(void)
{
pg_time_t stamp_time;
char msbuf[13];
+ /* leave if already computed */
+ if (formatted_log_time[0] != '\0')
+ return formatted_log_time;
+
if (!saved_timeval_set)
{
gettimeofday(&saved_timeval, NULL);
@@ -2318,16 +2325,25 @@ setup_formatted_log_time(void)
/* 'paste' milliseconds into place... */
sprintf(msbuf, ".%03d", (int) (saved_timeval.tv_usec / 1000));
memcpy(formatted_log_time + 19, msbuf, 4);
+
+ return formatted_log_time;
}
/*
- * setup formatted_start_time
+ * get_formatted_start_time -- compute and get the start timestamp.
+ *
+ * The timestamp is computed if not set yet. Note that the computed
+ * timestamp is returned in a static buffer, not palloc()'d.
*/
-static void
-setup_formatted_start_time(void)
+char *
+get_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
+ /* leave if already computed */
+ if (formatted_start_time[0] != '\0')
+ return formatted_start_time;
+
/*
* Note: we expect that guc.c will ensure that log_timezone is set up (at
* least with a minimal GMT value) before Log_line_prefix can become
@@ -2336,6 +2352,49 @@ setup_formatted_start_time(void)
pg_strftime(formatted_start_time, FORMATTED_TS_LEN,
"%Y-%m-%d %H:%M:%S %Z",
pg_localtime(&stamp_time, log_timezone));
+
+ return formatted_start_time;
+}
+
+/*
+ * check_log_of_query -- check if a query can be logged
+ */
+bool
+check_log_of_query(ErrorData *edata)
+{
+ /* log required? */
+ if (!is_log_level_output(edata->elevel, log_min_error_statement))
+ return false;
+
+ /* query log wanted? */
+ if (edata->hide_stmt)
+ return false;
+
+ /* query string available? */
+ if (debug_query_string == NULL)
+ return false;
+
+ return true;
+}
+
+/*
+ * get_backend_type_for_log -- backend type for log entries
+ *
+ * Returns a pointer to a static buffer, not palloc()'d.
+ */
+const char *
+get_backend_type_for_log(void)
+{
+ const char *backend_type_str;
+
+ if (MyProcPid == PostmasterPid)
+ backend_type_str = "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ backend_type_str = MyBgworkerEntry->bgw_type;
+ else
+ backend_type_str = GetBackendTypeDesc(MyBackendType);
+
+ return backend_type_str;
}
/*
@@ -2466,14 +2525,7 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
break;
case 'b':
{
- const char *backend_type_str;
-
- if (MyProcPid == PostmasterPid)
- backend_type_str = "postmaster";
- else if (MyBackendType == B_BG_WORKER)
- backend_type_str = MyBgworkerEntry->bgw_type;
- else
- backend_type_str = GetBackendTypeDesc(MyBackendType);
+ const char *backend_type_str = get_backend_type_for_log();
if (padding != 0)
appendStringInfo(buf, "%*s", padding, backend_type_str);
@@ -2561,7 +2613,10 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
appendStringInfo(buf, "%ld", log_line_number);
break;
case 'm':
- setup_formatted_log_time();
+ /* force a log timestamp reset */
+ formatted_log_time[0] = '\0';
+ (void) get_formatted_log_time();
+
if (padding != 0)
appendStringInfo(buf, "%*s", padding, formatted_log_time);
else
@@ -2602,12 +2657,14 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
break;
case 's':
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- if (padding != 0)
- appendStringInfo(buf, "%*s", padding, formatted_start_time);
- else
- appendStringInfoString(buf, formatted_start_time);
+ {
+ char *start_time = get_formatted_start_time();
+
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, start_time);
+ else
+ appendStringInfoString(buf, start_time);
+ }
break;
case 'i':
if (MyProcPort)
@@ -2763,6 +2820,8 @@ write_csvlog(ErrorData *edata)
{
StringInfoData buf;
bool print_stmt = false;
+ char *start_time;
+ char *log_time;
/* static counter for line numbers */
static long log_line_number = 0;
@@ -2785,17 +2844,9 @@ write_csvlog(ErrorData *edata)
initStringInfo(&buf);
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
appendStringInfoChar(&buf, ',');
/* username */
@@ -2853,9 +2904,8 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
appendStringInfoChar(&buf, ',');
/* Virtual transaction id */
@@ -2906,10 +2956,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
+ print_stmt = check_log_of_query(edata);
if (print_stmt)
appendCSVLiteral(&buf, debug_query_string);
appendStringInfoChar(&buf, ',');
@@ -2943,13 +2990,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
+ appendCSVLiteral(&buf, get_backend_type_for_log());
appendStringInfoChar(&buf, ',');
/* leader PID */
@@ -3101,9 +3142,7 @@ send_message_to_server_log(ErrorData *edata)
/*
* If the user wants the query that generated this error logged, do it.
*/
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
+ if (check_log_of_query(edata))
{
log_line_prefix(&buf, edata);
appendStringInfoString(&buf, _("STATEMENT: "));
--
2.33.0
v5-0002-Refactor-CSV-specific-code-into-its-own-file.patchtext/x-diff; charset=us-asciiDownload
From ebf79e3275609d93f5145fed5a7d3909a39946b4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:22:24 +0900
Subject: [PATCH v5 2/3] Refactor CSV-specific code into its own file
This requires more routines of elog.c to be published.
---
src/include/utils/elog.h | 4 +
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 268 +++++++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 251 ++---------------------------
4 files changed, 284 insertions(+), 240 deletions(-)
create mode 100644 src/backend/utils/error/csvlog.c
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index bc6f362b6a..ab08e7fac8 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -441,10 +441,14 @@ extern bool syslog_split_messages;
extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+extern void reset_formatted_start_time(void);
extern char *get_formatted_start_time(void);
extern char *get_formatted_log_time(void);
extern bool check_log_of_query(ErrorData *edata);
extern const char *get_backend_type_for_log(void);
+extern const char *error_severity(int elevel);
+extern void write_csvlog(ErrorData *edata);
+extern void write_pipe_chunks(char *data, int len, int dest);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..443a91e168
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,268 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of Californi
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * write_csvlog -- Generate and write CSV log entry
+ *
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ print_stmt = check_log_of_query(edata);
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 87e911ad28..4406d8deff 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -177,11 +177,8 @@ static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *st
static void write_console(const char *line, int len);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
-static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
@@ -2329,6 +2326,15 @@ get_formatted_log_time(void)
return formatted_log_time;
}
+/*
+ * reset_formatted_start_time -- reset the start timestamp
+ */
+void
+reset_formatted_start_time(void)
+{
+ formatted_start_time[0] = '\0';
+}
+
/*
* get_formatted_start_time -- compute and get the start timestamp.
*
@@ -2786,241 +2792,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-static void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
- char *start_time;
- char *log_time;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /* timestamp with milliseconds */
- log_time = get_formatted_log_time();
- appendStringInfoString(&buf, log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- start_time = get_formatted_start_time();
- appendStringInfoString(&buf, start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- print_stmt = check_log_of_query(edata);
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- appendCSVLiteral(&buf, get_backend_type_for_log());
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
@@ -3272,7 +3043,7 @@ send_message_to_server_log(ErrorData *edata)
* warning from ignoring write()'s result, so do a little dance with casting
* rc to void to shut up the compiler.
*/
-static void
+void
write_pipe_chunks(char *data, int len, int dest)
{
PipeProtoChunk p;
@@ -3508,7 +3279,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
--
2.33.0
v5-0003-JSON-logging.patchtext/x-diff; charset=us-asciiDownload
From 7a0d22829d0aff3c53d4bc95bce8a3af0f19d02a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:59:44 +0900
Subject: [PATCH v5 3/3] JSON logging
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog.h | 2 +
src/backend/postmaster/syslogger.c | 107 ++++++++--
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 18 ++
src/backend/utils/error/jsonlog.c | 323 +++++++++++++++++++++++++++++
src/backend/utils/misc/guc.c | 2 +
src/bin/pg_ctl/t/004_logrotate.pl | 12 +-
doc/src/sgml/config.sgml | 67 +++++-
10 files changed, 514 insertions(+), 26 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index c79dfbeba2..18448b76e5 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index ab08e7fac8..87b3398ca6 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
@@ -448,6 +449,7 @@ extern bool check_log_of_query(ErrorData *edata);
extern const char *get_backend_type_for_log(void);
extern const char *error_severity(int elevel);
extern void write_csvlog(ErrorData *edata);
+extern void write_jsonlog(ErrorData *edata);
extern void write_pipe_chunks(char *data, int len, int dest);
#ifdef HAVE_SYSLOG
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index d1f56b95a4..449d327268 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -281,6 +283,8 @@ SysLoggerMain(int argc, char *argv[])
last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -367,6 +371,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -417,6 +429,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -426,7 +444,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -632,6 +650,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -729,6 +761,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -805,6 +842,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -817,6 +855,9 @@ syslogger_forkexec(void)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ syslogger_fdget(jsonlogFile));
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -834,8 +875,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -848,6 +889,8 @@ syslogger_parseArgs(int argc, char *argv[])
syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
csvlogFile = syslogger_fdopen(fd);
+ fd = atoi(*argv++);
+ jsonlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
@@ -896,7 +939,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -918,6 +963,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1097,19 +1144,24 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
+ * If we're told to write to a structured log file, but it's not open,
+ * dump the data to syslogFile (which is always open) instead. This can
+ * happen if structured output is enabled after postmaster start and
+ * we've been unable to open logFile. There are also race conditions
+ * during a parameter change whereby backends might send us structured
+ * output before we open the logFile or after we close it. Writing
+ * formatted output to the regular log file isn't great, but it beats
+ * dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1180,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1292,6 +1345,8 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
logFileExt = NULL;
else if (target_dest == LOG_DESTINATION_CSVLOG)
logFileExt = ".csv";
+ else if (target_dest == LOG_DESTINATION_JSONLOG)
+ logFileExt = ".json";
else
{
/* cannot happen */
@@ -1379,6 +1434,12 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
&csvlogFile))
return;
+ /* file rotation for csvlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_JSONLOG, &last_json_file_name,
+ &jsonlogFile))
+ return;
+
update_metainfo_datafile();
set_next_rotation_time();
@@ -1465,7 +1526,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1523,6 +1585,19 @@ update_metainfo_datafile(void)
return;
}
}
+
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 4406d8deff..39467be917 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2984,6 +2984,22 @@ send_message_to_server_log(ErrorData *edata)
fallback_to_stderr = true;
}
+ /* Write to JSON log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_JSONLOG) != 0)
+ {
+ /*
+ * Send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written
+ * to stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_jsonlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
/*
* Write to stderr, if enabled or if required because of a previous
* limitation.
@@ -3059,6 +3075,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..f92ee64a47
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,323 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+static void appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ const char *fmt,...) pg_attribute_printf(3, 4);
+
+/*
+ * appendJSONKeyValue
+ * Append to given StringInfo a comma followed by a JSON key and value.
+ * Both the key and value will be escaped as JSON string literals.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value)
+{
+ if (value == NULL)
+ return;
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ escape_json(buf, value);
+}
+
+/*
+ * appendJSONKeyValueFmt
+ *
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /*
+ * Allocate result buffer. Note that in frontend this maps to malloc
+ * with exit-on-error.
+ */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+ appendJSONKeyValue(buf, key, value);
+
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a signed integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%d", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as an unsigned integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsUInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%u", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a long (a JSON number).
+ */
+static void
+appendJSONKeyValueAsLong(StringInfo buf, const char *key, long value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%ld", value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and jsonlog messages.
+ */
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+
+ /*
+ * First property does not use appendJSONKeyValue as it does not have
+ * comma prefix.
+ */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueAsInt(&buf, "leader_pid", leader->pid);
+ }
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueAsInt(&buf, "pid", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueAsLong(&buf, "line_num", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", start_time);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity", (char *) error_severity(edata->elevel));
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", "%lld", (long long) pgstat_get_my_query_id());
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code", unpack_sql_state(edata->sqlerrcode));
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log);
+ else if (edata->detail)
+ appendJSONKeyValue(&buf, "detail", edata->detail);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint);
+
+ /* Internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery);
+
+ /* If the internal query got printed, print internal pos, too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueAsUInt(&buf, "internal_position", edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (check_log_of_query(edata))
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueAsInt(&buf, "cursor_position", edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename);
+ appendJSONKeyValueAsInt(&buf, "file_line_num", edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log());
+
+ /* Error message */
+ appendJSONKeyValue(&buf, "message", edata->message);
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d2ce4a8450..0b79753ec3 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -11714,6 +11714,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index aa0d64a4f7..caa5740254 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 10;
+use Test::More tests => 14;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -63,7 +63,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -94,11 +94,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -120,7 +122,8 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
@@ -128,5 +131,6 @@ $node->psql('postgres', 'fee fi fo fum');
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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0a8e35c59f..7723e11cf7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5926,7 +5926,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5944,6 +5945,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ CSV-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5955,13 +5964,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6101,6 +6111,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7433,6 +7450,50 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ Each log line is serialized as a JSON object with the following fields:
+<programlisting>
+ {
+ "timestamp": time stamp with milliseconds (string),
+ "user": user name (string),
+ "dbname": database name (string),
+ "pid": process ID (number),
+ "remote_host": client host (string)
+ "remote_port": port number (string),
+ "session_id": session ID (string),
+ "line_num": per-session line number (number),
+ "ps": current ps display (string),
+ "session_start": session start time (string),
+ "vxid": virtual transaction ID (string),
+ "txid": regular transaction ID (string),
+ "error_severity": error severity (string),
+ "state_code": SQLSTATE code (string),
+ "detail": error message detail (string),
+ "hint": hint (string),
+ "internal_query": internal query that led to the error (string),
+ "internal_position": cursor index into internal query (number),
+ "context": error context (string),
+ "statement": client supplied query string (string),
+ "cursor_position": cursor index into query string (string),
+ "func_name": error location function name (string),
+ "file_name": error location file name (string),
+ "file_line_num": error location file line number (number),
+ "application_name": client application name (string),
+ "message": error message (string)
+ }
+</programlisting>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.33.0
On Fri, Oct 08, 2021 at 12:28:58PM +0900, Michael Paquier wrote:
0001 and 0003 have been applied independently, attached is a rebased
version.
Attached are rebased versions of the patch set, where I have done a
cleaner split:
- 0001 includes all the refactoring of the routines from elog.c.
- 0002 moves csv logging into its own file.
- 0003 introduces the JSON log.
0001 and 0002, the refactoring bits, are in a rather committable
shape, so I'd like to apply that as the last refactoring pieces I know
of for this thread. 0003 still needs a closer lookup, and one part I
do not like much in it is the split for [u]int and long values when it
comes to key and values.
--
Michael
Attachments:
v6-0001-Some-refactoring-of-elog-specific-routines.patchtext/x-diff; charset=us-asciiDownload
From 74a4d36e5f6cac1318a14168321c34edcd7d9b86 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 19 Oct 2021 16:25:45 +0900
Subject: [PATCH v6 1/3] Some refactoring of elog-specific routines
This refactors out the following things in elog.c, for ease of use
across multiple log destinations:
- start_timestamp (including reset)
- log_timestamp
- decide if query can be logged
- backend type
- write using the elog piped protocol
- Error severity to string.
These will be reused by csvlog and jsonlog.
---
src/include/utils/elog.h | 12 +++
src/backend/utils/error/elog.c | 159 +++++++++++++++++++++------------
2 files changed, 114 insertions(+), 57 deletions(-)
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..731f3e3cd8 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -442,6 +442,18 @@ extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+/* Common functions shared across destinations */
+extern void reset_formatted_start_time(void);
+extern char *get_formatted_start_time(void);
+extern char *get_formatted_log_time(void);
+extern const char *get_backend_type_for_log(void);
+extern bool check_log_of_query(ErrorData *edata);
+extern const char *error_severity(int elevel);
+extern void write_pipe_chunks(char *data, int len, int dest);
+
+/* Destination-specific functions */
+extern void write_csvlog(ErrorData *edata);
+
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
#endif
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index f33729513a..a162258bab 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -175,15 +175,10 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
-static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
@@ -2289,14 +2284,23 @@ write_console(const char *line, int len)
}
/*
- * setup formatted_log_time, for consistent times between CSV and regular logs
+ * get_formatted_log_time -- compute and get the log timestamp.
+ *
+ * The timestamp is computed if not set yet, so as it is kept consistent
+ * among all the log destinations that require it to be consistent. Note
+ * that the computed timestamp is returned in a static buffer, not
+ * palloc()'d.
*/
-static void
-setup_formatted_log_time(void)
+char *
+get_formatted_log_time(void)
{
pg_time_t stamp_time;
char msbuf[13];
+ /* leave if already computed */
+ if (formatted_log_time[0] != '\0')
+ return formatted_log_time;
+
if (!saved_timeval_set)
{
gettimeofday(&saved_timeval, NULL);
@@ -2318,16 +2322,34 @@ setup_formatted_log_time(void)
/* 'paste' milliseconds into place... */
sprintf(msbuf, ".%03d", (int) (saved_timeval.tv_usec / 1000));
memcpy(formatted_log_time + 19, msbuf, 4);
+
+ return formatted_log_time;
}
/*
- * setup formatted_start_time
+ * reset_formatted_start_time -- reset the start timestamp
*/
-static void
-setup_formatted_start_time(void)
+void
+reset_formatted_start_time(void)
+{
+ formatted_start_time[0] = '\0';
+}
+
+/*
+ * get_formatted_start_time -- compute and get the start timestamp.
+ *
+ * The timestamp is computed if not set yet. Note that the computed
+ * timestamp is returned in a static buffer, not palloc()'d.
+ */
+char *
+get_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
+ /* leave if already computed */
+ if (formatted_start_time[0] != '\0')
+ return formatted_start_time;
+
/*
* Note: we expect that guc.c will ensure that log_timezone is set up (at
* least with a minimal GMT value) before Log_line_prefix can become
@@ -2336,6 +2358,49 @@ setup_formatted_start_time(void)
pg_strftime(formatted_start_time, FORMATTED_TS_LEN,
"%Y-%m-%d %H:%M:%S %Z",
pg_localtime(&stamp_time, log_timezone));
+
+ return formatted_start_time;
+}
+
+/*
+ * check_log_of_query -- check if a query can be logged
+ */
+bool
+check_log_of_query(ErrorData *edata)
+{
+ /* log required? */
+ if (!is_log_level_output(edata->elevel, log_min_error_statement))
+ return false;
+
+ /* query log wanted? */
+ if (edata->hide_stmt)
+ return false;
+
+ /* query string available? */
+ if (debug_query_string == NULL)
+ return false;
+
+ return true;
+}
+
+/*
+ * get_backend_type_for_log -- backend type for log entries
+ *
+ * Returns a pointer to a static buffer, not palloc()'d.
+ */
+const char *
+get_backend_type_for_log(void)
+{
+ const char *backend_type_str;
+
+ if (MyProcPid == PostmasterPid)
+ backend_type_str = "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ backend_type_str = MyBgworkerEntry->bgw_type;
+ else
+ backend_type_str = GetBackendTypeDesc(MyBackendType);
+
+ return backend_type_str;
}
/*
@@ -2466,14 +2531,7 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
break;
case 'b':
{
- const char *backend_type_str;
-
- if (MyProcPid == PostmasterPid)
- backend_type_str = "postmaster";
- else if (MyBackendType == B_BG_WORKER)
- backend_type_str = MyBgworkerEntry->bgw_type;
- else
- backend_type_str = GetBackendTypeDesc(MyBackendType);
+ const char *backend_type_str = get_backend_type_for_log();
if (padding != 0)
appendStringInfo(buf, "%*s", padding, backend_type_str);
@@ -2561,7 +2619,10 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
appendStringInfo(buf, "%ld", log_line_number);
break;
case 'm':
- setup_formatted_log_time();
+ /* force a log timestamp reset */
+ formatted_log_time[0] = '\0';
+ (void) get_formatted_log_time();
+
if (padding != 0)
appendStringInfo(buf, "%*s", padding, formatted_log_time);
else
@@ -2602,12 +2663,14 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
break;
case 's':
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- if (padding != 0)
- appendStringInfo(buf, "%*s", padding, formatted_start_time);
- else
- appendStringInfoString(buf, formatted_start_time);
+ {
+ char *start_time = get_formatted_start_time();
+
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, start_time);
+ else
+ appendStringInfoString(buf, start_time);
+ }
break;
case 'i':
if (MyProcPort)
@@ -2758,11 +2821,13 @@ appendCSVLiteral(StringInfo buf, const char *data)
* Constructs the error message, depending on the Errordata it gets, in a CSV
* format which is described in doc/src/sgml/config.sgml.
*/
-static void
+void
write_csvlog(ErrorData *edata)
{
StringInfoData buf;
bool print_stmt = false;
+ char *start_time;
+ char *log_time;
/* static counter for line numbers */
static long log_line_number = 0;
@@ -2785,17 +2850,9 @@ write_csvlog(ErrorData *edata)
initStringInfo(&buf);
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
appendStringInfoChar(&buf, ',');
/* username */
@@ -2853,9 +2910,8 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
appendStringInfoChar(&buf, ',');
/* Virtual transaction id */
@@ -2906,10 +2962,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
+ print_stmt = check_log_of_query(edata);
if (print_stmt)
appendCSVLiteral(&buf, debug_query_string);
appendStringInfoChar(&buf, ',');
@@ -2943,13 +2996,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
+ appendCSVLiteral(&buf, get_backend_type_for_log());
appendStringInfoChar(&buf, ',');
/* leader PID */
@@ -3101,9 +3148,7 @@ send_message_to_server_log(ErrorData *edata)
/*
* If the user wants the query that generated this error logged, do it.
*/
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
+ if (check_log_of_query(edata))
{
log_line_prefix(&buf, edata);
appendStringInfoString(&buf, _("STATEMENT: "));
@@ -3233,7 +3278,7 @@ send_message_to_server_log(ErrorData *edata)
* warning from ignoring write()'s result, so do a little dance with casting
* rc to void to shut up the compiler.
*/
-static void
+void
write_pipe_chunks(char *data, int len, int dest)
{
PipeProtoChunk p;
@@ -3469,7 +3514,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
--
2.33.0
v6-0002-Refactor-CSV-specific-code-into-its-own-file.patchtext/x-diff; charset=us-asciiDownload
From 17af8a58c8286c7af347a95329769dd7d6c805c0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:22:24 +0900
Subject: [PATCH v6 2/3] Refactor CSV-specific code into its own file
---
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 268 +++++++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 235 ---------------------------
3 files changed, 269 insertions(+), 235 deletions(-)
create mode 100644 src/backend/utils/error/csvlog.c
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..443a91e168
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,268 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of Californi
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * write_csvlog -- Generate and write CSV log entry
+ *
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ print_stmt = check_log_of_query(edata);
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a162258bab..4406d8deff 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2792,241 +2792,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
- char *start_time;
- char *log_time;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /* timestamp with milliseconds */
- log_time = get_formatted_log_time();
- appendStringInfoString(&buf, log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- start_time = get_formatted_start_time();
- appendStringInfoString(&buf, start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- print_stmt = check_log_of_query(edata);
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- appendCSVLiteral(&buf, get_backend_type_for_log());
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
--
2.33.0
v6-0003-JSON-logging.patchtext/x-diff; charset=us-asciiDownload
From c3a46da88ed75090d8582961c9c05e1ca887af5a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:59:44 +0900
Subject: [PATCH v6 3/3] JSON logging
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog.h | 2 +
src/backend/postmaster/syslogger.c | 107 ++++++++--
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 18 ++
src/backend/utils/error/jsonlog.c | 323 +++++++++++++++++++++++++++++
src/backend/utils/misc/guc.c | 2 +
src/bin/pg_ctl/t/004_logrotate.pl | 12 +-
doc/src/sgml/config.sgml | 67 +++++-
10 files changed, 514 insertions(+), 26 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index c79dfbeba2..18448b76e5 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 731f3e3cd8..a32c790421 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
@@ -453,6 +454,7 @@ extern void write_pipe_chunks(char *data, int len, int dest);
/* Destination-specific functions */
extern void write_csvlog(ErrorData *edata);
+extern void write_jsonlog(ErrorData *edata);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index d1f56b95a4..449d327268 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -281,6 +283,8 @@ SysLoggerMain(int argc, char *argv[])
last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -367,6 +371,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -417,6 +429,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -426,7 +444,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -632,6 +650,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -729,6 +761,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -805,6 +842,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -817,6 +855,9 @@ syslogger_forkexec(void)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ syslogger_fdget(jsonlogFile));
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -834,8 +875,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -848,6 +889,8 @@ syslogger_parseArgs(int argc, char *argv[])
syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
csvlogFile = syslogger_fdopen(fd);
+ fd = atoi(*argv++);
+ jsonlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
@@ -896,7 +939,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -918,6 +963,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1097,19 +1144,24 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
+ * If we're told to write to a structured log file, but it's not open,
+ * dump the data to syslogFile (which is always open) instead. This can
+ * happen if structured output is enabled after postmaster start and
+ * we've been unable to open logFile. There are also race conditions
+ * during a parameter change whereby backends might send us structured
+ * output before we open the logFile or after we close it. Writing
+ * formatted output to the regular log file isn't great, but it beats
+ * dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1180,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1292,6 +1345,8 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
logFileExt = NULL;
else if (target_dest == LOG_DESTINATION_CSVLOG)
logFileExt = ".csv";
+ else if (target_dest == LOG_DESTINATION_JSONLOG)
+ logFileExt = ".json";
else
{
/* cannot happen */
@@ -1379,6 +1434,12 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
&csvlogFile))
return;
+ /* file rotation for csvlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_JSONLOG, &last_json_file_name,
+ &jsonlogFile))
+ return;
+
update_metainfo_datafile();
set_next_rotation_time();
@@ -1465,7 +1526,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1523,6 +1585,19 @@ update_metainfo_datafile(void)
return;
}
}
+
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 4406d8deff..39467be917 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2984,6 +2984,22 @@ send_message_to_server_log(ErrorData *edata)
fallback_to_stderr = true;
}
+ /* Write to JSON log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_JSONLOG) != 0)
+ {
+ /*
+ * Send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written
+ * to stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_jsonlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
/*
* Write to stderr, if enabled or if required because of a previous
* limitation.
@@ -3059,6 +3075,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..f92ee64a47
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,323 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+static void appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ const char *fmt,...) pg_attribute_printf(3, 4);
+
+/*
+ * appendJSONKeyValue
+ * Append to given StringInfo a comma followed by a JSON key and value.
+ * Both the key and value will be escaped as JSON string literals.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value)
+{
+ if (value == NULL)
+ return;
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ escape_json(buf, value);
+}
+
+/*
+ * appendJSONKeyValueFmt
+ *
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /*
+ * Allocate result buffer. Note that in frontend this maps to malloc
+ * with exit-on-error.
+ */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+ appendJSONKeyValue(buf, key, value);
+
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a signed integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%d", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as an unsigned integer (a JSON number).
+ */
+static void
+appendJSONKeyValueAsUInt(StringInfo buf, const char *key, int value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%u", value);
+}
+
+/*
+ * appendJSONKeyValueAsInt
+ * Append to given StringInfo a comma followed by a JSON key and value with
+ * value being formatted as a long (a JSON number).
+ */
+static void
+appendJSONKeyValueAsLong(StringInfo buf, const char *key, long value)
+{
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+ appendStringInfo(buf, "%ld", value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /*
+ * timestamp with milliseconds
+ *
+ * Check if the timestamp is already calculated for the syslog message,
+ * and use it if so. Otherwise, get the current timestamp. This is done
+ * to put same timestamp in both syslog and jsonlog messages.
+ */
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+
+ /*
+ * First property does not use appendJSONKeyValue as it does not have
+ * comma prefix.
+ */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueAsInt(&buf, "leader_pid", leader->pid);
+ }
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueAsInt(&buf, "pid", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueAsLong(&buf, "line_num", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", start_time);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity", (char *) error_severity(edata->elevel));
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", "%lld", (long long) pgstat_get_my_query_id());
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code", unpack_sql_state(edata->sqlerrcode));
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log);
+ else if (edata->detail)
+ appendJSONKeyValue(&buf, "detail", edata->detail);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint);
+
+ /* Internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery);
+
+ /* If the internal query got printed, print internal pos, too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueAsUInt(&buf, "internal_position", edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (check_log_of_query(edata))
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueAsInt(&buf, "cursor_position", edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename);
+ appendJSONKeyValueAsInt(&buf, "file_line_num", edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log());
+
+ /* Error message */
+ appendJSONKeyValue(&buf, "message", edata->message);
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d2ce4a8450..0b79753ec3 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -11714,6 +11714,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index 13e91f3bc9..5f05fd2bad 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 10;
+use Test::More tests => 14;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -65,7 +65,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -96,11 +96,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -122,7 +124,8 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
@@ -130,5 +133,6 @@ $node->psql('postgres', 'fee fi fo fum');
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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0bcc6fd322..72a8204818 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5926,7 +5926,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5944,6 +5945,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ CSV-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5955,13 +5964,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6101,6 +6111,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7433,6 +7450,50 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ Each log line is serialized as a JSON object with the following fields:
+<programlisting>
+ {
+ "timestamp": time stamp with milliseconds (string),
+ "user": user name (string),
+ "dbname": database name (string),
+ "pid": process ID (number),
+ "remote_host": client host (string)
+ "remote_port": port number (string),
+ "session_id": session ID (string),
+ "line_num": per-session line number (number),
+ "ps": current ps display (string),
+ "session_start": session start time (string),
+ "vxid": virtual transaction ID (string),
+ "txid": regular transaction ID (string),
+ "error_severity": error severity (string),
+ "state_code": SQLSTATE code (string),
+ "detail": error message detail (string),
+ "hint": hint (string),
+ "internal_query": internal query that led to the error (string),
+ "internal_position": cursor index into internal query (number),
+ "context": error context (string),
+ "statement": client supplied query string (string),
+ "cursor_position": cursor index into query string (string),
+ "func_name": error location function name (string),
+ "file_name": error location file name (string),
+ "file_line_num": error location file line number (number),
+ "application_name": client application name (string),
+ "message": error message (string)
+ }
+</programlisting>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.33.0
On Tue, Oct 19, 2021 at 08:02:02PM +0900, Michael Paquier wrote:
0001 and 0002, the refactoring bits, are in a rather committable
shape, so I'd like to apply that as the last refactoring pieces I know
of for this thread. 0003 still needs a closer lookup, and one part I
do not like much in it is the split for [u]int and long values when it
comes to key and values.
I have finally come around 0003 and reviewed it. There were a couple
of issues within it, from complications in the code that did not feel
necessary to incorrect handling of the values logged, mostly around
when values should be escaped or not. jsonlog.c has been reorganized
so as its fields match with csvlog.c, and I have simplified the APIs
in charge of saving the integers into a single one with an argument
list and an option to control if the value should be escaped or not.
postgresql.conf.sample also needed a refresh.
I have also spent some time on the documentation, where the list of
JSON keys with their descriptions and types has been changed to a
table, for clarity. The list was a bit incorrect (incorrect fields
and missing entries), so that should hopefully be clean now.
Patch 0003 has been heavily reworked, and it would be good to have an
extra pair of eyes on it. So I have switched the CF entry as "Needs
Review" and added my name to the list of authors (originally this
stuff took code portions of own module, as well).
--
Michael
Attachments:
v7-0001-Some-refactoring-of-elog-specific-routines.patchtext/x-diff; charset=us-asciiDownload
From 6d71178c0809a5ed9bc131f6d601411ec2f2ec9f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 19 Oct 2021 16:25:45 +0900
Subject: [PATCH v7 1/3] Some refactoring of elog-specific routines
This refactors out the following things in elog.c, for ease of use
across multiple log destinations:
- start_timestamp (including reset)
- log_timestamp
- decide if query can be logged
- backend type
- write using the elog piped protocol
- Error severity to string.
These will be reused by csvlog and jsonlog.
---
src/include/utils/elog.h | 12 +++
src/backend/utils/error/elog.c | 159 +++++++++++++++++++++------------
2 files changed, 114 insertions(+), 57 deletions(-)
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..731f3e3cd8 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -442,6 +442,18 @@ extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+/* Common functions shared across destinations */
+extern void reset_formatted_start_time(void);
+extern char *get_formatted_start_time(void);
+extern char *get_formatted_log_time(void);
+extern const char *get_backend_type_for_log(void);
+extern bool check_log_of_query(ErrorData *edata);
+extern const char *error_severity(int elevel);
+extern void write_pipe_chunks(char *data, int len, int dest);
+
+/* Destination-specific functions */
+extern void write_csvlog(ErrorData *edata);
+
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
#endif
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index f33729513a..a162258bab 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -175,15 +175,10 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
-static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
@@ -2289,14 +2284,23 @@ write_console(const char *line, int len)
}
/*
- * setup formatted_log_time, for consistent times between CSV and regular logs
+ * get_formatted_log_time -- compute and get the log timestamp.
+ *
+ * The timestamp is computed if not set yet, so as it is kept consistent
+ * among all the log destinations that require it to be consistent. Note
+ * that the computed timestamp is returned in a static buffer, not
+ * palloc()'d.
*/
-static void
-setup_formatted_log_time(void)
+char *
+get_formatted_log_time(void)
{
pg_time_t stamp_time;
char msbuf[13];
+ /* leave if already computed */
+ if (formatted_log_time[0] != '\0')
+ return formatted_log_time;
+
if (!saved_timeval_set)
{
gettimeofday(&saved_timeval, NULL);
@@ -2318,16 +2322,34 @@ setup_formatted_log_time(void)
/* 'paste' milliseconds into place... */
sprintf(msbuf, ".%03d", (int) (saved_timeval.tv_usec / 1000));
memcpy(formatted_log_time + 19, msbuf, 4);
+
+ return formatted_log_time;
}
/*
- * setup formatted_start_time
+ * reset_formatted_start_time -- reset the start timestamp
*/
-static void
-setup_formatted_start_time(void)
+void
+reset_formatted_start_time(void)
+{
+ formatted_start_time[0] = '\0';
+}
+
+/*
+ * get_formatted_start_time -- compute and get the start timestamp.
+ *
+ * The timestamp is computed if not set yet. Note that the computed
+ * timestamp is returned in a static buffer, not palloc()'d.
+ */
+char *
+get_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
+ /* leave if already computed */
+ if (formatted_start_time[0] != '\0')
+ return formatted_start_time;
+
/*
* Note: we expect that guc.c will ensure that log_timezone is set up (at
* least with a minimal GMT value) before Log_line_prefix can become
@@ -2336,6 +2358,49 @@ setup_formatted_start_time(void)
pg_strftime(formatted_start_time, FORMATTED_TS_LEN,
"%Y-%m-%d %H:%M:%S %Z",
pg_localtime(&stamp_time, log_timezone));
+
+ return formatted_start_time;
+}
+
+/*
+ * check_log_of_query -- check if a query can be logged
+ */
+bool
+check_log_of_query(ErrorData *edata)
+{
+ /* log required? */
+ if (!is_log_level_output(edata->elevel, log_min_error_statement))
+ return false;
+
+ /* query log wanted? */
+ if (edata->hide_stmt)
+ return false;
+
+ /* query string available? */
+ if (debug_query_string == NULL)
+ return false;
+
+ return true;
+}
+
+/*
+ * get_backend_type_for_log -- backend type for log entries
+ *
+ * Returns a pointer to a static buffer, not palloc()'d.
+ */
+const char *
+get_backend_type_for_log(void)
+{
+ const char *backend_type_str;
+
+ if (MyProcPid == PostmasterPid)
+ backend_type_str = "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ backend_type_str = MyBgworkerEntry->bgw_type;
+ else
+ backend_type_str = GetBackendTypeDesc(MyBackendType);
+
+ return backend_type_str;
}
/*
@@ -2466,14 +2531,7 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
break;
case 'b':
{
- const char *backend_type_str;
-
- if (MyProcPid == PostmasterPid)
- backend_type_str = "postmaster";
- else if (MyBackendType == B_BG_WORKER)
- backend_type_str = MyBgworkerEntry->bgw_type;
- else
- backend_type_str = GetBackendTypeDesc(MyBackendType);
+ const char *backend_type_str = get_backend_type_for_log();
if (padding != 0)
appendStringInfo(buf, "%*s", padding, backend_type_str);
@@ -2561,7 +2619,10 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
appendStringInfo(buf, "%ld", log_line_number);
break;
case 'm':
- setup_formatted_log_time();
+ /* force a log timestamp reset */
+ formatted_log_time[0] = '\0';
+ (void) get_formatted_log_time();
+
if (padding != 0)
appendStringInfo(buf, "%*s", padding, formatted_log_time);
else
@@ -2602,12 +2663,14 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
break;
case 's':
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- if (padding != 0)
- appendStringInfo(buf, "%*s", padding, formatted_start_time);
- else
- appendStringInfoString(buf, formatted_start_time);
+ {
+ char *start_time = get_formatted_start_time();
+
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, start_time);
+ else
+ appendStringInfoString(buf, start_time);
+ }
break;
case 'i':
if (MyProcPort)
@@ -2758,11 +2821,13 @@ appendCSVLiteral(StringInfo buf, const char *data)
* Constructs the error message, depending on the Errordata it gets, in a CSV
* format which is described in doc/src/sgml/config.sgml.
*/
-static void
+void
write_csvlog(ErrorData *edata)
{
StringInfoData buf;
bool print_stmt = false;
+ char *start_time;
+ char *log_time;
/* static counter for line numbers */
static long log_line_number = 0;
@@ -2785,17 +2850,9 @@ write_csvlog(ErrorData *edata)
initStringInfo(&buf);
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
appendStringInfoChar(&buf, ',');
/* username */
@@ -2853,9 +2910,8 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
appendStringInfoChar(&buf, ',');
/* Virtual transaction id */
@@ -2906,10 +2962,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
+ print_stmt = check_log_of_query(edata);
if (print_stmt)
appendCSVLiteral(&buf, debug_query_string);
appendStringInfoChar(&buf, ',');
@@ -2943,13 +2996,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
+ appendCSVLiteral(&buf, get_backend_type_for_log());
appendStringInfoChar(&buf, ',');
/* leader PID */
@@ -3101,9 +3148,7 @@ send_message_to_server_log(ErrorData *edata)
/*
* If the user wants the query that generated this error logged, do it.
*/
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
+ if (check_log_of_query(edata))
{
log_line_prefix(&buf, edata);
appendStringInfoString(&buf, _("STATEMENT: "));
@@ -3233,7 +3278,7 @@ send_message_to_server_log(ErrorData *edata)
* warning from ignoring write()'s result, so do a little dance with casting
* rc to void to shut up the compiler.
*/
-static void
+void
write_pipe_chunks(char *data, int len, int dest)
{
PipeProtoChunk p;
@@ -3469,7 +3514,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
--
2.33.1
v7-0002-Refactor-CSV-specific-code-into-its-own-file.patchtext/x-diff; charset=us-asciiDownload
From a0ab2f6c420a08c65b042ba183394c40dee0d4bb Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:22:24 +0900
Subject: [PATCH v7 2/3] Refactor CSV-specific code into its own file
---
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 268 +++++++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 235 ---------------------------
3 files changed, 269 insertions(+), 235 deletions(-)
create mode 100644 src/backend/utils/error/csvlog.c
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..443a91e168
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,268 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of Californi
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * write_csvlog -- Generate and write CSV log entry
+ *
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ print_stmt = check_log_of_query(edata);
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a162258bab..4406d8deff 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2792,241 +2792,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
- char *start_time;
- char *log_time;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /* timestamp with milliseconds */
- log_time = get_formatted_log_time();
- appendStringInfoString(&buf, log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- start_time = get_formatted_start_time();
- appendStringInfoString(&buf, start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- print_stmt = check_log_of_query(edata);
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- appendCSVLiteral(&buf, get_backend_type_for_log());
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
--
2.33.1
v7-0003-JSON-logging.patchtext/x-diff; charset=us-asciiDownload
From c393518a927d08fd82b6063620f25728edd6131e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 10 Nov 2021 22:40:33 +0900
Subject: [PATCH v7 3/3] JSON logging
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog.h | 2 +
src/backend/postmaster/syslogger.c | 107 ++++++-
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 18 ++
src/backend/utils/error/jsonlog.c | 294 ++++++++++++++++++
src/backend/utils/misc/guc.c | 2 +
src/backend/utils/misc/postgresql.conf.sample | 13 +-
src/bin/pg_ctl/t/004_logrotate.pl | 12 +-
doc/src/sgml/config.sgml | 203 +++++++++++-
11 files changed, 628 insertions(+), 32 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index c79dfbeba2..18448b76e5 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 731f3e3cd8..a32c790421 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
@@ -453,6 +454,7 @@ extern void write_pipe_chunks(char *data, int len, int dest);
/* Destination-specific functions */
extern void write_csvlog(ErrorData *edata);
+extern void write_jsonlog(ErrorData *edata);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index d1f56b95a4..449d327268 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -281,6 +283,8 @@ SysLoggerMain(int argc, char *argv[])
last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -367,6 +371,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -417,6 +429,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -426,7 +444,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -632,6 +650,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -729,6 +761,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -805,6 +842,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -817,6 +855,9 @@ syslogger_forkexec(void)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ syslogger_fdget(jsonlogFile));
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -834,8 +875,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -848,6 +889,8 @@ syslogger_parseArgs(int argc, char *argv[])
syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
csvlogFile = syslogger_fdopen(fd);
+ fd = atoi(*argv++);
+ jsonlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
@@ -896,7 +939,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -918,6 +963,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1097,19 +1144,24 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
+ * If we're told to write to a structured log file, but it's not open,
+ * dump the data to syslogFile (which is always open) instead. This can
+ * happen if structured output is enabled after postmaster start and
+ * we've been unable to open logFile. There are also race conditions
+ * during a parameter change whereby backends might send us structured
+ * output before we open the logFile or after we close it. Writing
+ * formatted output to the regular log file isn't great, but it beats
+ * dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1180,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1292,6 +1345,8 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
logFileExt = NULL;
else if (target_dest == LOG_DESTINATION_CSVLOG)
logFileExt = ".csv";
+ else if (target_dest == LOG_DESTINATION_JSONLOG)
+ logFileExt = ".json";
else
{
/* cannot happen */
@@ -1379,6 +1434,12 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
&csvlogFile))
return;
+ /* file rotation for csvlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_JSONLOG, &last_json_file_name,
+ &jsonlogFile))
+ return;
+
update_metainfo_datafile();
set_next_rotation_time();
@@ -1465,7 +1526,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1523,6 +1585,19 @@ update_metainfo_datafile(void)
return;
}
}
+
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 4406d8deff..39467be917 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2984,6 +2984,22 @@ send_message_to_server_log(ErrorData *edata)
fallback_to_stderr = true;
}
+ /* Write to JSON log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_JSONLOG) != 0)
+ {
+ /*
+ * Send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written
+ * to stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_jsonlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
/*
* Write to stderr, if enabled or if required because of a previous
* limitation.
@@ -3059,6 +3075,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..f2101b59ff
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,294 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+static void appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key,
+ const char *fmt,...) pg_attribute_printf(4, 5);
+
+/*
+ * appendJSONKeyValue
+ * Append to a StringInfo a comma followed by a JSON key and value.
+ * The key is always escaped. The value can be escaped optionally.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value,
+ bool escape_value)
+{
+ Assert(key != NULL);
+
+ if (value == NULL)
+ return;
+
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+
+ if (escape_value)
+ escape_json(buf, value);
+ else
+ appendStringInfoString(buf, value);
+}
+
+
+/*
+ * appendJSONKeyValueFmt
+ *
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /* Allocate result buffer */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+
+ appendJSONKeyValue(buf, key, value, escape_key);
+
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+
+ /*
+ * First property does not use appendJSONKeyValue as it does not have
+ * comma prefix.
+ */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name, true);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name, true);
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueFmt(&buf, "pid", false, "%d", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host, true);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port, false);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", true, "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueFmt(&buf, "line_num", false, "%ld", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data, true);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", start_time, true);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", true, "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", false, "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity",
+ (char *) error_severity(edata->elevel), true);
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code",
+ unpack_sql_state(edata->sqlerrcode), true);
+
+ /* errmessage */
+ appendJSONKeyValue(&buf, "message", edata->message, true);
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log, true);
+ else
+ appendJSONKeyValue(&buf, "detail", edata->detail, true);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint, true);
+
+ /* internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery, true);
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueFmt(&buf, "internal_position", false, "%u",
+ edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context, true);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (check_log_of_query(edata))
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string, true);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueFmt(&buf, "cursor_position", false, "%d",
+ edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname, true);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename, true);
+ appendJSONKeyValueFmt(&buf, "file_line_num", false, "%d",
+ edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name, true);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log(), true);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueFmt(&buf, "leader_pid", false, "%d", leader->pid);
+ }
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", false, "%lld",
+ (long long) pgstat_get_my_query_id());
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e91d5a3cfd..f1f8be805c 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -11727,6 +11727,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 1cbc9feeb6..137827d134 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -432,14 +432,15 @@
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
- # stderr, csvlog, syslog, and eventlog,
- # depending on platform. csvlog
- # requires logging_collector to be on.
+ # stderr, csvlog, jsonlog, syslog, and
+ # eventlog, depending on platform.
+ # csvlog and jsonlog require
+ # logging_collector to be on.
# This is used when logging to stderr:
-#logging_collector = off # Enable capturing of stderr and csvlog
- # into log files. Required to be on for
- # csvlogs.
+#logging_collector = off # Enable capturing of stderr, jsonlog
+ # and csvlog into log files. Required
+ # to be on for csvlogs and jsonlogs.
# (change requires restart)
# These are only used if logging_collector is on:
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index 3813a3b1fd..e2e0a27d73 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
-use Test::More tests => 10;
+use Test::More tests => 14;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -65,7 +65,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -96,11 +96,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -122,7 +124,8 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
@@ -130,5 +133,6 @@ $node->psql('postgres', 'fee fi fo fum');
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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 3f806740d5..7e00cdc988 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5928,7 +5928,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5946,6 +5947,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ JSON-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5957,13 +5966,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6103,6 +6113,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7465,6 +7482,186 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ </para>
+
+ <para>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ <para>
+ Each log line is serialized as a JSON object as of the following
+ set of keys with their values.
+ </para>
+
+ <table>
+ <title>Keys and values of JSON log entries</title>
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Key name</entry>
+ <entry>Type</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>timestamp</literal></entry>
+ <entry>string</entry>
+ <entry>Time stamp with milliseconds</entry>
+ </row>
+ <row>
+ <entry><literal>user</literal></entry>
+ <entry>string</entry>
+ <entry>User name</entry>
+ </row>
+ <row>
+ <entry><literal>dbname</literal></entry>
+ <entry>string</entry>
+ <entry>Database name</entry>
+ </row>
+ <row>
+ <entry><literal>pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID</entry>
+ </row>
+ <row>
+ <entry><literal>remote_host</literal></entry>
+ <entry>string</entry>
+ <entry>Client host</entry>
+ </row>
+ <row>
+ <entry><literal>remote_port</literal></entry>
+ <entry>number</entry>
+ <entry>Client port</entry>
+ </row>
+ <row>
+ <entry><literal>session_id</literal></entry>
+ <entry>string</entry>
+ <entry>Session ID</entry>
+ </row>
+ <row>
+ <entry><literal>line_num</literal></entry>
+ <entry>number</entry>
+ <entry>Per-session line number</entry>
+ </row>
+ <row>
+ <entry><literal>ps</literal></entry>
+ <entry>string</entry>
+ <entry>Current ps display</entry>
+ </row>
+ <row>
+ <entry><literal>session_start</literal></entry>
+ <entry>string</entry>
+ <entry>Session start time</entry>
+ </row>
+ <row>
+ <entry><literal>vxid</literal></entry>
+ <entry>string</entry>
+ <entry>Virtual transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>txid</literal></entry>
+ <entry>string</entry>
+ <entry>Regular transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>error_severity</literal></entry>
+ <entry>string</entry>
+ <entry>Error severity</entry>
+ </row>
+ <row>
+ <entry><literal>state_code</literal></entry>
+ <entry>string</entry>
+ <entry>SQLSTATE code</entry>
+ </row>
+ <row>
+ <entry><literal>message</literal></entry>
+ <entry>string</entry>
+ <entry>Error message</entry>
+ </row>
+ <row>
+ <entry><literal>detail</literal></entry>
+ <entry>string</entry>
+ <entry>Error message detail</entry>
+ </row>
+ <row>
+ <entry><literal>hint</literal></entry>
+ <entry>string</entry>
+ <entry>Error message hint</entry>
+ </row>
+ <row>
+ <entry><literal>internal_query</literal></entry>
+ <entry>string</entry>
+ <entry>Internal query that led to the error</entry>
+ </row>
+ <row>
+ <entry><literal>internal_position</literal></entry>
+ <entry>number</entry>
+ <entry>Cursor index into internal query</entry>
+ </row>
+ <row>
+ <entry><literal>context</literal></entry>
+ <entry>string</entry>
+ <entry>Error context</entry>
+ </row>
+ <row>
+ <entry><literal>statement</literal></entry>
+ <entry>string</entry>
+ <entry>Client-supplied query string</entry>
+ </row>
+ <row>
+ <entry><literal>cursor_position</literal></entry>
+ <entry>string</entry>
+ <entry>Cursor index into query string</entry>
+ </row>
+ <row>
+ <entry><literal>func_name</literal></entry>
+ <entry>string</entry>
+ <entry>Error location function name</entry>
+ </row>
+ <row>
+ <entry><literal>file_name</literal></entry>
+ <entry>string</entry>
+ <entry>File name of error location</entry>
+ </row>
+ <row>
+ <entry><literal>file_line_num</literal></entry>
+ <entry>number</entry>
+ <entry>File line number of the error location</entry>
+ </row>
+ <row>
+ <entry><literal>application_name</literal></entry>
+ <entry>string</entry>
+ <entry>Client application name</entry>
+ </row>
+ <row>
+ <entry><literal>backend_type</literal></entry>
+ <entry>string</entry>
+ <entry>Type of backend</entry>
+ </row>
+ <row>
+ <entry><literal>leader_pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID of leader for active parallel workers</entry>
+ </row>
+ <row>
+ <entry><literal>query_id</literal></entry>
+ <entry>number</entry>
+ <entry>Query ID</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.33.1
Hi,
On 2021-11-10 22:44:49 +0900, Michael Paquier wrote:
Patch 0003 has been heavily reworked, and it would be good to have an
extra pair of eyes on it. So I have switched the CF entry as "Needs
Review" and added my name to the list of authors (originally this
stuff took code portions of own module, as well).
The tests don't seem to pass on windows:
https://cirrus-ci.com/task/5412456754315264?logs=test_bin#L47
https://api.cirrus-ci.com/v1/artifact/task/5412456754315264/tap/src/bin/pg_ctl/tmp_check/log/regress_log_004_logrotate
psql:<stdin>:1: ERROR: division by zero
could not open "c:/cirrus/src/bin/pg_ctl/tmp_check/t_004_logrotate_primary_data/pgdata/current_logfiles": The system cannot find the file specified at t/004_logrotate.pl line 87.
Greetings,
Andres Freund
On Sun, Jan 02, 2022 at 01:34:45PM -0800, Andres Freund wrote:
The tests don't seem to pass on windows:
https://cirrus-ci.com/task/5412456754315264?logs=test_bin#L47
https://api.cirrus-ci.com/v1/artifact/task/5412456754315264/tap/src/bin/pg_ctl/tmp_check/log/regress_log_004_logrotatepsql:<stdin>:1: ERROR: division by zero
could not open "c:/cirrus/src/bin/pg_ctl/tmp_check/t_004_logrotate_primary_data/pgdata/current_logfiles": The system cannot find the file specified at t/004_logrotate.pl line 87.
This seems to point out that the syslogger is too slow to capture the
logrotate signal, and the patch set is introducing nothing new in
terms of infrastructure, just an extra value for log_destination.
This stuff passes here, and I am not spotting something amiss after an
extra close read.
Attached is an updated patch set that increases the test timeout (5min
-> 10min). That should help, I assume.
--
Michael
Attachments:
v8-0003-JSON-logging.patchtext/x-diff; charset=us-asciiDownload
From 7417ee1ac6e6ee4d95f9b2e5f0b411c94515a957 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 11 Nov 2021 09:02:33 +0900
Subject: [PATCH v8 3/3] JSON logging
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog.h | 2 +
src/backend/postmaster/syslogger.c | 107 ++++++-
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 18 ++
src/backend/utils/error/jsonlog.c | 294 ++++++++++++++++++
src/backend/utils/misc/guc.c | 4 +-
src/backend/utils/misc/postgresql.conf.sample | 13 +-
src/bin/pg_ctl/t/004_logrotate.pl | 16 +-
doc/src/sgml/config.sgml | 203 +++++++++++-
11 files changed, 631 insertions(+), 35 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index c79dfbeba2..18448b76e5 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 731f3e3cd8..a32c790421 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
@@ -453,6 +454,7 @@ extern void write_pipe_chunks(char *data, int len, int dest);
/* Destination-specific functions */
extern void write_csvlog(ErrorData *edata);
+extern void write_jsonlog(ErrorData *edata);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index d1f56b95a4..0cf03bcdef 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -281,6 +283,8 @@ SysLoggerMain(int argc, char *argv[])
last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -367,6 +371,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -417,6 +429,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -426,7 +444,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -632,6 +650,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -729,6 +761,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -805,6 +842,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -817,6 +855,9 @@ syslogger_forkexec(void)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ syslogger_fdget(jsonlogFile));
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -834,8 +875,8 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
- argv += 3;
+ Assert(argc == 6);
+ argv += 4;
/*
* Re-open the error output files that were opened by SysLogger_Start().
@@ -848,6 +889,8 @@ syslogger_parseArgs(int argc, char *argv[])
syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
csvlogFile = syslogger_fdopen(fd);
+ fd = atoi(*argv++);
+ jsonlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
@@ -896,7 +939,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -918,6 +963,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1097,19 +1144,24 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
+ * If we're told to write to a structured log file, but it's not open,
+ * dump the data to syslogFile (which is always open) instead. This can
+ * happen if structured output is enabled after postmaster start and
+ * we've been unable to open logFile. There are also race conditions
+ * during a parameter change whereby backends might send us structured
+ * output before we open the logFile or after we close it. Writing
+ * formatted output to the regular log file isn't great, but it beats
+ * dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1180,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1292,6 +1345,8 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
logFileExt = NULL;
else if (target_dest == LOG_DESTINATION_CSVLOG)
logFileExt = ".csv";
+ else if (target_dest == LOG_DESTINATION_JSONLOG)
+ logFileExt = ".json";
else
{
/* cannot happen */
@@ -1379,6 +1434,12 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
&csvlogFile))
return;
+ /* file rotation for jsonlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_JSONLOG, &last_json_file_name,
+ &jsonlogFile))
+ return;
+
update_metainfo_datafile();
set_next_rotation_time();
@@ -1465,7 +1526,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1523,6 +1585,19 @@ update_metainfo_datafile(void)
return;
}
}
+
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 88faf4dfd7..4931859627 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 4406d8deff..39467be917 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2984,6 +2984,22 @@ send_message_to_server_log(ErrorData *edata)
fallback_to_stderr = true;
}
+ /* Write to JSON log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_JSONLOG) != 0)
+ {
+ /*
+ * Send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written
+ * to stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_jsonlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
/*
* Write to stderr, if enabled or if required because of a previous
* limitation.
@@ -3059,6 +3075,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..f2101b59ff
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,294 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+static void appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key,
+ const char *fmt,...) pg_attribute_printf(4, 5);
+
+/*
+ * appendJSONKeyValue
+ * Append to a StringInfo a comma followed by a JSON key and value.
+ * The key is always escaped. The value can be escaped optionally.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value,
+ bool escape_value)
+{
+ Assert(key != NULL);
+
+ if (value == NULL)
+ return;
+
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+
+ if (escape_value)
+ escape_json(buf, value);
+ else
+ appendStringInfoString(buf, value);
+}
+
+
+/*
+ * appendJSONKeyValueFmt
+ *
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /* Allocate result buffer */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+
+ appendJSONKeyValue(buf, key, value, escape_key);
+
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+
+ /*
+ * First property does not use appendJSONKeyValue as it does not have
+ * comma prefix.
+ */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name, true);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name, true);
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueFmt(&buf, "pid", false, "%d", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host, true);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port, false);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", true, "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueFmt(&buf, "line_num", false, "%ld", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data, true);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", start_time, true);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", true, "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", false, "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity",
+ (char *) error_severity(edata->elevel), true);
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code",
+ unpack_sql_state(edata->sqlerrcode), true);
+
+ /* errmessage */
+ appendJSONKeyValue(&buf, "message", edata->message, true);
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log, true);
+ else
+ appendJSONKeyValue(&buf, "detail", edata->detail, true);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint, true);
+
+ /* internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery, true);
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueFmt(&buf, "internal_position", false, "%u",
+ edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context, true);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (check_log_of_query(edata))
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string, true);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueFmt(&buf, "cursor_position", false, "%d",
+ edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname, true);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename, true);
+ appendJSONKeyValueFmt(&buf, "file_line_num", false, "%d",
+ edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name, true);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log(), true);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueFmt(&buf, "leader_pid", false, "%d", leader->pid);
+ }
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", false, "%lld",
+ (long long) pgstat_get_my_query_id());
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index f9504d3aec..2c70804fae 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -4264,7 +4264,7 @@ static struct config_string ConfigureNamesString[] =
{"log_destination", PGC_SIGHUP, LOGGING_WHERE,
gettext_noop("Sets the destination for server log output."),
gettext_noop("Valid values are combinations of \"stderr\", "
- "\"syslog\", \"csvlog\", and \"eventlog\", "
+ "\"syslog\", \"csvlog\", \"jsonlog\" and \"eventlog\", "
"depending on the platform."),
GUC_LIST_INPUT
},
@@ -11740,6 +11740,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a1acd46b61..817d5f5324 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -432,14 +432,15 @@
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
- # stderr, csvlog, syslog, and eventlog,
- # depending on platform. csvlog
- # requires logging_collector to be on.
+ # stderr, csvlog, jsonlog, syslog, and
+ # eventlog, depending on platform.
+ # csvlog and jsonlog require
+ # logging_collector to be on.
# This is used when logging to stderr:
-#logging_collector = off # Enable capturing of stderr and csvlog
- # into log files. Required to be on for
- # csvlogs.
+#logging_collector = off # Enable capturing of stderr, jsonlog
+ # and csvlog into log files. Required
+ # to be on for csvlogs and jsonlogs.
# (change requires restart)
# These are only used if logging_collector is on:
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index 3813a3b1fd..4b6ccd8e9b 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
-use Test::More tests => 10;
+use Test::More tests => 14;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -39,7 +39,7 @@ sub check_log_pattern
my $node = shift;
my $lfname = fetch_file_name($logfiles, $format);
- my $max_attempts = 180 * 10;
+ my $max_attempts = 360 * 10;
my $logcontents;
for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
@@ -65,7 +65,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -78,7 +78,7 @@ $node->start();
$node->psql('postgres', 'SELECT 1/0');
# might need to retry if logging collector process is slow...
-my $max_attempts = 180 * 10;
+my $max_attempts = 360 * 10;
my $current_logfiles;
for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
@@ -96,11 +96,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -122,7 +124,8 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
@@ -130,5 +133,6 @@ $node->psql('postgres', 'fee fi fo fum');
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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index afbb6c35e3..c29ced427c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5931,7 +5931,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5949,6 +5950,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ JSON-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5960,13 +5969,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6106,6 +6116,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7467,6 +7484,186 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ </para>
+
+ <para>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ <para>
+ Each log line is serialized as a JSON object as of the following
+ set of keys with their values.
+ </para>
+
+ <table>
+ <title>Keys and values of JSON log entries</title>
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Key name</entry>
+ <entry>Type</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>timestamp</literal></entry>
+ <entry>string</entry>
+ <entry>Time stamp with milliseconds</entry>
+ </row>
+ <row>
+ <entry><literal>user</literal></entry>
+ <entry>string</entry>
+ <entry>User name</entry>
+ </row>
+ <row>
+ <entry><literal>dbname</literal></entry>
+ <entry>string</entry>
+ <entry>Database name</entry>
+ </row>
+ <row>
+ <entry><literal>pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID</entry>
+ </row>
+ <row>
+ <entry><literal>remote_host</literal></entry>
+ <entry>string</entry>
+ <entry>Client host</entry>
+ </row>
+ <row>
+ <entry><literal>remote_port</literal></entry>
+ <entry>number</entry>
+ <entry>Client port</entry>
+ </row>
+ <row>
+ <entry><literal>session_id</literal></entry>
+ <entry>string</entry>
+ <entry>Session ID</entry>
+ </row>
+ <row>
+ <entry><literal>line_num</literal></entry>
+ <entry>number</entry>
+ <entry>Per-session line number</entry>
+ </row>
+ <row>
+ <entry><literal>ps</literal></entry>
+ <entry>string</entry>
+ <entry>Current ps display</entry>
+ </row>
+ <row>
+ <entry><literal>session_start</literal></entry>
+ <entry>string</entry>
+ <entry>Session start time</entry>
+ </row>
+ <row>
+ <entry><literal>vxid</literal></entry>
+ <entry>string</entry>
+ <entry>Virtual transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>txid</literal></entry>
+ <entry>string</entry>
+ <entry>Regular transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>error_severity</literal></entry>
+ <entry>string</entry>
+ <entry>Error severity</entry>
+ </row>
+ <row>
+ <entry><literal>state_code</literal></entry>
+ <entry>string</entry>
+ <entry>SQLSTATE code</entry>
+ </row>
+ <row>
+ <entry><literal>message</literal></entry>
+ <entry>string</entry>
+ <entry>Error message</entry>
+ </row>
+ <row>
+ <entry><literal>detail</literal></entry>
+ <entry>string</entry>
+ <entry>Error message detail</entry>
+ </row>
+ <row>
+ <entry><literal>hint</literal></entry>
+ <entry>string</entry>
+ <entry>Error message hint</entry>
+ </row>
+ <row>
+ <entry><literal>internal_query</literal></entry>
+ <entry>string</entry>
+ <entry>Internal query that led to the error</entry>
+ </row>
+ <row>
+ <entry><literal>internal_position</literal></entry>
+ <entry>number</entry>
+ <entry>Cursor index into internal query</entry>
+ </row>
+ <row>
+ <entry><literal>context</literal></entry>
+ <entry>string</entry>
+ <entry>Error context</entry>
+ </row>
+ <row>
+ <entry><literal>statement</literal></entry>
+ <entry>string</entry>
+ <entry>Client-supplied query string</entry>
+ </row>
+ <row>
+ <entry><literal>cursor_position</literal></entry>
+ <entry>string</entry>
+ <entry>Cursor index into query string</entry>
+ </row>
+ <row>
+ <entry><literal>func_name</literal></entry>
+ <entry>string</entry>
+ <entry>Error location function name</entry>
+ </row>
+ <row>
+ <entry><literal>file_name</literal></entry>
+ <entry>string</entry>
+ <entry>File name of error location</entry>
+ </row>
+ <row>
+ <entry><literal>file_line_num</literal></entry>
+ <entry>number</entry>
+ <entry>File line number of the error location</entry>
+ </row>
+ <row>
+ <entry><literal>application_name</literal></entry>
+ <entry>string</entry>
+ <entry>Client application name</entry>
+ </row>
+ <row>
+ <entry><literal>backend_type</literal></entry>
+ <entry>string</entry>
+ <entry>Type of backend</entry>
+ </row>
+ <row>
+ <entry><literal>leader_pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID of leader for active parallel workers</entry>
+ </row>
+ <row>
+ <entry><literal>query_id</literal></entry>
+ <entry>number</entry>
+ <entry>Query ID</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.34.1
v8-0001-Some-refactoring-of-elog-specific-routines.patchtext/x-diff; charset=us-asciiDownload
From 33fbcbe4c8d4ac43a10d6f9362a961352a8df8ca Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 19 Oct 2021 16:25:45 +0900
Subject: [PATCH v8 1/3] Some refactoring of elog-specific routines
This refactors out the following things in elog.c, for ease of use
across multiple log destinations:
- start_timestamp (including reset)
- log_timestamp
- decide if query can be logged
- backend type
- write using the elog piped protocol
- Error severity to string.
These will be reused by csvlog and jsonlog.
---
src/include/utils/elog.h | 12 +++
src/backend/utils/error/elog.c | 159 +++++++++++++++++++++------------
2 files changed, 114 insertions(+), 57 deletions(-)
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f53607e12e..731f3e3cd8 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -442,6 +442,18 @@ extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+/* Common functions shared across destinations */
+extern void reset_formatted_start_time(void);
+extern char *get_formatted_start_time(void);
+extern char *get_formatted_log_time(void);
+extern const char *get_backend_type_for_log(void);
+extern bool check_log_of_query(ErrorData *edata);
+extern const char *error_severity(int elevel);
+extern void write_pipe_chunks(char *data, int len, int dest);
+
+/* Destination-specific functions */
+extern void write_csvlog(ErrorData *edata);
+
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
#endif
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index f33729513a..a162258bab 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -175,15 +175,10 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
-static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
@@ -2289,14 +2284,23 @@ write_console(const char *line, int len)
}
/*
- * setup formatted_log_time, for consistent times between CSV and regular logs
+ * get_formatted_log_time -- compute and get the log timestamp.
+ *
+ * The timestamp is computed if not set yet, so as it is kept consistent
+ * among all the log destinations that require it to be consistent. Note
+ * that the computed timestamp is returned in a static buffer, not
+ * palloc()'d.
*/
-static void
-setup_formatted_log_time(void)
+char *
+get_formatted_log_time(void)
{
pg_time_t stamp_time;
char msbuf[13];
+ /* leave if already computed */
+ if (formatted_log_time[0] != '\0')
+ return formatted_log_time;
+
if (!saved_timeval_set)
{
gettimeofday(&saved_timeval, NULL);
@@ -2318,16 +2322,34 @@ setup_formatted_log_time(void)
/* 'paste' milliseconds into place... */
sprintf(msbuf, ".%03d", (int) (saved_timeval.tv_usec / 1000));
memcpy(formatted_log_time + 19, msbuf, 4);
+
+ return formatted_log_time;
}
/*
- * setup formatted_start_time
+ * reset_formatted_start_time -- reset the start timestamp
*/
-static void
-setup_formatted_start_time(void)
+void
+reset_formatted_start_time(void)
+{
+ formatted_start_time[0] = '\0';
+}
+
+/*
+ * get_formatted_start_time -- compute and get the start timestamp.
+ *
+ * The timestamp is computed if not set yet. Note that the computed
+ * timestamp is returned in a static buffer, not palloc()'d.
+ */
+char *
+get_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
+ /* leave if already computed */
+ if (formatted_start_time[0] != '\0')
+ return formatted_start_time;
+
/*
* Note: we expect that guc.c will ensure that log_timezone is set up (at
* least with a minimal GMT value) before Log_line_prefix can become
@@ -2336,6 +2358,49 @@ setup_formatted_start_time(void)
pg_strftime(formatted_start_time, FORMATTED_TS_LEN,
"%Y-%m-%d %H:%M:%S %Z",
pg_localtime(&stamp_time, log_timezone));
+
+ return formatted_start_time;
+}
+
+/*
+ * check_log_of_query -- check if a query can be logged
+ */
+bool
+check_log_of_query(ErrorData *edata)
+{
+ /* log required? */
+ if (!is_log_level_output(edata->elevel, log_min_error_statement))
+ return false;
+
+ /* query log wanted? */
+ if (edata->hide_stmt)
+ return false;
+
+ /* query string available? */
+ if (debug_query_string == NULL)
+ return false;
+
+ return true;
+}
+
+/*
+ * get_backend_type_for_log -- backend type for log entries
+ *
+ * Returns a pointer to a static buffer, not palloc()'d.
+ */
+const char *
+get_backend_type_for_log(void)
+{
+ const char *backend_type_str;
+
+ if (MyProcPid == PostmasterPid)
+ backend_type_str = "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ backend_type_str = MyBgworkerEntry->bgw_type;
+ else
+ backend_type_str = GetBackendTypeDesc(MyBackendType);
+
+ return backend_type_str;
}
/*
@@ -2466,14 +2531,7 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
break;
case 'b':
{
- const char *backend_type_str;
-
- if (MyProcPid == PostmasterPid)
- backend_type_str = "postmaster";
- else if (MyBackendType == B_BG_WORKER)
- backend_type_str = MyBgworkerEntry->bgw_type;
- else
- backend_type_str = GetBackendTypeDesc(MyBackendType);
+ const char *backend_type_str = get_backend_type_for_log();
if (padding != 0)
appendStringInfo(buf, "%*s", padding, backend_type_str);
@@ -2561,7 +2619,10 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
appendStringInfo(buf, "%ld", log_line_number);
break;
case 'm':
- setup_formatted_log_time();
+ /* force a log timestamp reset */
+ formatted_log_time[0] = '\0';
+ (void) get_formatted_log_time();
+
if (padding != 0)
appendStringInfo(buf, "%*s", padding, formatted_log_time);
else
@@ -2602,12 +2663,14 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
break;
case 's':
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- if (padding != 0)
- appendStringInfo(buf, "%*s", padding, formatted_start_time);
- else
- appendStringInfoString(buf, formatted_start_time);
+ {
+ char *start_time = get_formatted_start_time();
+
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, start_time);
+ else
+ appendStringInfoString(buf, start_time);
+ }
break;
case 'i':
if (MyProcPort)
@@ -2758,11 +2821,13 @@ appendCSVLiteral(StringInfo buf, const char *data)
* Constructs the error message, depending on the Errordata it gets, in a CSV
* format which is described in doc/src/sgml/config.sgml.
*/
-static void
+void
write_csvlog(ErrorData *edata)
{
StringInfoData buf;
bool print_stmt = false;
+ char *start_time;
+ char *log_time;
/* static counter for line numbers */
static long log_line_number = 0;
@@ -2785,17 +2850,9 @@ write_csvlog(ErrorData *edata)
initStringInfo(&buf);
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
appendStringInfoChar(&buf, ',');
/* username */
@@ -2853,9 +2910,8 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
appendStringInfoChar(&buf, ',');
/* Virtual transaction id */
@@ -2906,10 +2962,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
+ print_stmt = check_log_of_query(edata);
if (print_stmt)
appendCSVLiteral(&buf, debug_query_string);
appendStringInfoChar(&buf, ',');
@@ -2943,13 +2996,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
+ appendCSVLiteral(&buf, get_backend_type_for_log());
appendStringInfoChar(&buf, ',');
/* leader PID */
@@ -3101,9 +3148,7 @@ send_message_to_server_log(ErrorData *edata)
/*
* If the user wants the query that generated this error logged, do it.
*/
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
+ if (check_log_of_query(edata))
{
log_line_prefix(&buf, edata);
appendStringInfoString(&buf, _("STATEMENT: "));
@@ -3233,7 +3278,7 @@ send_message_to_server_log(ErrorData *edata)
* warning from ignoring write()'s result, so do a little dance with casting
* rc to void to shut up the compiler.
*/
-static void
+void
write_pipe_chunks(char *data, int len, int dest)
{
PipeProtoChunk p;
@@ -3469,7 +3514,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
--
2.34.1
v8-0002-Refactor-CSV-specific-code-into-its-own-file.patchtext/x-diff; charset=us-asciiDownload
From 85d9fbee22f56e342ffcf8082a320764b0ad1237 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:22:24 +0900
Subject: [PATCH v8 2/3] Refactor CSV-specific code into its own file
---
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 268 +++++++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 235 ---------------------------
3 files changed, 269 insertions(+), 235 deletions(-)
create mode 100644 src/backend/utils/error/csvlog.c
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..443a91e168
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,268 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of Californi
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * write_csvlog -- Generate and write CSV log entry
+ *
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ print_stmt = check_log_of_query(edata);
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a162258bab..4406d8deff 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2792,241 +2792,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
- char *start_time;
- char *log_time;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /* timestamp with milliseconds */
- log_time = get_formatted_log_time();
- appendStringInfoString(&buf, log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- start_time = get_formatted_start_time();
- appendStringInfoString(&buf, start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- print_stmt = check_log_of_query(edata);
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- appendCSVLiteral(&buf, get_backend_type_for_log());
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
--
2.34.1
On 1/5/22 02:32, Michael Paquier wrote:
On Sun, Jan 02, 2022 at 01:34:45PM -0800, Andres Freund wrote:
The tests don't seem to pass on windows:
https://cirrus-ci.com/task/5412456754315264?logs=test_bin#L47
https://api.cirrus-ci.com/v1/artifact/task/5412456754315264/tap/src/bin/pg_ctl/tmp_check/log/regress_log_004_logrotatepsql:<stdin>:1: ERROR: division by zero
could not open "c:/cirrus/src/bin/pg_ctl/tmp_check/t_004_logrotate_primary_data/pgdata/current_logfiles": The system cannot find the file specified at t/004_logrotate.pl line 87.This seems to point out that the syslogger is too slow to capture the
logrotate signal, and the patch set is introducing nothing new in
terms of infrastructure, just an extra value for log_destination.
This stuff passes here, and I am not spotting something amiss after an
extra close read.Attached is an updated patch set that increases the test timeout (5min
-> 10min). That should help, I assume.
ITYM 3 min -> 6 min. But in any case, is that really going to solve
this? The file should exist, even if its contents are not up to date, AIUI.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On 1/6/22 13:06, Andrew Dunstan wrote:
On 1/5/22 02:32, Michael Paquier wrote:
On Sun, Jan 02, 2022 at 01:34:45PM -0800, Andres Freund wrote:
The tests don't seem to pass on windows:
https://cirrus-ci.com/task/5412456754315264?logs=test_bin#L47
https://api.cirrus-ci.com/v1/artifact/task/5412456754315264/tap/src/bin/pg_ctl/tmp_check/log/regress_log_004_logrotatepsql:<stdin>:1: ERROR: division by zero
could not open "c:/cirrus/src/bin/pg_ctl/tmp_check/t_004_logrotate_primary_data/pgdata/current_logfiles": The system cannot find the file specified at t/004_logrotate.pl line 87.This seems to point out that the syslogger is too slow to capture the
logrotate signal, and the patch set is introducing nothing new in
terms of infrastructure, just an extra value for log_destination.
This stuff passes here, and I am not spotting something amiss after an
extra close read.Attached is an updated patch set that increases the test timeout (5min
-> 10min). That should help, I assume.ITYM 3 min -> 6 min. But in any case, is that really going to solve
this? The file should exist, even if its contents are not up to date, AIUI.
I have tested on an msys2 setup with your v8 patches and I am getting this:
# Failed test 'current_logfiles is sane'
# at t/004_logrotate.pl line 96.
# 'stderr log/postgresql-2022-01-06_222419.log
# csvlog log/postgresql-2022-01-06_222419.csv
# '
# doesn't match '(?^:^stderr log/postgresql-.*log
# csvlog log/postgresql-.*csv
# jsonlog log/postgresql-.*json$)'
# Failed test 'found expected log file content for stderr'
# at t/004_logrotate.pl line 103.
# ''
# doesn't match '(?^:division by zero)'
# Failed test 'found expected log file content for jsonlog'
# at t/004_logrotate.pl line 105.
# undef
# doesn't match '(?^:division by zero)'
# Failed test 'pg_current_logfile() gives correct answer with jsonlog'
# at t/004_logrotate.pl line 105.
# got: ''
# expected: undef
# Looks like you failed 4 tests of 14.
[22:37:31] t/004_logrotate.pl ...
Dubious, test returned 4 (wstat 1024, 0x400)
Failed 4/14 subtests
[22:37:31]
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Thu, Jan 06, 2022 at 06:28:26PM -0500, Andrew Dunstan wrote:
I have tested on an msys2 setup with your v8 patches and I am getting this:
# Failed test 'current_logfiles is sane'
# at t/004_logrotate.pl line 96.
# 'stderr log/postgresql-2022-01-06_222419.log
# csvlog log/postgresql-2022-01-06_222419.csv
Yes, I was waiting for the latest results, but that did not help at
all. Something is wrong with the patch, I am not sure what yet, but
that seems like a mistake in the backend part of it rather than the
tests. I have switched the CF entry as waiting on author until this
is addressed.
--
Michael
On Fri, Jan 07, 2022 at 03:49:47PM +0900, Michael Paquier wrote:
Yes, I was waiting for the latest results, but that did not help at
all. Something is wrong with the patch, I am not sure what yet, but
that seems like a mistake in the backend part of it rather than the
tests. I have switched the CF entry as waiting on author until this
is addressed.
The issue comes from an incorrect change in syslogger_parseArgs()
where I missed that the incrementation of argv by 3 has no need to be
changed. A build with -DEXEC_BACKEND is enough to show the failure,
which caused a crash when starting up the syslogger because of a NULL
pointer dereference. The attached v9 should be enough to switch the
CF bot to green.
--
Michael
Attachments:
v9-0001-Some-refactoring-of-elog-specific-routines.patchtext/x-diff; charset=us-asciiDownload
From 6ab52532b232bc3314cef5c9c4ac38110957ab34 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 19 Oct 2021 16:25:45 +0900
Subject: [PATCH v9 1/3] Some refactoring of elog-specific routines
This refactors out the following things in elog.c, for ease of use
across multiple log destinations:
- start_timestamp (including reset)
- log_timestamp
- decide if query can be logged
- backend type
- write using the elog piped protocol
- Error severity to string.
These will be reused by csvlog and jsonlog.
---
src/include/utils/elog.h | 12 +++
src/backend/utils/error/elog.c | 159 +++++++++++++++++++++------------
2 files changed, 114 insertions(+), 57 deletions(-)
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 8df2799dc2..f37956f100 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -442,6 +442,18 @@ extern void DebugFileOpen(void);
extern char *unpack_sql_state(int sql_state);
extern bool in_error_recursion_trouble(void);
+/* Common functions shared across destinations */
+extern void reset_formatted_start_time(void);
+extern char *get_formatted_start_time(void);
+extern char *get_formatted_log_time(void);
+extern const char *get_backend_type_for_log(void);
+extern bool check_log_of_query(ErrorData *edata);
+extern const char *error_severity(int elevel);
+extern void write_pipe_chunks(char *data, int len, int dest);
+
+/* Destination-specific functions */
+extern void write_csvlog(ErrorData *edata);
+
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
#endif
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 2f2c3ba41b..4ccad9bac1 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -175,15 +175,10 @@ static const char *err_gettext(const char *str) pg_attribute_format_arg(1);
static pg_noinline void set_backtrace(ErrorData *edata, int num_skip);
static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str);
static void write_console(const char *line, int len);
-static void setup_formatted_log_time(void);
-static void setup_formatted_start_time(void);
static const char *process_log_prefix_padding(const char *p, int *padding);
static void log_line_prefix(StringInfo buf, ErrorData *edata);
-static void write_csvlog(ErrorData *edata);
static void send_message_to_server_log(ErrorData *edata);
-static void write_pipe_chunks(char *data, int len, int dest);
static void send_message_to_frontend(ErrorData *edata);
-static const char *error_severity(int elevel);
static void append_with_tabs(StringInfo buf, const char *str);
@@ -2289,14 +2284,23 @@ write_console(const char *line, int len)
}
/*
- * setup formatted_log_time, for consistent times between CSV and regular logs
+ * get_formatted_log_time -- compute and get the log timestamp.
+ *
+ * The timestamp is computed if not set yet, so as it is kept consistent
+ * among all the log destinations that require it to be consistent. Note
+ * that the computed timestamp is returned in a static buffer, not
+ * palloc()'d.
*/
-static void
-setup_formatted_log_time(void)
+char *
+get_formatted_log_time(void)
{
pg_time_t stamp_time;
char msbuf[13];
+ /* leave if already computed */
+ if (formatted_log_time[0] != '\0')
+ return formatted_log_time;
+
if (!saved_timeval_set)
{
gettimeofday(&saved_timeval, NULL);
@@ -2318,16 +2322,34 @@ setup_formatted_log_time(void)
/* 'paste' milliseconds into place... */
sprintf(msbuf, ".%03d", (int) (saved_timeval.tv_usec / 1000));
memcpy(formatted_log_time + 19, msbuf, 4);
+
+ return formatted_log_time;
}
/*
- * setup formatted_start_time
+ * reset_formatted_start_time -- reset the start timestamp
*/
-static void
-setup_formatted_start_time(void)
+void
+reset_formatted_start_time(void)
+{
+ formatted_start_time[0] = '\0';
+}
+
+/*
+ * get_formatted_start_time -- compute and get the start timestamp.
+ *
+ * The timestamp is computed if not set yet. Note that the computed
+ * timestamp is returned in a static buffer, not palloc()'d.
+ */
+char *
+get_formatted_start_time(void)
{
pg_time_t stamp_time = (pg_time_t) MyStartTime;
+ /* leave if already computed */
+ if (formatted_start_time[0] != '\0')
+ return formatted_start_time;
+
/*
* Note: we expect that guc.c will ensure that log_timezone is set up (at
* least with a minimal GMT value) before Log_line_prefix can become
@@ -2336,6 +2358,49 @@ setup_formatted_start_time(void)
pg_strftime(formatted_start_time, FORMATTED_TS_LEN,
"%Y-%m-%d %H:%M:%S %Z",
pg_localtime(&stamp_time, log_timezone));
+
+ return formatted_start_time;
+}
+
+/*
+ * check_log_of_query -- check if a query can be logged
+ */
+bool
+check_log_of_query(ErrorData *edata)
+{
+ /* log required? */
+ if (!is_log_level_output(edata->elevel, log_min_error_statement))
+ return false;
+
+ /* query log wanted? */
+ if (edata->hide_stmt)
+ return false;
+
+ /* query string available? */
+ if (debug_query_string == NULL)
+ return false;
+
+ return true;
+}
+
+/*
+ * get_backend_type_for_log -- backend type for log entries
+ *
+ * Returns a pointer to a static buffer, not palloc()'d.
+ */
+const char *
+get_backend_type_for_log(void)
+{
+ const char *backend_type_str;
+
+ if (MyProcPid == PostmasterPid)
+ backend_type_str = "postmaster";
+ else if (MyBackendType == B_BG_WORKER)
+ backend_type_str = MyBgworkerEntry->bgw_type;
+ else
+ backend_type_str = GetBackendTypeDesc(MyBackendType);
+
+ return backend_type_str;
}
/*
@@ -2466,14 +2531,7 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
break;
case 'b':
{
- const char *backend_type_str;
-
- if (MyProcPid == PostmasterPid)
- backend_type_str = "postmaster";
- else if (MyBackendType == B_BG_WORKER)
- backend_type_str = MyBgworkerEntry->bgw_type;
- else
- backend_type_str = GetBackendTypeDesc(MyBackendType);
+ const char *backend_type_str = get_backend_type_for_log();
if (padding != 0)
appendStringInfo(buf, "%*s", padding, backend_type_str);
@@ -2561,7 +2619,10 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
appendStringInfo(buf, "%ld", log_line_number);
break;
case 'm':
- setup_formatted_log_time();
+ /* force a log timestamp reset */
+ formatted_log_time[0] = '\0';
+ (void) get_formatted_log_time();
+
if (padding != 0)
appendStringInfo(buf, "%*s", padding, formatted_log_time);
else
@@ -2602,12 +2663,14 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
break;
case 's':
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- if (padding != 0)
- appendStringInfo(buf, "%*s", padding, formatted_start_time);
- else
- appendStringInfoString(buf, formatted_start_time);
+ {
+ char *start_time = get_formatted_start_time();
+
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, start_time);
+ else
+ appendStringInfoString(buf, start_time);
+ }
break;
case 'i':
if (MyProcPort)
@@ -2758,11 +2821,13 @@ appendCSVLiteral(StringInfo buf, const char *data)
* Constructs the error message, depending on the Errordata it gets, in a CSV
* format which is described in doc/src/sgml/config.sgml.
*/
-static void
+void
write_csvlog(ErrorData *edata)
{
StringInfoData buf;
bool print_stmt = false;
+ char *start_time;
+ char *log_time;
/* static counter for line numbers */
static long log_line_number = 0;
@@ -2785,17 +2850,9 @@ write_csvlog(ErrorData *edata)
initStringInfo(&buf);
- /*
- * timestamp with milliseconds
- *
- * Check if the timestamp is already calculated for the syslog message,
- * and use it if so. Otherwise, get the current timestamp. This is done
- * to put same timestamp in both syslog and csvlog messages.
- */
- if (formatted_log_time[0] == '\0')
- setup_formatted_log_time();
-
- appendStringInfoString(&buf, formatted_log_time);
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
appendStringInfoChar(&buf, ',');
/* username */
@@ -2853,9 +2910,8 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* session start timestamp */
- if (formatted_start_time[0] == '\0')
- setup_formatted_start_time();
- appendStringInfoString(&buf, formatted_start_time);
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
appendStringInfoChar(&buf, ',');
/* Virtual transaction id */
@@ -2906,10 +2962,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* user query --- only reported if not disabled by the caller */
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
- print_stmt = true;
+ print_stmt = check_log_of_query(edata);
if (print_stmt)
appendCSVLiteral(&buf, debug_query_string);
appendStringInfoChar(&buf, ',');
@@ -2943,13 +2996,7 @@ write_csvlog(ErrorData *edata)
appendStringInfoChar(&buf, ',');
/* backend type */
- if (MyProcPid == PostmasterPid)
- appendCSVLiteral(&buf, "postmaster");
- else if (MyBackendType == B_BG_WORKER)
- appendCSVLiteral(&buf, MyBgworkerEntry->bgw_type);
- else
- appendCSVLiteral(&buf, GetBackendTypeDesc(MyBackendType));
-
+ appendCSVLiteral(&buf, get_backend_type_for_log());
appendStringInfoChar(&buf, ',');
/* leader PID */
@@ -3101,9 +3148,7 @@ send_message_to_server_log(ErrorData *edata)
/*
* If the user wants the query that generated this error logged, do it.
*/
- if (is_log_level_output(edata->elevel, log_min_error_statement) &&
- debug_query_string != NULL &&
- !edata->hide_stmt)
+ if (check_log_of_query(edata))
{
log_line_prefix(&buf, edata);
appendStringInfoString(&buf, _("STATEMENT: "));
@@ -3233,7 +3278,7 @@ send_message_to_server_log(ErrorData *edata)
* warning from ignoring write()'s result, so do a little dance with casting
* rc to void to shut up the compiler.
*/
-static void
+void
write_pipe_chunks(char *data, int len, int dest)
{
PipeProtoChunk p;
@@ -3469,7 +3514,7 @@ send_message_to_frontend(ErrorData *edata)
* The string is not localized here, but we mark the strings for translation
* so that callers can invoke _() on the result.
*/
-static const char *
+const char *
error_severity(int elevel)
{
const char *prefix;
--
2.34.1
v9-0002-Refactor-CSV-specific-code-into-its-own-file.patchtext/x-diff; charset=us-asciiDownload
From 6bd32c8a49fd4b6a1b08b39593c1e610c711f761 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 5 Oct 2021 15:22:24 +0900
Subject: [PATCH v9 2/3] Refactor CSV-specific code into its own file
---
src/backend/utils/error/Makefile | 1 +
src/backend/utils/error/csvlog.c | 268 +++++++++++++++++++++++++++++++
src/backend/utils/error/elog.c | 235 ---------------------------
3 files changed, 269 insertions(+), 235 deletions(-)
create mode 100644 src/backend/utils/error/csvlog.c
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index 612da215d0..ef770dd2f2 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
+ csvlog.o \
elog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
new file mode 100644
index 0000000000..443a91e168
--- /dev/null
+++ b/src/backend/utils/error/csvlog.c
@@ -0,0 +1,268 @@
+/*-------------------------------------------------------------------------
+ *
+ * csvlog.c
+ * CSV logging
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of Californi
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/csvlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+
+
+/*
+ * append a CSV'd version of a string to a StringInfo
+ * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
+ * If it's NULL, append nothing.
+ */
+static inline void
+appendCSVLiteral(StringInfo buf, const char *data)
+{
+ const char *p = data;
+ char c;
+
+ /* avoid confusing an empty string with NULL */
+ if (p == NULL)
+ return;
+
+ appendStringInfoCharMacro(buf, '"');
+ while ((c = *p++) != '\0')
+ {
+ if (c == '"')
+ appendStringInfoCharMacro(buf, '"');
+ appendStringInfoCharMacro(buf, c);
+ }
+ appendStringInfoCharMacro(buf, '"');
+}
+
+/*
+ * write_csvlog -- Generate and write CSV log entry
+ *
+ * Constructs the error message, depending on the Errordata it gets, in a CSV
+ * format which is described in doc/src/sgml/config.sgml.
+ */
+void
+write_csvlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ bool print_stmt = false;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* has counter been reset in current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+ appendStringInfoString(&buf, log_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* username */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->user_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* database name */
+ if (MyProcPort)
+ appendCSVLiteral(&buf, MyProcPort->database_name);
+ appendStringInfoChar(&buf, ',');
+
+ /* Process id */
+ if (MyProcPid != 0)
+ appendStringInfo(&buf, "%d", MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->remote_host);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->remote_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session id */
+ appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Line number */
+ appendStringInfo(&buf, "%ld", log_line_number);
+ appendStringInfoChar(&buf, ',');
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendCSVLiteral(&buf, msgbuf.data);
+
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendStringInfoString(&buf, start_time);
+ appendStringInfoChar(&buf, ',');
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
+ appendStringInfoChar(&buf, ',');
+
+ /* Transaction id */
+ appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
+ appendStringInfoChar(&buf, ',');
+
+ /* Error severity */
+ appendStringInfoString(&buf, _(error_severity(edata->elevel)));
+ appendStringInfoChar(&buf, ',');
+
+ /* SQL state code */
+ appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
+ appendStringInfoChar(&buf, ',');
+
+ /* errmessage */
+ appendCSVLiteral(&buf, edata->message);
+ appendStringInfoChar(&buf, ',');
+
+ /* errdetail or errdetail_log */
+ if (edata->detail_log)
+ appendCSVLiteral(&buf, edata->detail_log);
+ else
+ appendCSVLiteral(&buf, edata->detail);
+ appendStringInfoChar(&buf, ',');
+
+ /* errhint */
+ appendCSVLiteral(&buf, edata->hint);
+ appendStringInfoChar(&buf, ',');
+
+ /* internal query */
+ appendCSVLiteral(&buf, edata->internalquery);
+ appendStringInfoChar(&buf, ',');
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendStringInfo(&buf, "%d", edata->internalpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* errcontext */
+ if (!edata->hide_ctx)
+ appendCSVLiteral(&buf, edata->context);
+ appendStringInfoChar(&buf, ',');
+
+ /* user query --- only reported if not disabled by the caller */
+ print_stmt = check_log_of_query(edata);
+ if (print_stmt)
+ appendCSVLiteral(&buf, debug_query_string);
+ appendStringInfoChar(&buf, ',');
+ if (print_stmt && edata->cursorpos > 0)
+ appendStringInfo(&buf, "%d", edata->cursorpos);
+ appendStringInfoChar(&buf, ',');
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ StringInfoData msgbuf;
+
+ initStringInfo(&msgbuf);
+
+ if (edata->funcname && edata->filename)
+ appendStringInfo(&msgbuf, "%s, %s:%d",
+ edata->funcname, edata->filename,
+ edata->lineno);
+ else if (edata->filename)
+ appendStringInfo(&msgbuf, "%s:%d",
+ edata->filename, edata->lineno);
+ appendCSVLiteral(&buf, msgbuf.data);
+ pfree(msgbuf.data);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* application name */
+ if (application_name)
+ appendCSVLiteral(&buf, application_name);
+
+ appendStringInfoChar(&buf, ',');
+
+ /* backend type */
+ appendCSVLiteral(&buf, get_backend_type_for_log());
+ appendStringInfoChar(&buf, ',');
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendStringInfo(&buf, "%d", leader->pid);
+ }
+ appendStringInfoChar(&buf, ',');
+
+ /* query id */
+ appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
+
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 4ccad9bac1..511b43e4ac 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2792,241 +2792,6 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
}
}
-/*
- * append a CSV'd version of a string to a StringInfo
- * We use the PostgreSQL defaults for CSV, i.e. quote = escape = '"'
- * If it's NULL, append nothing.
- */
-static inline void
-appendCSVLiteral(StringInfo buf, const char *data)
-{
- const char *p = data;
- char c;
-
- /* avoid confusing an empty string with NULL */
- if (p == NULL)
- return;
-
- appendStringInfoCharMacro(buf, '"');
- while ((c = *p++) != '\0')
- {
- if (c == '"')
- appendStringInfoCharMacro(buf, '"');
- appendStringInfoCharMacro(buf, c);
- }
- appendStringInfoCharMacro(buf, '"');
-}
-
-/*
- * Constructs the error message, depending on the Errordata it gets, in a CSV
- * format which is described in doc/src/sgml/config.sgml.
- */
-void
-write_csvlog(ErrorData *edata)
-{
- StringInfoData buf;
- bool print_stmt = false;
- char *start_time;
- char *log_time;
-
- /* static counter for line numbers */
- static long log_line_number = 0;
-
- /* has counter been reset in current process? */
- static int log_my_pid = 0;
-
- /*
- * This is one of the few places where we'd rather not inherit a static
- * variable's value from the postmaster. But since we will, reset it when
- * MyProcPid changes.
- */
- if (log_my_pid != MyProcPid)
- {
- log_line_number = 0;
- log_my_pid = MyProcPid;
- formatted_start_time[0] = '\0';
- }
- log_line_number++;
-
- initStringInfo(&buf);
-
- /* timestamp with milliseconds */
- log_time = get_formatted_log_time();
- appendStringInfoString(&buf, log_time);
- appendStringInfoChar(&buf, ',');
-
- /* username */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->user_name);
- appendStringInfoChar(&buf, ',');
-
- /* database name */
- if (MyProcPort)
- appendCSVLiteral(&buf, MyProcPort->database_name);
- appendStringInfoChar(&buf, ',');
-
- /* Process id */
- if (MyProcPid != 0)
- appendStringInfo(&buf, "%d", MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Remote host and port */
- if (MyProcPort && MyProcPort->remote_host)
- {
- appendStringInfoChar(&buf, '"');
- appendStringInfoString(&buf, MyProcPort->remote_host);
- if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
- {
- appendStringInfoChar(&buf, ':');
- appendStringInfoString(&buf, MyProcPort->remote_port);
- }
- appendStringInfoChar(&buf, '"');
- }
- appendStringInfoChar(&buf, ',');
-
- /* session id */
- appendStringInfo(&buf, "%lx.%x", (long) MyStartTime, MyProcPid);
- appendStringInfoChar(&buf, ',');
-
- /* Line number */
- appendStringInfo(&buf, "%ld", log_line_number);
- appendStringInfoChar(&buf, ',');
-
- /* PS display */
- if (MyProcPort)
- {
- StringInfoData msgbuf;
- const char *psdisp;
- int displen;
-
- initStringInfo(&msgbuf);
-
- psdisp = get_ps_display(&displen);
- appendBinaryStringInfo(&msgbuf, psdisp, displen);
- appendCSVLiteral(&buf, msgbuf.data);
-
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* session start timestamp */
- start_time = get_formatted_start_time();
- appendStringInfoString(&buf, start_time);
- appendStringInfoChar(&buf, ',');
-
- /* Virtual transaction id */
- /* keep VXID format in sync with lockfuncs.c */
- if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
- appendStringInfo(&buf, "%d/%u", MyProc->backendId, MyProc->lxid);
- appendStringInfoChar(&buf, ',');
-
- /* Transaction id */
- appendStringInfo(&buf, "%u", GetTopTransactionIdIfAny());
- appendStringInfoChar(&buf, ',');
-
- /* Error severity */
- appendStringInfoString(&buf, _(error_severity(edata->elevel)));
- appendStringInfoChar(&buf, ',');
-
- /* SQL state code */
- appendStringInfoString(&buf, unpack_sql_state(edata->sqlerrcode));
- appendStringInfoChar(&buf, ',');
-
- /* errmessage */
- appendCSVLiteral(&buf, edata->message);
- appendStringInfoChar(&buf, ',');
-
- /* errdetail or errdetail_log */
- if (edata->detail_log)
- appendCSVLiteral(&buf, edata->detail_log);
- else
- appendCSVLiteral(&buf, edata->detail);
- appendStringInfoChar(&buf, ',');
-
- /* errhint */
- appendCSVLiteral(&buf, edata->hint);
- appendStringInfoChar(&buf, ',');
-
- /* internal query */
- appendCSVLiteral(&buf, edata->internalquery);
- appendStringInfoChar(&buf, ',');
-
- /* if printed internal query, print internal pos too */
- if (edata->internalpos > 0 && edata->internalquery != NULL)
- appendStringInfo(&buf, "%d", edata->internalpos);
- appendStringInfoChar(&buf, ',');
-
- /* errcontext */
- if (!edata->hide_ctx)
- appendCSVLiteral(&buf, edata->context);
- appendStringInfoChar(&buf, ',');
-
- /* user query --- only reported if not disabled by the caller */
- print_stmt = check_log_of_query(edata);
- if (print_stmt)
- appendCSVLiteral(&buf, debug_query_string);
- appendStringInfoChar(&buf, ',');
- if (print_stmt && edata->cursorpos > 0)
- appendStringInfo(&buf, "%d", edata->cursorpos);
- appendStringInfoChar(&buf, ',');
-
- /* file error location */
- if (Log_error_verbosity >= PGERROR_VERBOSE)
- {
- StringInfoData msgbuf;
-
- initStringInfo(&msgbuf);
-
- if (edata->funcname && edata->filename)
- appendStringInfo(&msgbuf, "%s, %s:%d",
- edata->funcname, edata->filename,
- edata->lineno);
- else if (edata->filename)
- appendStringInfo(&msgbuf, "%s:%d",
- edata->filename, edata->lineno);
- appendCSVLiteral(&buf, msgbuf.data);
- pfree(msgbuf.data);
- }
- appendStringInfoChar(&buf, ',');
-
- /* application name */
- if (application_name)
- appendCSVLiteral(&buf, application_name);
-
- appendStringInfoChar(&buf, ',');
-
- /* backend type */
- appendCSVLiteral(&buf, get_backend_type_for_log());
- appendStringInfoChar(&buf, ',');
-
- /* leader PID */
- if (MyProc)
- {
- PGPROC *leader = MyProc->lockGroupLeader;
-
- /*
- * Show the leader only for active parallel workers. This leaves out
- * the leader of a parallel group.
- */
- if (leader && leader->pid != MyProcPid)
- appendStringInfo(&buf, "%d", leader->pid);
- }
- appendStringInfoChar(&buf, ',');
-
- /* query id */
- appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
-
- appendStringInfoChar(&buf, '\n');
-
- /* If in the syslogger process, try to write messages direct to file */
- if (MyBackendType == B_LOGGER)
- write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
- else
- write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_CSVLOG);
-
- pfree(buf.data);
-}
-
/*
* Unpack MAKE_SQLSTATE code. Note that this returns a pointer to a
* static buffer.
--
2.34.1
v9-0003-JSON-logging.patchtext/x-diff; charset=us-asciiDownload
From 077656335da1aaf12b543f5d05c493804adfa4d5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 10 Jan 2022 21:44:53 +0900
Subject: [PATCH v9 3/3] JSON logging
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog.h | 2 +
src/backend/postmaster/syslogger.c | 105 ++++++-
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 18 ++
src/backend/utils/error/jsonlog.c | 294 ++++++++++++++++++
src/backend/utils/misc/guc.c | 4 +-
src/backend/utils/misc/postgresql.conf.sample | 13 +-
src/bin/pg_ctl/t/004_logrotate.pl | 16 +-
doc/src/sgml/config.sgml | 203 +++++++++++-
11 files changed, 630 insertions(+), 34 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index 2df68a196e..1ca326e52e 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index f37956f100..77c5a6b9a3 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
@@ -453,6 +454,7 @@ extern void write_pipe_chunks(char *data, int len, int dest);
/* Destination-specific functions */
extern void write_csvlog(ErrorData *edata);
+extern void write_jsonlog(ErrorData *edata);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 2256f072aa..eb53260943 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -281,6 +283,8 @@ SysLoggerMain(int argc, char *argv[])
last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -367,6 +371,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off and
+ * we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -417,6 +429,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -426,7 +444,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -632,6 +650,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -729,6 +761,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -805,6 +842,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -817,6 +855,9 @@ syslogger_forkexec(void)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ syslogger_fdget(jsonlogFile));
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -834,7 +875,7 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
+ Assert(argc == 6);
argv += 3;
/*
@@ -848,6 +889,8 @@ syslogger_parseArgs(int argc, char *argv[])
syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
csvlogFile = syslogger_fdopen(fd);
+ fd = atoi(*argv++);
+ jsonlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
@@ -896,7 +939,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -918,6 +963,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1097,19 +1144,24 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
+ * If we're told to write to a structured log file, but it's not open,
+ * dump the data to syslogFile (which is always open) instead. This can
+ * happen if structured output is enabled after postmaster start and
+ * we've been unable to open logFile. There are also race conditions
+ * during a parameter change whereby backends might send us structured
+ * output before we open the logFile or after we close it. Writing
+ * formatted output to the regular log file isn't great, but it beats
+ * dropping log output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1180,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1292,6 +1345,8 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
logFileExt = NULL;
else if (target_dest == LOG_DESTINATION_CSVLOG)
logFileExt = ".csv";
+ else if (target_dest == LOG_DESTINATION_JSONLOG)
+ logFileExt = ".json";
else
{
/* cannot happen */
@@ -1379,6 +1434,12 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
&csvlogFile))
return;
+ /* file rotation for jsonlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_JSONLOG, &last_json_file_name,
+ &jsonlogFile))
+ return;
+
update_metainfo_datafile();
set_next_rotation_time();
@@ -1465,7 +1526,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1523,6 +1585,19 @@ update_metainfo_datafile(void)
return;
}
}
+
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fe4f180b6f..b3e1453bed 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 511b43e4ac..680497de46 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2984,6 +2984,22 @@ send_message_to_server_log(ErrorData *edata)
fallback_to_stderr = true;
}
+ /* Write to JSON log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_JSONLOG) != 0)
+ {
+ /*
+ * Send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written
+ * to stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_jsonlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
/*
* Write to stderr, if enabled or if required because of a previous
* limitation.
@@ -3059,6 +3075,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..f2101b59ff
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,294 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+static void appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key,
+ const char *fmt,...) pg_attribute_printf(4, 5);
+
+/*
+ * appendJSONKeyValue
+ * Append to a StringInfo a comma followed by a JSON key and value.
+ * The key is always escaped. The value can be escaped optionally.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value,
+ bool escape_value)
+{
+ Assert(key != NULL);
+
+ if (value == NULL)
+ return;
+
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+
+ if (escape_value)
+ escape_json(buf, value);
+ else
+ appendStringInfoString(buf, value);
+}
+
+
+/*
+ * appendJSONKeyValueFmt
+ *
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /* Allocate result buffer */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+
+ appendJSONKeyValue(buf, key, value, escape_key);
+
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+
+ /*
+ * First property does not use appendJSONKeyValue as it does not have
+ * comma prefix.
+ */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name, true);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name, true);
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueFmt(&buf, "pid", false, "%d", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host, true);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port, false);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", true, "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueFmt(&buf, "line_num", false, "%ld", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data, true);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", start_time, true);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", true, "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", false, "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity",
+ (char *) error_severity(edata->elevel), true);
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code",
+ unpack_sql_state(edata->sqlerrcode), true);
+
+ /* errmessage */
+ appendJSONKeyValue(&buf, "message", edata->message, true);
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log, true);
+ else
+ appendJSONKeyValue(&buf, "detail", edata->detail, true);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint, true);
+
+ /* internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery, true);
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueFmt(&buf, "internal_position", false, "%u",
+ edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context, true);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (check_log_of_query(edata))
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string, true);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueFmt(&buf, "cursor_position", false, "%d",
+ edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname, true);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename, true);
+ appendJSONKeyValueFmt(&buf, "file_line_num", false, "%d",
+ edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name, true);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log(), true);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueFmt(&buf, "leader_pid", false, "%d", leader->pid);
+ }
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", false, "%lld",
+ (long long) pgstat_get_my_query_id());
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 6fc5cbc09a..b651429ed6 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -4264,7 +4264,7 @@ static struct config_string ConfigureNamesString[] =
{"log_destination", PGC_SIGHUP, LOGGING_WHERE,
gettext_noop("Sets the destination for server log output."),
gettext_noop("Valid values are combinations of \"stderr\", "
- "\"syslog\", \"csvlog\", and \"eventlog\", "
+ "\"syslog\", \"csvlog\", \"jsonlog\" and \"eventlog\", "
"depending on the platform."),
GUC_LIST_INPUT
},
@@ -11740,6 +11740,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a1acd46b61..817d5f5324 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -432,14 +432,15 @@
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
- # stderr, csvlog, syslog, and eventlog,
- # depending on platform. csvlog
- # requires logging_collector to be on.
+ # stderr, csvlog, jsonlog, syslog, and
+ # eventlog, depending on platform.
+ # csvlog and jsonlog require
+ # logging_collector to be on.
# This is used when logging to stderr:
-#logging_collector = off # Enable capturing of stderr and csvlog
- # into log files. Required to be on for
- # csvlogs.
+#logging_collector = off # Enable capturing of stderr, jsonlog
+ # and csvlog into log files. Required
+ # to be on for csvlogs and jsonlogs.
# (change requires restart)
# These are only used if logging_collector is on:
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index e04331bfef..c0194359b4 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
-use Test::More tests => 10;
+use Test::More tests => 14;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -39,7 +39,7 @@ sub check_log_pattern
my $node = shift;
my $lfname = fetch_file_name($logfiles, $format);
- my $max_attempts = 180 * 10;
+ my $max_attempts = 360 * 10;
my $logcontents;
for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
@@ -65,7 +65,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -78,7 +78,7 @@ $node->start();
$node->psql('postgres', 'SELECT 1/0');
# might need to retry if logging collector process is slow...
-my $max_attempts = 180 * 10;
+my $max_attempts = 360 * 10;
my $current_logfiles;
for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
@@ -96,11 +96,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -122,7 +124,8 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
@@ -130,5 +133,6 @@ $node->psql('postgres', 'fee fi fo fum');
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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index afbb6c35e3..c29ced427c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5931,7 +5931,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5949,6 +5950,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ JSON-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5960,13 +5969,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6106,6 +6116,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7467,6 +7484,186 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ </para>
+
+ <para>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ <para>
+ Each log line is serialized as a JSON object as of the following
+ set of keys with their values.
+ </para>
+
+ <table>
+ <title>Keys and values of JSON log entries</title>
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Key name</entry>
+ <entry>Type</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>timestamp</literal></entry>
+ <entry>string</entry>
+ <entry>Time stamp with milliseconds</entry>
+ </row>
+ <row>
+ <entry><literal>user</literal></entry>
+ <entry>string</entry>
+ <entry>User name</entry>
+ </row>
+ <row>
+ <entry><literal>dbname</literal></entry>
+ <entry>string</entry>
+ <entry>Database name</entry>
+ </row>
+ <row>
+ <entry><literal>pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID</entry>
+ </row>
+ <row>
+ <entry><literal>remote_host</literal></entry>
+ <entry>string</entry>
+ <entry>Client host</entry>
+ </row>
+ <row>
+ <entry><literal>remote_port</literal></entry>
+ <entry>number</entry>
+ <entry>Client port</entry>
+ </row>
+ <row>
+ <entry><literal>session_id</literal></entry>
+ <entry>string</entry>
+ <entry>Session ID</entry>
+ </row>
+ <row>
+ <entry><literal>line_num</literal></entry>
+ <entry>number</entry>
+ <entry>Per-session line number</entry>
+ </row>
+ <row>
+ <entry><literal>ps</literal></entry>
+ <entry>string</entry>
+ <entry>Current ps display</entry>
+ </row>
+ <row>
+ <entry><literal>session_start</literal></entry>
+ <entry>string</entry>
+ <entry>Session start time</entry>
+ </row>
+ <row>
+ <entry><literal>vxid</literal></entry>
+ <entry>string</entry>
+ <entry>Virtual transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>txid</literal></entry>
+ <entry>string</entry>
+ <entry>Regular transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>error_severity</literal></entry>
+ <entry>string</entry>
+ <entry>Error severity</entry>
+ </row>
+ <row>
+ <entry><literal>state_code</literal></entry>
+ <entry>string</entry>
+ <entry>SQLSTATE code</entry>
+ </row>
+ <row>
+ <entry><literal>message</literal></entry>
+ <entry>string</entry>
+ <entry>Error message</entry>
+ </row>
+ <row>
+ <entry><literal>detail</literal></entry>
+ <entry>string</entry>
+ <entry>Error message detail</entry>
+ </row>
+ <row>
+ <entry><literal>hint</literal></entry>
+ <entry>string</entry>
+ <entry>Error message hint</entry>
+ </row>
+ <row>
+ <entry><literal>internal_query</literal></entry>
+ <entry>string</entry>
+ <entry>Internal query that led to the error</entry>
+ </row>
+ <row>
+ <entry><literal>internal_position</literal></entry>
+ <entry>number</entry>
+ <entry>Cursor index into internal query</entry>
+ </row>
+ <row>
+ <entry><literal>context</literal></entry>
+ <entry>string</entry>
+ <entry>Error context</entry>
+ </row>
+ <row>
+ <entry><literal>statement</literal></entry>
+ <entry>string</entry>
+ <entry>Client-supplied query string</entry>
+ </row>
+ <row>
+ <entry><literal>cursor_position</literal></entry>
+ <entry>string</entry>
+ <entry>Cursor index into query string</entry>
+ </row>
+ <row>
+ <entry><literal>func_name</literal></entry>
+ <entry>string</entry>
+ <entry>Error location function name</entry>
+ </row>
+ <row>
+ <entry><literal>file_name</literal></entry>
+ <entry>string</entry>
+ <entry>File name of error location</entry>
+ </row>
+ <row>
+ <entry><literal>file_line_num</literal></entry>
+ <entry>number</entry>
+ <entry>File line number of the error location</entry>
+ </row>
+ <row>
+ <entry><literal>application_name</literal></entry>
+ <entry>string</entry>
+ <entry>Client application name</entry>
+ </row>
+ <row>
+ <entry><literal>backend_type</literal></entry>
+ <entry>string</entry>
+ <entry>Type of backend</entry>
+ </row>
+ <row>
+ <entry><literal>leader_pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID of leader for active parallel workers</entry>
+ </row>
+ <row>
+ <entry><literal>query_id</literal></entry>
+ <entry>number</entry>
+ <entry>Query ID</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.34.1
On 1/10/22, 4:51 AM, "Michael Paquier" <michael@paquier.xyz> wrote:
The issue comes from an incorrect change in syslogger_parseArgs()
where I missed that the incrementation of argv by 3 has no need to be
changed. A build with -DEXEC_BACKEND is enough to show the failure,
which caused a crash when starting up the syslogger because of a NULL
pointer dereference. The attached v9 should be enough to switch the
CF bot to green.
I've been looking at the latest patch set intermittently and playing
around with jsonlog a little. It seems to work well, and I don't have
any significant comments about the code. 0001 and 0002 seem
straightforward and uncontroversial. IIUC 0003 simply introduces
jsonlog using the existing framework.
I wonder if we should consider tracking each log destination as a set
of function pointers. The main logging code would just loop through
the enabled log destinations and use these functions, and it otherwise
would be completely detached (i.e., no "if jsonlog" blocks). This
might open up the ability to define custom log destinations via
modules, too. However, I don't know if there's any real demand for
something like this, and it should probably be done separately from
introducing jsonlog, anyway.
Nathan
On Tue, Jan 11, 2022 at 08:34:26PM +0000, Bossart, Nathan wrote:
I've been looking at the latest patch set intermittently and playing
around with jsonlog a little. It seems to work well, and I don't have
any significant comments about the code. 0001 and 0002 seem
straightforward and uncontroversial.
Thanks. I have looked again at 0001 and 0002 today and applied both,
so it means that we are done with all the refactoring pieces proposed
up to now.
IIUC 0003 simply introduces jsonlog using the existing framework.
This part will have to wait a bit more, but yes, this piece should be
straight-forward.
I wonder if we should consider tracking each log destination as a set
of function pointers. The main logging code would just loop through
the enabled log destinations and use these functions, and it otherwise
would be completely detached (i.e., no "if jsonlog" blocks). This
might open up the ability to define custom log destinations via
modules, too. However, I don't know if there's any real demand for
something like this, and it should probably be done separately from
introducing jsonlog, anyway.
I am not sure that this is worth the complications, either.
--
Michael
Attachments:
v10-0001-JSON-logging.patchtext/x-diff; charset=us-asciiDownload
From b5b0e35d66e491df0f75c874ff4dd73fb7607c68 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 12 Jan 2022 15:19:16 +0900
Subject: [PATCH v10] JSON logging
---
src/include/postmaster/syslogger.h | 1 +
src/include/utils/elog.h | 2 +
src/backend/postmaster/syslogger.c | 105 ++++++-
src/backend/utils/adt/misc.c | 5 +-
src/backend/utils/error/Makefile | 3 +-
src/backend/utils/error/elog.c | 18 ++
src/backend/utils/error/jsonlog.c | 294 ++++++++++++++++++
src/backend/utils/misc/guc.c | 4 +-
src/backend/utils/misc/postgresql.conf.sample | 13 +-
src/bin/pg_ctl/t/004_logrotate.pl | 20 +-
doc/src/sgml/config.sgml | 203 +++++++++++-
11 files changed, 632 insertions(+), 36 deletions(-)
create mode 100644 src/backend/utils/error/jsonlog.c
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index 2df68a196e..1ca326e52e 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -64,6 +64,7 @@ typedef union
/* log destinations */
#define PIPE_PROTO_DEST_STDERR 0x10
#define PIPE_PROTO_DEST_CSVLOG 0x20
+#define PIPE_PROTO_DEST_JSONLOG 0x40
/* GUC options */
extern bool Logging_collector;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 5bc38663cb..3eb8de3966 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -436,6 +436,7 @@ extern bool syslog_split_messages;
#define LOG_DESTINATION_SYSLOG 2
#define LOG_DESTINATION_EVENTLOG 4
#define LOG_DESTINATION_CSVLOG 8
+#define LOG_DESTINATION_JSONLOG 16
/* Other exported functions */
extern void DebugFileOpen(void);
@@ -453,6 +454,7 @@ extern void write_pipe_chunks(char *data, int len, int dest);
/* Destination-specific functions */
extern void write_csvlog(ErrorData *edata);
+extern void write_jsonlog(ErrorData *edata);
#ifdef HAVE_SYSLOG
extern void set_syslog_parameters(const char *ident, int facility);
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 2256f072aa..8d26573817 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -86,9 +86,11 @@ static bool pipe_eof_seen = false;
static bool rotation_disabled = false;
static FILE *syslogFile = NULL;
static FILE *csvlogFile = NULL;
+static FILE *jsonlogFile = NULL;
NON_EXEC_STATIC pg_time_t first_syslogger_file_time = 0;
static char *last_sys_file_name = NULL;
static char *last_csv_file_name = NULL;
+static char *last_json_file_name = NULL;
/*
* Buffers for saving partial messages from different backends.
@@ -281,6 +283,8 @@ SysLoggerMain(int argc, char *argv[])
last_sys_file_name = logfile_getname(first_syslogger_file_time, NULL);
if (csvlogFile != NULL)
last_csv_file_name = logfile_getname(first_syslogger_file_time, ".csv");
+ if (jsonlogFile != NULL)
+ last_json_file_name = logfile_getname(first_syslogger_file_time, ".json");
/* remember active logfile parameters */
currentLogDir = pstrdup(Log_directory);
@@ -367,6 +371,14 @@ SysLoggerMain(int argc, char *argv[])
(csvlogFile != NULL))
rotation_requested = true;
+ /*
+ * Force a rotation if JSONLOG output was just turned on or off
+ * and we need to open or close jsonlogFile accordingly.
+ */
+ if (((Log_destination & LOG_DESTINATION_JSONLOG) != 0) !=
+ (jsonlogFile != NULL))
+ rotation_requested = true;
+
/*
* If rotation time parameter changed, reset next rotation time,
* but don't immediately force a rotation.
@@ -417,6 +429,12 @@ SysLoggerMain(int argc, char *argv[])
rotation_requested = true;
size_rotation_for |= LOG_DESTINATION_CSVLOG;
}
+ if (jsonlogFile != NULL &&
+ ftell(jsonlogFile) >= Log_RotationSize * 1024L)
+ {
+ rotation_requested = true;
+ size_rotation_for |= LOG_DESTINATION_JSONLOG;
+ }
}
if (rotation_requested)
@@ -426,7 +444,7 @@ SysLoggerMain(int argc, char *argv[])
* was sent by pg_rotate_logfile() or "pg_ctl logrotate".
*/
if (!time_based_rotation && size_rotation_for == 0)
- size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG;
+ size_rotation_for = LOG_DESTINATION_STDERR | LOG_DESTINATION_CSVLOG | LOG_DESTINATION_JSONLOG;
logfile_rotate(time_based_rotation, size_rotation_for);
}
@@ -632,6 +650,20 @@ SysLogger_Start(void)
pfree(filename);
}
+ /*
+ * Likewise for the initial JSON log file, if that's enabled. (Note that
+ * we open syslogFile even when only JSON output is nominally enabled,
+ * since some code paths will write to syslogFile anyway.)
+ */
+ if (Log_destination & LOG_DESTINATION_JSONLOG)
+ {
+ filename = logfile_getname(first_syslogger_file_time, ".json");
+
+ jsonlogFile = logfile_open(filename, "a", false);
+
+ pfree(filename);
+ }
+
#ifdef EXEC_BACKEND
switch ((sysloggerPid = syslogger_forkexec()))
#else
@@ -729,6 +761,11 @@ SysLogger_Start(void)
fclose(csvlogFile);
csvlogFile = NULL;
}
+ if (jsonlogFile != NULL)
+ {
+ fclose(jsonlogFile);
+ jsonlogFile = NULL;
+ }
return (int) sysloggerPid;
}
@@ -805,6 +842,7 @@ syslogger_forkexec(void)
int ac = 0;
char filenobuf[32];
char csvfilenobuf[32];
+ char jsonfilenobuf[32];
av[ac++] = "postgres";
av[ac++] = "--forklog";
@@ -817,6 +855,9 @@ syslogger_forkexec(void)
snprintf(csvfilenobuf, sizeof(csvfilenobuf), "%d",
syslogger_fdget(csvlogFile));
av[ac++] = csvfilenobuf;
+ snprintf(jsonfilenobuf, sizeof(jsonfilenobuf), "%d",
+ syslogger_fdget(jsonlogFile));
+ av[ac++] = jsonfilenobuf;
av[ac] = NULL;
Assert(ac < lengthof(av));
@@ -834,7 +875,7 @@ syslogger_parseArgs(int argc, char *argv[])
{
int fd;
- Assert(argc == 5);
+ Assert(argc == 6);
argv += 3;
/*
@@ -848,6 +889,8 @@ syslogger_parseArgs(int argc, char *argv[])
syslogFile = syslogger_fdopen(fd);
fd = atoi(*argv++);
csvlogFile = syslogger_fdopen(fd);
+ fd = atoi(*argv++);
+ jsonlogFile = syslogger_fdopen(fd);
}
#endif /* EXEC_BACKEND */
@@ -896,7 +939,9 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
/* Do we have a valid header? */
memcpy(&p, cursor, offsetof(PipeProtoHeader, data));
- dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR | PIPE_PROTO_DEST_CSVLOG);
+ dest_flags = p.flags & (PIPE_PROTO_DEST_STDERR |
+ PIPE_PROTO_DEST_CSVLOG |
+ PIPE_PROTO_DEST_JSONLOG);
if (p.nuls[0] == '\0' && p.nuls[1] == '\0' &&
p.len > 0 && p.len <= PIPE_MAX_PAYLOAD &&
p.pid != 0 &&
@@ -918,6 +963,8 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer)
dest = LOG_DESTINATION_STDERR;
else if ((p.flags & PIPE_PROTO_DEST_CSVLOG) != 0)
dest = LOG_DESTINATION_CSVLOG;
+ else if ((p.flags & PIPE_PROTO_DEST_JSONLOG) != 0)
+ dest = LOG_DESTINATION_JSONLOG;
else
{
/* this should never happen as of the header validation */
@@ -1097,19 +1144,24 @@ write_syslogger_file(const char *buffer, int count, int destination)
FILE *logfile;
/*
- * If we're told to write to csvlogFile, but it's not open, dump the data
- * to syslogFile (which is always open) instead. This can happen if CSV
- * output is enabled after postmaster start and we've been unable to open
- * csvlogFile. There are also race conditions during a parameter change
- * whereby backends might send us CSV output before we open csvlogFile or
- * after we close it. Writing CSV-formatted output to the regular log
- * file isn't great, but it beats dropping log output on the floor.
+ * If we're told to write to a structured log file, but it's not open,
+ * dump the data to syslogFile (which is always open) instead. This can
+ * happen if structured output is enabled after postmaster start and we've
+ * been unable to open logFile. There are also race conditions during a
+ * parameter change whereby backends might send us structured output
+ * before we open the logFile or after we close it. Writing formatted
+ * output to the regular log file isn't great, but it beats dropping log
+ * output on the floor.
*
- * Think not to improve this by trying to open csvlogFile on-the-fly. Any
+ * Think not to improve this by trying to open logFile on-the-fly. Any
* failure in that would lead to recursion.
*/
- logfile = (destination == LOG_DESTINATION_CSVLOG &&
- csvlogFile != NULL) ? csvlogFile : syslogFile;
+ if ((destination & LOG_DESTINATION_CSVLOG) && csvlogFile != NULL)
+ logfile = csvlogFile;
+ else if ((destination & LOG_DESTINATION_JSONLOG) && jsonlogFile != NULL)
+ logfile = jsonlogFile;
+ else
+ logfile = syslogFile;
rc = fwrite(buffer, 1, count, logfile);
@@ -1180,7 +1232,8 @@ pipeThread(void *arg)
if (Log_RotationSize > 0)
{
if (ftell(syslogFile) >= Log_RotationSize * 1024L ||
- (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L))
+ (csvlogFile != NULL && ftell(csvlogFile) >= Log_RotationSize * 1024L) ||
+ (jsonlogFile != NULL && ftell(jsonlogFile) >= Log_RotationSize * 1024L))
SetLatch(MyLatch);
}
LeaveCriticalSection(&sysloggerSection);
@@ -1292,6 +1345,8 @@ logfile_rotate_dest(bool time_based_rotation, int size_rotation_for,
logFileExt = NULL;
else if (target_dest == LOG_DESTINATION_CSVLOG)
logFileExt = ".csv";
+ else if (target_dest == LOG_DESTINATION_JSONLOG)
+ logFileExt = ".json";
else
{
/* cannot happen */
@@ -1379,6 +1434,12 @@ logfile_rotate(bool time_based_rotation, int size_rotation_for)
&csvlogFile))
return;
+ /* file rotation for jsonlog */
+ if (!logfile_rotate_dest(time_based_rotation, size_rotation_for, fntime,
+ LOG_DESTINATION_JSONLOG, &last_json_file_name,
+ &jsonlogFile))
+ return;
+
update_metainfo_datafile();
set_next_rotation_time();
@@ -1465,7 +1526,8 @@ update_metainfo_datafile(void)
mode_t oumask;
if (!(Log_destination & LOG_DESTINATION_STDERR) &&
- !(Log_destination & LOG_DESTINATION_CSVLOG))
+ !(Log_destination & LOG_DESTINATION_CSVLOG) &&
+ !(Log_destination & LOG_DESTINATION_JSONLOG))
{
if (unlink(LOG_METAINFO_DATAFILE) < 0 && errno != ENOENT)
ereport(LOG,
@@ -1523,6 +1585,19 @@ update_metainfo_datafile(void)
return;
}
}
+
+ if (last_json_file_name && (Log_destination & LOG_DESTINATION_JSONLOG))
+ {
+ if (fprintf(fh, "jsonlog %s\n", last_json_file_name) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write file \"%s\": %m",
+ LOG_METAINFO_DATAFILE_TMP)));
+ fclose(fh);
+ return;
+ }
+ }
fclose(fh);
if (rename(LOG_METAINFO_DATAFILE_TMP, LOG_METAINFO_DATAFILE) != 0)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fe4f180b6f..936fec451f 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -843,11 +843,12 @@ pg_current_logfile(PG_FUNCTION_ARGS)
{
logfmt = text_to_cstring(PG_GETARG_TEXT_PP(0));
- if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0)
+ if (strcmp(logfmt, "stderr") != 0 && strcmp(logfmt, "csvlog") != 0 &&
+ strcmp(logfmt, "jsonlog") != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("log format \"%s\" is not supported", logfmt),
- errhint("The supported log formats are \"stderr\" and \"csvlog\".")));
+ errhint("The supported log formats are \"stderr\", \"csvlog\", and \"jsonlog\".")));
}
fd = AllocateFile(LOG_METAINFO_DATAFILE, "r");
diff --git a/src/backend/utils/error/Makefile b/src/backend/utils/error/Makefile
index ef770dd2f2..65ba61fb3c 100644
--- a/src/backend/utils/error/Makefile
+++ b/src/backend/utils/error/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
assert.o \
csvlog.o \
- elog.o
+ elog.o \
+ jsonlog.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 4db41ba564..650927109d 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2984,6 +2984,22 @@ send_message_to_server_log(ErrorData *edata)
fallback_to_stderr = true;
}
+ /* Write to JSON log, if enabled */
+ if ((Log_destination & LOG_DESTINATION_JSONLOG) != 0)
+ {
+ /*
+ * Send JSON data if it's safe to do so (syslogger doesn't need the
+ * pipe). If this is not possible, fallback to an entry written to
+ * stderr.
+ */
+ if (redirection_done || MyBackendType == B_LOGGER)
+ {
+ write_jsonlog(edata);
+ }
+ else
+ fallback_to_stderr = true;
+ }
+
/*
* Write to stderr, if enabled or if required because of a previous
* limitation.
@@ -3059,6 +3075,8 @@ write_pipe_chunks(char *data, int len, int dest)
p.proto.flags |= PIPE_PROTO_DEST_STDERR;
else if (dest == LOG_DESTINATION_CSVLOG)
p.proto.flags |= PIPE_PROTO_DEST_CSVLOG;
+ else if (dest == LOG_DESTINATION_JSONLOG)
+ p.proto.flags |= PIPE_PROTO_DEST_JSONLOG;
/* write all but the last chunk */
while (len > PIPE_MAX_PAYLOAD)
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
new file mode 100644
index 0000000000..51b81fca23
--- /dev/null
+++ b/src/backend/utils/error/jsonlog.c
@@ -0,0 +1,294 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsonlog.c
+ * JSON logging
+ *
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/error/jsonlog.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "libpq/libpq.h"
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/syslogger.h"
+#include "storage/lock.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/backend_status.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/json.h"
+#include "utils/ps_status.h"
+
+static void appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key,
+ const char *fmt,...) pg_attribute_printf(4, 5);
+
+/*
+ * appendJSONKeyValue
+ * Append to a StringInfo a comma followed by a JSON key and value.
+ * The key is always escaped. The value can be escaped optionally.
+ */
+static void
+appendJSONKeyValue(StringInfo buf, const char *key, const char *value,
+ bool escape_value)
+{
+ Assert(key != NULL);
+
+ if (value == NULL)
+ return;
+
+ appendStringInfoChar(buf, ',');
+ escape_json(buf, key);
+ appendStringInfoChar(buf, ':');
+
+ if (escape_value)
+ escape_json(buf, value);
+ else
+ appendStringInfoString(buf, value);
+}
+
+
+/*
+ * appendJSONKeyValueFmt
+ *
+ * Evaluate the fmt string and then invoke appendJSONKeyValue as the
+ * value of the JSON property. Both the key and value will be escaped by
+ * appendJSONKeyValue.
+ */
+static void
+appendJSONKeyValueFmt(StringInfo buf, const char *key,
+ bool escape_key, const char *fmt,...)
+{
+ int save_errno = errno;
+ size_t len = 128; /* initial assumption about buffer size */
+ char *value;
+
+ for (;;)
+ {
+ va_list args;
+ size_t newlen;
+
+ /* Allocate result buffer */
+ value = (char *) palloc(len);
+
+ /* Try to format the data. */
+ errno = save_errno;
+ va_start(args, fmt);
+ newlen = pvsnprintf(value, len, fmt, args);
+ va_end(args);
+
+ if (newlen < len)
+ break; /* success */
+
+ /* Release buffer and loop around to try again with larger len. */
+ pfree(value);
+ len = newlen;
+ }
+
+ appendJSONKeyValue(buf, key, value, escape_key);
+
+ /* Clean up */
+ pfree(value);
+}
+
+/*
+ * Write logs in json format.
+ */
+void
+write_jsonlog(ErrorData *edata)
+{
+ StringInfoData buf;
+ char *start_time;
+ char *log_time;
+
+ /* static counter for line numbers */
+ static long log_line_number = 0;
+
+ /* Has the counter been reset in the current process? */
+ static int log_my_pid = 0;
+
+ /*
+ * This is one of the few places where we'd rather not inherit a static
+ * variable's value from the postmaster. But since we will, reset it when
+ * MyProcPid changes.
+ */
+ if (log_my_pid != MyProcPid)
+ {
+ log_line_number = 0;
+ log_my_pid = MyProcPid;
+ reset_formatted_start_time();
+ }
+ log_line_number++;
+
+ initStringInfo(&buf);
+
+ /* Initialize string */
+ appendStringInfoChar(&buf, '{');
+
+ /* timestamp with milliseconds */
+ log_time = get_formatted_log_time();
+
+ /*
+ * First property does not use appendJSONKeyValue as it does not have
+ * comma prefix.
+ */
+ escape_json(&buf, "timestamp");
+ appendStringInfoChar(&buf, ':');
+ escape_json(&buf, log_time);
+
+ /* username */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "user", MyProcPort->user_name, true);
+
+ /* database name */
+ if (MyProcPort)
+ appendJSONKeyValue(&buf, "dbname", MyProcPort->database_name, true);
+
+ /* Process ID */
+ if (MyProcPid != 0)
+ appendJSONKeyValueFmt(&buf, "pid", false, "%d", MyProcPid);
+
+ /* Remote host and port */
+ if (MyProcPort && MyProcPort->remote_host)
+ {
+ appendJSONKeyValue(&buf, "remote_host", MyProcPort->remote_host, true);
+ if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0')
+ appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port, false);
+ }
+
+ /* Session id */
+ appendJSONKeyValueFmt(&buf, "session_id", true, "%lx.%x", (long) MyStartTime, MyProcPid);
+
+ /* Line number */
+ appendJSONKeyValueFmt(&buf, "line_num", false, "%ld", log_line_number);
+
+ /* PS display */
+ if (MyProcPort)
+ {
+ StringInfoData msgbuf;
+ const char *psdisp;
+ int displen;
+
+ initStringInfo(&msgbuf);
+ psdisp = get_ps_display(&displen);
+ appendBinaryStringInfo(&msgbuf, psdisp, displen);
+ appendJSONKeyValue(&buf, "ps", msgbuf.data, true);
+
+ pfree(msgbuf.data);
+ }
+
+ /* session start timestamp */
+ start_time = get_formatted_start_time();
+ appendJSONKeyValue(&buf, "session_start", start_time, true);
+
+ /* Virtual transaction id */
+ /* keep VXID format in sync with lockfuncs.c */
+ if (MyProc != NULL && MyProc->backendId != InvalidBackendId)
+ appendJSONKeyValueFmt(&buf, "vxid", true, "%d/%u", MyProc->backendId, MyProc->lxid);
+
+ /* Transaction id */
+ appendJSONKeyValueFmt(&buf, "txid", false, "%u", GetTopTransactionIdIfAny());
+
+ /* Error severity */
+ if (edata->elevel)
+ appendJSONKeyValue(&buf, "error_severity",
+ (char *) error_severity(edata->elevel), true);
+
+ /* SQL state code */
+ if (edata->sqlerrcode)
+ appendJSONKeyValue(&buf, "state_code",
+ unpack_sql_state(edata->sqlerrcode), true);
+
+ /* errmessage */
+ appendJSONKeyValue(&buf, "message", edata->message, true);
+
+ /* errdetail or error_detail log */
+ if (edata->detail_log)
+ appendJSONKeyValue(&buf, "detail", edata->detail_log, true);
+ else
+ appendJSONKeyValue(&buf, "detail", edata->detail, true);
+
+ /* errhint */
+ if (edata->hint)
+ appendJSONKeyValue(&buf, "hint", edata->hint, true);
+
+ /* internal query */
+ if (edata->internalquery)
+ appendJSONKeyValue(&buf, "internal_query", edata->internalquery, true);
+
+ /* if printed internal query, print internal pos too */
+ if (edata->internalpos > 0 && edata->internalquery != NULL)
+ appendJSONKeyValueFmt(&buf, "internal_position", false, "%u",
+ edata->internalpos);
+
+ /* errcontext */
+ if (edata->context && !edata->hide_ctx)
+ appendJSONKeyValue(&buf, "context", edata->context, true);
+
+ /* user query --- only reported if not disabled by the caller */
+ if (check_log_of_query(edata))
+ {
+ appendJSONKeyValue(&buf, "statement", debug_query_string, true);
+ if (edata->cursorpos > 0)
+ appendJSONKeyValueFmt(&buf, "cursor_position", false, "%d",
+ edata->cursorpos);
+ }
+
+ /* file error location */
+ if (Log_error_verbosity >= PGERROR_VERBOSE)
+ {
+ if (edata->funcname)
+ appendJSONKeyValue(&buf, "func_name", edata->funcname, true);
+ if (edata->filename)
+ {
+ appendJSONKeyValue(&buf, "file_name", edata->filename, true);
+ appendJSONKeyValueFmt(&buf, "file_line_num", false, "%d",
+ edata->lineno);
+ }
+ }
+
+ /* Application name */
+ if (application_name && application_name[0] != '\0')
+ appendJSONKeyValue(&buf, "application_name", application_name, true);
+
+ /* backend type */
+ appendJSONKeyValue(&buf, "backend_type", get_backend_type_for_log(), true);
+
+ /* leader PID */
+ if (MyProc)
+ {
+ PGPROC *leader = MyProc->lockGroupLeader;
+
+ /*
+ * Show the leader only for active parallel workers. This leaves out
+ * the leader of a parallel group.
+ */
+ if (leader && leader->pid != MyProcPid)
+ appendJSONKeyValueFmt(&buf, "leader_pid", false, "%d", leader->pid);
+ }
+
+ /* query id */
+ appendJSONKeyValueFmt(&buf, "query_id", false, "%lld",
+ (long long) pgstat_get_my_query_id());
+
+ /* Finish string */
+ appendStringInfoChar(&buf, '}');
+ appendStringInfoChar(&buf, '\n');
+
+ /* If in the syslogger process, try to write messages direct to file */
+ if (MyBackendType == B_LOGGER)
+ write_syslogger_file(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+ else
+ write_pipe_chunks(buf.data, buf.len, LOG_DESTINATION_JSONLOG);
+
+ pfree(buf.data);
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 6fc5cbc09a..b651429ed6 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -4264,7 +4264,7 @@ static struct config_string ConfigureNamesString[] =
{"log_destination", PGC_SIGHUP, LOGGING_WHERE,
gettext_noop("Sets the destination for server log output."),
gettext_noop("Valid values are combinations of \"stderr\", "
- "\"syslog\", \"csvlog\", and \"eventlog\", "
+ "\"syslog\", \"csvlog\", \"jsonlog\" and \"eventlog\", "
"depending on the platform."),
GUC_LIST_INPUT
},
@@ -11740,6 +11740,8 @@ check_log_destination(char **newval, void **extra, GucSource source)
newlogdest |= LOG_DESTINATION_STDERR;
else if (pg_strcasecmp(tok, "csvlog") == 0)
newlogdest |= LOG_DESTINATION_CSVLOG;
+ else if (pg_strcasecmp(tok, "jsonlog") == 0)
+ newlogdest |= LOG_DESTINATION_JSONLOG;
#ifdef HAVE_SYSLOG
else if (pg_strcasecmp(tok, "syslog") == 0)
newlogdest |= LOG_DESTINATION_SYSLOG;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a1acd46b61..817d5f5324 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -432,14 +432,15 @@
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
- # stderr, csvlog, syslog, and eventlog,
- # depending on platform. csvlog
- # requires logging_collector to be on.
+ # stderr, csvlog, jsonlog, syslog, and
+ # eventlog, depending on platform.
+ # csvlog and jsonlog require
+ # logging_collector to be on.
# This is used when logging to stderr:
-#logging_collector = off # Enable capturing of stderr and csvlog
- # into log files. Required to be on for
- # csvlogs.
+#logging_collector = off # Enable capturing of stderr, jsonlog
+ # and csvlog into log files. Required
+ # to be on for csvlogs and jsonlogs.
# (change requires restart)
# These are only used if logging_collector is on:
diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl
index e04331bfef..de6028760d 100644
--- a/src/bin/pg_ctl/t/004_logrotate.pl
+++ b/src/bin/pg_ctl/t/004_logrotate.pl
@@ -6,7 +6,7 @@ use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
-use Test::More tests => 10;
+use Test::More tests => 14;
use Time::HiRes qw(usleep);
# Extract the file name of a $format from the contents of
@@ -65,7 +65,7 @@ $node->init();
$node->append_conf(
'postgresql.conf', qq(
logging_collector = on
-log_destination = 'stderr, csvlog'
+log_destination = 'stderr, csvlog, jsonlog'
# these ensure stability of test results:
log_rotation_age = 0
lc_messages = 'C'
@@ -96,11 +96,13 @@ note "current_logfiles = $current_logfiles";
like(
$current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'current_logfiles is sane');
-check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
-check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('stderr', $current_logfiles, 'division by zero', $node);
+check_log_pattern('csvlog', $current_logfiles, 'division by zero', $node);
+check_log_pattern('jsonlog', $current_logfiles, 'division by zero', $node);
# Sleep 2 seconds and ask for log rotation; this should result in
# output into a different log file name.
@@ -122,13 +124,15 @@ note "now current_logfiles = $new_current_logfiles";
like(
$new_current_logfiles,
qr|^stderr log/postgresql-.*log
-csvlog log/postgresql-.*csv$|,
+csvlog log/postgresql-.*csv
+jsonlog log/postgresql-.*json$|,
'new current_logfiles is sane');
# Verify that log output gets to this file, too
$node->psql('postgres', 'fee fi fo fum');
-check_log_pattern('stderr', $new_current_logfiles, 'syntax error', $node);
-check_log_pattern('csvlog', $new_current_logfiles, 'syntax error', $node);
+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);
$node->stop();
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index afbb6c35e3..c29ced427c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5931,7 +5931,8 @@ SELECT * FROM parent WHERE key = 2400;
<para>
<productname>PostgreSQL</productname> supports several methods
for logging server messages, including
- <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem> and
+ <systemitem>stderr</systemitem>, <systemitem>csvlog</systemitem>,
+ <systemitem>jsonlog</systemitem>, and
<systemitem>syslog</systemitem>. On Windows,
<systemitem>eventlog</systemitem> is also supported. Set this
parameter to a list of desired log destinations separated by
@@ -5949,6 +5950,14 @@ SELECT * FROM parent WHERE key = 2400;
<xref linkend="guc-logging-collector"/> must be enabled to generate
CSV-format log output.
</para>
+ <para>
+ If <systemitem>jsonlog</systemitem> is included in <varname>log_destination</varname>,
+ log entries are output in <acronym>JSON</acronym> format, which is convenient for
+ loading logs into programs.
+ See <xref linkend="runtime-config-logging-jsonlog"/> for details.
+ <xref linkend="guc-logging-collector"/> must be enabled to generate
+ JSON-format log output.
+ </para>
<para>
When either <systemitem>stderr</systemitem> or
<systemitem>csvlog</systemitem> are included, the file
@@ -5960,13 +5969,14 @@ SELECT * FROM parent WHERE key = 2400;
<programlisting>
stderr log/postgresql.log
csvlog log/postgresql.csv
+jsonlog log/postgresql.json
</programlisting>
<filename>current_logfiles</filename> is recreated when a new log file
is created as an effect of rotation, and
when <varname>log_destination</varname> is reloaded. It is removed when
- neither <systemitem>stderr</systemitem>
- nor <systemitem>csvlog</systemitem> are included
+ none of <systemitem>stderr</systemitem>,
+ <systemitem>csvlog</systemitem>, <systemitem>jsonlog</systemitem> are included
in <varname>log_destination</varname>, and when the logging collector is
disabled.
</para>
@@ -6106,6 +6116,13 @@ local0.* /var/log/postgresql
(If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
replaced instead.)
</para>
+ <para>
+ If JSON-format output is enabled in <varname>log_destination</varname>,
+ <literal>.json</literal> will be appended to the timestamped
+ log file name to create the file name for JSON-format output.
+ (If <varname>log_filename</varname> ends in <literal>.log</literal>, the suffix is
+ replaced instead.)
+ </para>
<para>
This parameter can only be set in the <filename>postgresql.conf</filename>
file or on the server command line.
@@ -7467,6 +7484,186 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</orderedlist>
</para>
</sect2>
+ <sect2 id="runtime-config-logging-jsonlog">
+ <title>Using JSON-Format Log Output</title>
+
+ <para>
+ Including <literal>jsonlog</literal> in the <varname>log_destination</varname> list
+ provides a convenient way to import log files into many different programs.
+ This option emits log lines in (<acronym>JSON</acronym>) format.
+ </para>
+
+ <para>
+ String fields with null values are excluded from output.
+ Additional fields may be added in the future. User applications that process jsonlog
+ output should ignore unknown fields.
+ </para>
+
+ <para>
+ Each log line is serialized as a JSON object as of the following
+ set of keys with their values.
+ </para>
+
+ <table>
+ <title>Keys and values of JSON log entries</title>
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Key name</entry>
+ <entry>Type</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>timestamp</literal></entry>
+ <entry>string</entry>
+ <entry>Time stamp with milliseconds</entry>
+ </row>
+ <row>
+ <entry><literal>user</literal></entry>
+ <entry>string</entry>
+ <entry>User name</entry>
+ </row>
+ <row>
+ <entry><literal>dbname</literal></entry>
+ <entry>string</entry>
+ <entry>Database name</entry>
+ </row>
+ <row>
+ <entry><literal>pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID</entry>
+ </row>
+ <row>
+ <entry><literal>remote_host</literal></entry>
+ <entry>string</entry>
+ <entry>Client host</entry>
+ </row>
+ <row>
+ <entry><literal>remote_port</literal></entry>
+ <entry>number</entry>
+ <entry>Client port</entry>
+ </row>
+ <row>
+ <entry><literal>session_id</literal></entry>
+ <entry>string</entry>
+ <entry>Session ID</entry>
+ </row>
+ <row>
+ <entry><literal>line_num</literal></entry>
+ <entry>number</entry>
+ <entry>Per-session line number</entry>
+ </row>
+ <row>
+ <entry><literal>ps</literal></entry>
+ <entry>string</entry>
+ <entry>Current ps display</entry>
+ </row>
+ <row>
+ <entry><literal>session_start</literal></entry>
+ <entry>string</entry>
+ <entry>Session start time</entry>
+ </row>
+ <row>
+ <entry><literal>vxid</literal></entry>
+ <entry>string</entry>
+ <entry>Virtual transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>txid</literal></entry>
+ <entry>string</entry>
+ <entry>Regular transaction ID</entry>
+ </row>
+ <row>
+ <entry><literal>error_severity</literal></entry>
+ <entry>string</entry>
+ <entry>Error severity</entry>
+ </row>
+ <row>
+ <entry><literal>state_code</literal></entry>
+ <entry>string</entry>
+ <entry>SQLSTATE code</entry>
+ </row>
+ <row>
+ <entry><literal>message</literal></entry>
+ <entry>string</entry>
+ <entry>Error message</entry>
+ </row>
+ <row>
+ <entry><literal>detail</literal></entry>
+ <entry>string</entry>
+ <entry>Error message detail</entry>
+ </row>
+ <row>
+ <entry><literal>hint</literal></entry>
+ <entry>string</entry>
+ <entry>Error message hint</entry>
+ </row>
+ <row>
+ <entry><literal>internal_query</literal></entry>
+ <entry>string</entry>
+ <entry>Internal query that led to the error</entry>
+ </row>
+ <row>
+ <entry><literal>internal_position</literal></entry>
+ <entry>number</entry>
+ <entry>Cursor index into internal query</entry>
+ </row>
+ <row>
+ <entry><literal>context</literal></entry>
+ <entry>string</entry>
+ <entry>Error context</entry>
+ </row>
+ <row>
+ <entry><literal>statement</literal></entry>
+ <entry>string</entry>
+ <entry>Client-supplied query string</entry>
+ </row>
+ <row>
+ <entry><literal>cursor_position</literal></entry>
+ <entry>string</entry>
+ <entry>Cursor index into query string</entry>
+ </row>
+ <row>
+ <entry><literal>func_name</literal></entry>
+ <entry>string</entry>
+ <entry>Error location function name</entry>
+ </row>
+ <row>
+ <entry><literal>file_name</literal></entry>
+ <entry>string</entry>
+ <entry>File name of error location</entry>
+ </row>
+ <row>
+ <entry><literal>file_line_num</literal></entry>
+ <entry>number</entry>
+ <entry>File line number of the error location</entry>
+ </row>
+ <row>
+ <entry><literal>application_name</literal></entry>
+ <entry>string</entry>
+ <entry>Client application name</entry>
+ </row>
+ <row>
+ <entry><literal>backend_type</literal></entry>
+ <entry>string</entry>
+ <entry>Type of backend</entry>
+ </row>
+ <row>
+ <entry><literal>leader_pid</literal></entry>
+ <entry>number</entry>
+ <entry>Process ID of leader for active parallel workers</entry>
+ </row>
+ <row>
+ <entry><literal>query_id</literal></entry>
+ <entry>number</entry>
+ <entry>Query ID</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
<sect2>
<title>Process Title</title>
--
2.34.1
On Wed, Jan 12, 2022 at 03:27:19PM +0900, Michael Paquier wrote:
This part will have to wait a bit more, but yes, this piece should be
straight-forward.
Okay, this last piece has been applied this morning, after more review
and a couple of adjustments, mainly cosmetic (pg_current_logfile
missed a refresh, incorrect copyright in jsonlog.c, etc.). Let's see
what the buildfarm thinks.
--
Michael
On Mon, Jan 17, 2022 at 10:48:06AM +0900, Michael Paquier wrote:
Okay, this last piece has been applied this morning, after more review
and a couple of adjustments, mainly cosmetic (pg_current_logfile
missed a refresh, incorrect copyright in jsonlog.c, etc.). Let's see
what the buildfarm thinks.
By the way, while on it, using directly COPY to load the logs from a
generated .json file can be trickier than it looks, as backslashes
require an extra escap when loading the data. One idea, while not the
best performance-wise, is to rely on COPY FROM PROGRAM with commands
like that:
CREATE TABLE logs (data jsonb);
COPY logs FROM PROGRAM 'cat logs.json | sed ''s/\\/\\\\/g''';
--
Michael
So, thinking about this, there is one important piece that is missing
here, which is the ability to change the default format for what we
write to stderr. Right now, if you have stderr output, it is always in
the "plain multiline" format, with no option to change it. If you want
a JSON log, you have to read a file. But ISTM it would be pretty useful
if you could say "log_default_format=json" and get the log that we get
in stderr in the JSON format instead.
From what I hear in the container world, what they would *prefer* (but
they don't often get) is to receive the JSON-format logs directly in
stderr from the daemons they run; they capture stderr and they have the
logs just in the format they need, without having to open the log files,
parsing the lines to rewrite in a different format as is done currently.
I think this would be a relatively easy patch to do. Opinions?
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
So, thinking about this, there is one important piece that is missing
here, which is the ability to change the default format for what we
write to stderr. Right now, if you have stderr output, it is always in
the "plain multiline" format, with no option to change it. If you want
a JSON log, you have to read a file. But ISTM it would be pretty useful
if you could say "log_default_format=json" and get the log that we get
in stderr in the JSON format instead.
From what I hear in the container world, what they would *prefer* (but
they don't often get) is to receive the JSON-format logs directly in
stderr from the daemons they run; they capture stderr and they have the
logs just in the format they need, without having to open the log files,
parsing the lines to rewrite in a different format as is done currently.
I think this would be a relatively easy patch to do. Opinions?
I think assuming that everything that comes out on the postmaster's stderr
is generated by our code is hopelessly naive. See for example glibc's
bleats when it detects malloc corruption, or when loading a shlib fails.
So I don't believe something like this can be made to work reliably.
The existing syslogger logic does have the ability to cope with
such out-of-protocol data. So maybe, if you are using syslogger,
you could have it transform such messages into some
lowest-common-denominator jsonlog format. But it's not going to
work to expect that to happen with raw stderr.
regards, tom lane
On Thu, Feb 10, 2022 at 07:45:17PM -0300, Alvaro Herrera wrote:
From what I hear in the container world, what they would *prefer* (but
they don't often get) is to receive the JSON-format logs directly in
stderr from the daemons they run; they capture stderr and they have the
logs just in the format they need, without having to open the log files,
parsing the lines to rewrite in a different format as is done currently.
Yes, I have been pinged about that, which is why there are still cases
for my out-of-core extension jsonlog that uses the elog hook.
I think this would be a relatively easy patch to do. Opinions?
The postmaster goes through a couple of loops with the fd to open for
the default format, that the syslogger inherits from the postmaster,
and I am pretty sure that there are a couple of code paths around the
postmaster startup that can be tricky to reason about.
Making the new parameter PGC_POSTMASTER makes things easier to handle,
still the postmaster generates a couple of LOG entries and redirects
them to stderr before loading any GUC values, which would mean that we
cannot make sure that all the logs are valid JSON objects. If we want
to be 100% waterproof here, we may want to track down the format to
use by default with a mean different than a GUC for the postmaster
startup? A file holding this information in the root of the data
folder would be one way.
--
Michael