>From 213f647657f318141e3866087a17a863a0f322d9 Mon Sep 17 00:00:00 2001
From: Oskari Saarenmaa <os@ohmu.fi>
Date: Tue, 14 Jan 2014 15:47:39 +0200
Subject: [PATCH] Filter error log statements by sqlstate

Allow the default log_min_error_statement to be overridden per sqlstate to
make it possible to filter out some error types while maintaining a low
log_min_error_statement or enable logging for some error types when the
default is to not log anything.
---
 doc/src/sgml/config.sgml          |  30 ++++++
 src/backend/utils/error/elog.c    | 220 +++++++++++++++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c      |  14 ++-
 src/include/utils/guc.h           |   4 +
 src/include/utils/guc_tables.h    |   1 +
 src/test/regress/expected/guc.out |  24 +++++
 src/test/regress/sql/guc.sql      |   8 ++
 7 files changed, 298 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0f2f2bf..73a58ad 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3743,6 +3743,36 @@ local0.*    /var/log/postgresql
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-log-sqlstate-error-statement" xreflabel="log_sqlstate_error_statement">
+      <term><varname>log_sqlstate_error_statement</varname> (<type>string</type>)</term>
+      <indexterm>
+       <primary><varname>log_sqlstate_error_statement</> configuration parameter</primary>
+      </indexterm>
+      <listitem>
+       <para>
+        Controls which error types in SQL statements condition are recorded
+        in the server log.  This overrides the global <xref
+        linkend="guc-log-min-messages"> per error type and can be used to
+        disable logging for certain error types and/or to enable logging for
+        other types.
+       </para>
+       <para>
+        The value must be a comma-separated list in the form
+        <literal>'ERRORCODE:LOGLEVEL,...'</literal>.  For example, a setting
+        of <literal>'P0001:PANIC,22012:ERROR'</literal> would never log the
+        SQL statements for errors generated by the PL/pgSQL
+        <literal>RAISE</literal> statement but would always log the
+        statements causing division by zero errors.
+
+        See <xref linkend="errcodes-appendix"> for the list of valid error
+        codes and <xref linkend="guc-log-min-messages"> for valid log
+        levels.
+
+        Only superusers can change this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-log-min-duration-statement" xreflabel="log_min_duration_statement">
       <term><varname>log_min_duration_statement</varname> (<type>integer</type>)</term>
       <indexterm>
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 8705586..2e74fd5 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -74,7 +74,9 @@
 #include "storage/ipc.h"
 #include "storage/proc.h"
 #include "tcop/tcopprot.h"
+#include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/guc_tables.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 
@@ -111,6 +113,11 @@ char	   *Log_line_prefix = NULL;		/* format for extra log line info */
 int			Log_destination = LOG_DESTINATION_STDERR;
 char	   *Log_destination_string = NULL;
 
+static uint64 *log_sqlstate_error_statement = NULL;
+static size_t log_sqlstate_error_statement_len = 0;
+
+static int	get_sqlstate_error_level(int sqlstate);
+
 #ifdef HAVE_SYSLOG
 
 /*
@@ -2496,6 +2503,7 @@ static void
 write_csvlog(ErrorData *edata)
 {
 	StringInfoData buf;
+	int			requested_log_level;
 	bool		print_stmt = false;
 
 	/* static counter for line numbers */
@@ -2639,7 +2647,10 @@ 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) &&
+	requested_log_level = get_sqlstate_error_level(edata->sqlerrcode);
+	if (requested_log_level < 0)
+		requested_log_level = log_min_error_statement;
+	if (is_log_level_output(edata->elevel, requested_log_level) &&
 		debug_query_string != NULL &&
 		!edata->hide_stmt)
 		print_stmt = true;
@@ -2712,6 +2723,7 @@ static void
 send_message_to_server_log(ErrorData *edata)
 {
 	StringInfoData buf;
+	int			requested_log_level;
 
 	initStringInfo(&buf);
 
@@ -2796,7 +2808,10 @@ 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) &&
+	requested_log_level = get_sqlstate_error_level(edata->sqlerrcode);
+	if (requested_log_level < 0)
+		requested_log_level = log_min_error_statement;
+	if (is_log_level_output(edata->elevel, requested_log_level) &&
 		debug_query_string != NULL &&
 		!edata->hide_stmt)
 	{
@@ -3598,3 +3613,204 @@ trace_recovery(int trace_level)
 
 	return trace_level;
 }
