>From 61fe332f35f49c59257e9dcd0b5e2ff80f1f4055 Mon Sep 17 00:00:00 2001
From: Oskari Saarenmaa <os@ohmu.fi>
Date: Thu, 9 Jan 2014 20:49:28 +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.
---
 src/backend/utils/error/elog.c | 183 ++++++++++++++++++++++++++++++++++++++++-
 src/backend/utils/misc/guc.c   |  14 +++-
 src/include/utils/guc.h        |   4 +
 src/include/utils/guc_tables.h |   1 +
 4 files changed, 199 insertions(+), 3 deletions(-)

diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 3de162b..c843e1a 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
 
 /*
@@ -2475,6 +2482,7 @@ static void
 write_csvlog(ErrorData *edata)
 {
 	StringInfoData buf;
+	int		requested_log_level;
 	bool		print_stmt = false;
 
 	/* static counter for line numbers */
@@ -2618,7 +2626,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;
@@ -2691,6 +2702,7 @@ static void
 send_message_to_server_log(ErrorData *edata)
 {
 	StringInfoData buf;
+	int requested_log_level;
 
 	initStringInfo(&buf);
 
@@ -2775,7 +2787,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)
 	{
@@ -3577,3 +3592,167 @@ trace_recovery(int trace_level)
 
 	return trace_level;
 }
+
+
+/*
+*/
+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;
+}
+
+bool
+check_log_sqlstate_error(char **newval, void **extra, GucSource source)
+{
+	const struct config_enum_entry *enum_entry;
+	char	   *rawstring, *new_newval, *rp;
+	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;
+
+	/* return the canonical version */
+	new_newval = (char *) malloc(strlen("XX000:warning,") * new_array_len);
+	if (!new_newval)
+	{
+		free(new_array);
+		return false;
+	}
+
+	rp = 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;
+			}
+		}
+		if (i > 1)
+			*rp++ = ',';
+		rp += sprintf(rp, "%s:%s",
+				unpack_sql_state(new_array[i] >> 32), level_str);
+	}
+
+	free(*newval);
+	*newval = new_newval;
+	*extra = new_array;
+
+	return true;
+}
+
+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..775a20e 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);
-- 
1.8.4.2