+
+
+/*
+ * Statement log error level can be overriden per sqlstate, the variable
+ * log_sqlstate_error_statement is a sorted array of
+ * log_sqlstate_error_statement_len uint64s where the high 32 bits contain
+ * the sqlstate and low 32 bits contain the required error level.
+ */
+
+/*
+ * get_sqlstate_error_level - perform a binary search for the requested
+ * state and return if it was found; return -1 if it was not found.
+ */
+static int
+get_sqlstate_error_level(int sqlstate)
+{
+	uint64		left = 0,
+				right = log_sqlstate_error_statement_len;
+
+	while (left < right)
+	{
+		uint64		middle = left + (right - left) / 2;
+		int			m_sqlstate = log_sqlstate_error_statement[middle] >> 32;
+
+		if (m_sqlstate == sqlstate)
+			return log_sqlstate_error_statement[middle] & 0xFFFFFFFF;
+		else if (m_sqlstate < sqlstate)
+			left = middle + 1;
+		else
+			right = middle;
+	}
+	return -1;
+}
+
+/*
+ * check_log_sqlstate_error - validate sqlstate:errorlevel lists and create
+ * sorted uint64 arrays from them; last occurance of each sqlstate is used.
+ */
+bool
+check_log_sqlstate_error(char **newval, void **extra, GucSource source)
+{
+	const struct config_enum_entry *enum_entry;
+	char	   *rawstring,
+			   *new_newval,
+			   *new_newval_end,
+			   *vptr;
+	List	   *elemlist;
+	ListCell   *l;
+	uint64	   *new_array = NULL;
+	int			i,
+				new_array_len = 0;
+
+	/* Need a modifiable copy of string */
+	rawstring = pstrdup(*newval);
+
+	/* Parse string into list of identifiers */
+	if (!SplitIdentifierString(rawstring, ',', &elemlist))
+	{
+		/* syntax error in list */
+		GUC_check_errdetail("List syntax is invalid.");
+		pfree(rawstring);
+		list_free(elemlist);
+		return false;
+	}
+
+	/*
+	 * GUC wants malloced results, allocate room for as many elements on the
+	 * list plus one to hold the array size
+	 */
+	new_array = (uint64 *) malloc(sizeof(uint64) * (list_length(elemlist) + 1));
+	if (!new_array)
+	{
+		pfree(rawstring);
+		list_free(elemlist);
+		return false;
+	}
+
+	/* validate list and insert the results in a sorted array */
+	foreach(l, elemlist)
+	{
+		char	   *tok = lfirst(l),
+				   *level_str = strchr(tok, ':');
+		int			level = -1,
+					sqlstate;
+		uint64		value;
+
+		if (level_str != NULL && (level_str - tok) == 5)
+		{
+			for (enum_entry = server_message_level_options;
+				 enum_entry && enum_entry->name;
+				 enum_entry++)
+			{
+				if (pg_strcasecmp(enum_entry->name, level_str + 1) == 0)
+				{
+					level = enum_entry->val;
+					break;
+				}
+			}
+		}
+		if (level < 0)
+		{
+			GUC_check_errdetail("Invalid sqlstate error definition: \"%s\".", tok);
+			new_array_len = -1;
+			break;
+		}
+		sqlstate = MAKE_SQLSTATE(pg_ascii_toupper(tok[0]), pg_ascii_toupper(tok[1]),
+								 pg_ascii_toupper(tok[2]), pg_ascii_toupper(tok[3]),
+								 pg_ascii_toupper(tok[4]));
+		value = (((uint64) sqlstate) << 32) | ((uint64) level);
+
+		for (i = 0; i <= new_array_len; i++)
+		{
+			if (i == new_array_len)
+			{
+				new_array[++new_array_len] = value;
+				break;
+			}
+			else if (sqlstate == (int) (new_array[i + 1] >> 32))
+			{
+				new_array[i + 1] = value;
+				break;
+			}
+			else if (sqlstate < (int) (new_array[i + 1] >> 32))
+			{
+				memmove(&new_array[i + 2], &new_array[i + 1],
+						(new_array_len - i) * sizeof(uint64));
+				++new_array_len;
+				new_array[i + 1] = value;
+				break;
+			}
+		}
+	}
+
+	pfree(rawstring);
+	list_free(elemlist);
+
+	if (new_array_len < 0)
+	{
+		free(new_array);
+		return false;
+	}
+
+	/* store the length in the first field */
+	new_array[0] = new_array_len;
+
+	/*
+	 * calculate the maximum length for the canonical string and
+	 * allocate a buffer for it.  sqlstate is always 5 characters and
+	 * the longest error level string currently is 'warning'.
+	 */
+	new_newval = (char *) malloc(strlen("XX000:warning,") * new_array_len + 1);
+	if (!new_newval)
+	{
+		free(new_array);
+		return false;
+	}
+
+	vptr = new_newval;
+	for (i = 1; i <= new_array_len; i++)
+	{
+		const char *level_str = "null";
+
+		for (enum_entry = server_message_level_options;
+			 enum_entry && enum_entry->name;
+			 enum_entry++)
+		{
+			if (enum_entry->val == (new_array[i] & 0xFFFFFFFF))
+			{
+				level_str = enum_entry->name;
+				break;
+			}
+		}
+
+		vptr += snprintf(vptr, new_newval_end - vptr, "%s%s:%s",
+						 (i > 1) ? "," : "",
+						 unpack_sql_state(new_array[i] >> 32),
+						 level_str);
+	}
+	*vptr = 0;
+
+	free(*newval);
+	*newval = new_newval;
+	*extra = new_array;
+
+	return true;
+}
+
+/*
+ * assign_log_sqlstate_error - take the array generated by
+ * check_log_sqlstate_error into use.  'extra' is an array of uint64s where
+ * the first element contains the list length and the remainder is
+ * sqlstate:errorlevel values.
+ */
+void
+assign_log_sqlstate_error(const char *newval, void *extra)
+{
+	uint64	   *myextra = (uint64 *) extra;
+
+	log_sqlstate_error_statement_len = myextra[0];
+	log_sqlstate_error_statement = &myextra[1];
+}
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1217098..f6cbee5 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -245,7 +245,7 @@ static const struct config_enum_entry client_message_level_options[] = {
 	{NULL, 0, false}
 };
 
-static const struct config_enum_entry server_message_level_options[] = {
+const struct config_enum_entry server_message_level_options[] = {
 	{"debug", DEBUG2, true},
 	{"debug5", DEBUG5, false},
 	{"debug4", DEBUG4, false},
@@ -465,6 +465,7 @@ static char *server_version_string;
 static int	server_version_num;
 static char *timezone_string;
 static char *log_timezone_string;
+static char *log_sqlstate_error_statement_str;
 static char *timezone_abbreviations_string;
 static char *XactIsoLevel_string;
 static char *session_authorization_string;
@@ -2947,6 +2948,17 @@ static struct config_string ConfigureNamesString[] =
 	},
 
 	{
+		{"log_sqlstate_error_statement", PGC_SUSET, LOGGING_WHEN,
+			gettext_noop("Overrides minimum error level per error type"),
+			gettext_noop("Value must be a comma-separated list in the format "
+						 "\"sqlstate:level,...\"."),
+		},
+		&log_sqlstate_error_statement_str,
+		"",
+		check_log_sqlstate_error, assign_log_sqlstate_error, NULL
+	},
+
+	{
 		{"syslog_ident", PGC_SIGHUP, LOGGING_WHERE,
 			gettext_noop("Sets the program name used to identify PostgreSQL "
 						 "messages in syslog."),
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 3adcc99..2ed5677 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -391,4 +391,8 @@ extern bool check_effective_cache_size(int *newval, void **extra, GucSource sour
 extern void set_default_effective_cache_size(void);
 extern void assign_xlog_sync_method(int new_sync_method, void *extra);
 
+/* in src/backend/utils/error/elog.c */
+extern void assign_log_sqlstate_error(const char *newval, void *extra);
+extern bool check_log_sqlstate_error(char **newval, void **extra, GucSource source);
+
 #endif   /* GUC_H */
diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h
index 47ff880..1e577a4 100644
--- a/src/include/utils/guc_tables.h
+++ b/src/include/utils/guc_tables.h
@@ -251,6 +251,7 @@ extern const char *const config_group_names[];
 extern const char *const config_type_names[];
 extern const char *const GucContext_Names[];
 extern const char *const GucSource_Names[];
+extern const struct config_enum_entry server_message_level_options[];
 
 /* get the current set of variables */
 extern struct config_generic **get_guc_variables(void);
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 4f0065c..0d0c4e0 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -736,3 +736,27 @@ NOTICE:  text search configuration "no_such_config" does not exist
 select func_with_bad_set();
 ERROR:  invalid value for parameter "default_text_search_config": "no_such_config"
 reset check_function_bodies;
+-- Test logging options
+SET log_sqlstate_error_statement = 'XX000:PANIC,22012:error,xx000:log';
+SHOW log_sqlstate_error_statement;
+ log_sqlstate_error_statement 
+------------------------------
+ XX000:log,22012:error
+(1 row)
+
+SET log_sqlstate_error_statement = '';
+SHOW log_sqlstate_error_statement;
+ log_sqlstate_error_statement 
+------------------------------
+ 
+(1 row)
+
+SET log_sqlstate_error_statement = 'x';
+ERROR:  invalid value for parameter "log_sqlstate_error_statement": "x"
+DETAIL:  Invalid sqlstate error definition: "x".
+SHOW log_sqlstate_error_statement;
+ log_sqlstate_error_statement 
+------------------------------
+ 
+(1 row)
+
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index 3de8a6b..508a8a4 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -275,3 +275,11 @@ set default_text_search_config = no_such_config;
 select func_with_bad_set();
 
 reset check_function_bodies;
+
+-- Test logging options
+SET log_sqlstate_error_statement = 'XX000:PANIC,22012:error,xx000:log';
+SHOW log_sqlstate_error_statement;
+SET log_sqlstate_error_statement = '';
+SHOW log_sqlstate_error_statement;
+SET log_sqlstate_error_statement = 'x';
+SHOW log_sqlstate_error_statement;
-- 
1.8.4.2

