diff --git a/contrib/Makefile b/contrib/Makefile
index da4e231..9144cb7 100644
*** a/contrib/Makefile
--- b/contrib/Makefile
*************** SUBDIRS = \
*** 34,39 ****
--- 34,40 ----
  		pg_buffercache	\
  		pg_freespacemap \
  		pg_prewarm	\
+ 		pg_stat_logmsg \
  		pg_stat_statements \
  		pg_surgery	\
  		pg_trgm		\
diff --git a/contrib/meson.build b/contrib/meson.build
index 84d4e18..4e8ac39 100644
*** a/contrib/meson.build
--- b/contrib/meson.build
*************** subdir('pgcrypto')
*** 44,49 ****
--- 44,50 ----
  subdir('pg_freespacemap')
  subdir('pg_prewarm')
  subdir('pgrowlocks')
+ subdir('pg_stat_logmsg')
  subdir('pg_stat_statements')
  subdir('pgstattuple')
  subdir('pg_surgery')
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index ab7e38b..4dfc5dd 100644
*** a/doc/src/sgml/contrib.sgml
--- b/doc/src/sgml/contrib.sgml
*************** CREATE EXTENSION <replaceable>extension_
*** 157,162 ****
--- 157,163 ----
   &pgfreespacemap;
   &pgprewarm;
   &pgrowlocks;
+  &pgstatlogmsg;
   &pgstatstatements;
   &pgstattuple;
   &pgsurgery;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 4c63a7e..acede66 100644
*** a/doc/src/sgml/filelist.sgml
--- b/doc/src/sgml/filelist.sgml
***************
*** 145,150 ****
--- 145,151 ----
  <!ENTITY pgfreespacemap  SYSTEM "pgfreespacemap.sgml">
  <!ENTITY pgprewarm       SYSTEM "pgprewarm.sgml">
  <!ENTITY pgrowlocks      SYSTEM "pgrowlocks.sgml">
+ <!ENTITY pgstatlogmsg    SYSTEM "pgstatlogmsg.sgml">
  <!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
  <!ENTITY pgstattuple     SYSTEM "pgstattuple.sgml">
  <!ENTITY pgsurgery       SYSTEM "pgsurgery.sgml">
diff --git a/contrib/pg_stat_logmsg/.gitignore b/contrib/pg_stat_logmsg/.gitignore
index ...5dcb3ff .
*** a/contrib/pg_stat_logmsg/.gitignore
--- b/contrib/pg_stat_logmsg/.gitignore
***************
*** 0 ****
--- 1,4 ----
+ # Generated subdirectories
+ /log/
+ /results/
+ /tmp_check/
diff --git a/contrib/pg_stat_logmsg/LICENSE b/contrib/pg_stat_logmsg/LICENSE
index ...998f814 .
*** a/contrib/pg_stat_logmsg/LICENSE
--- b/contrib/pg_stat_logmsg/LICENSE
***************
*** 0 ****
--- 1,4 ----
+ This code is released under the PostgreSQL licence, as given at
+ http://www.postgresql.org/about/licence/
+ 
+ Copyright is novated to the PostgreSQL Global Development Group.
diff --git a/contrib/pg_stat_logmsg/Makefile b/contrib/pg_stat_logmsg/Makefile
index ...73e2f0e .
*** a/contrib/pg_stat_logmsg/Makefile
--- b/contrib/pg_stat_logmsg/Makefile
***************
*** 0 ****
--- 1,29 ----
+ # contrib/pg_stat_logmsg/Makefile
+ 
+ MODULE_big = pg_stat_logmsg
+ OBJS = \
+ 	$(WIN32RES) \
+ 	pg_stat_logmsg.o
+ 
+ EXTENSION = pg_stat_logmsg
+ DATA = pg_stat_logmsg--1.0.sql
+ PGFILEDESC = "pg_stat_logmsg - statistics of log messages"
+ 
+ LDFLAGS_SL += $(filter -lm, $(LIBS))
+ 
+ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_logmsg/pg_stat_logmsg.conf
+ REGRESS = select
+ # Disabled because these tests require "shared_preload_libraries=pg_stat_logmsg",
+ # which typical installcheck users do not have (e.g. buildfarm clients).
+ NO_INSTALLCHECK = 1
+ 
+ ifdef USE_PGXS
+ PG_CONFIG = pg_config
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
+ include $(PGXS)
+ else
+ subdir = contrib/pg_stat_logmsg
+ top_builddir = ../..
+ include $(top_builddir)/src/Makefile.global
+ include $(top_srcdir)/contrib/contrib-global.mk
+ endif
diff --git a/contrib/pg_stat_logmsg/README.md b/contrib/pg_stat_logmsg/README.md
index ...b16ef1f .
*** a/contrib/pg_stat_logmsg/README.md
--- b/contrib/pg_stat_logmsg/README.md
***************
*** 0 ****
--- 1,137 ----
+ # PostgreSQL pg_stat_logmsg Extension Module
+ 
+ ## Overview
+ This extension adds support to track statistics of log messages into PostgreSQL. It behaves in a very similar manner to pg_stat_stayements, except counts are aggregated by log message file, line number, and level. The SQL error code and log message format string are also provided for context.
+ 
+ ## Syntax
+ 
+ 
+ 
+ ## Configuration
+ * Add pg_stat_logmsg to shared_preload_libraries in postgresql.conf.
+ ```
+ shared_preload_libraries = 'pg_stat_logmsg'
+ ```
+ * The following custom parameters may be set:
+ ```
+ # Sets the maximum number of messages tracked by pg_stat_logmsg.
+ pg_stat_logmsg.max = 5000
+ 
+ ```
+ 
+ ## Installation
+ 
+ ### Requirements
+ 
+ * PostgreSQL version 11 or newer
+ 
+ ### Compile and Install
+ 
+ Clone PostgreSQL repository:
+ 
+ ```bash
+ $> git clone https://github.com/postgres/postgres.git
+ ```
+ 
+ Checkout REL_16_STABLE (for example) branch:
+ 
+ ```bash
+ $> git checkout REL_16_STABLE
+ ```
+ 
+ Make PostgreSQL:
+ 
+ ```bash
+ $> ./configure
+ $> make install -s
+ ```
+ 
+ Change to the contrib directory:
+ 
+ ```bash
+ $> cd contrib
+ ```
+ 
+ Clone ```pg_stat_logmsg``` extension:
+ 
+ ```bash
+ $> git clone https://github.com/pg-ext/pg_stat_logmsg
+ ```
+ 
+ Change to ```pg_stat_logmsg``` directory:
+ 
+ ```bash
+ $> cd pg_stat_logmsg
+ ```
+ 
+ Build ```pg_stat_logmsg```:
+ 
+ ```bash
+ $> make
+ ```
+ 
+ Install ```pg_stat_logmsg```:
+ 
+ ```bash
+ $> make install
+ ```
+ 
+ #### Using PGXS
+ 
+ If an instance of PostgreSQL is already installed, then PGXS can be utilized to build and install ```pg_stat_logmsg```.  Ensure that PostgreSQL binaries are available via the ```$PATH``` environment variable then use the following commands.
+ 
+ ```bash
+ $> make USE_PGXS=1
+ $> make USE_PGXS=1 install
+ ```
+ 
+ ### Configure
+ 
+ The following bash commands should configure your system to utilize pg_stat_logmsg. Replace all paths as appropriate. It may be prudent to visually inspect the files afterward to ensure the changes took place.
+ 
+ ###### Initialize PostgreSQL (if needed):
+ 
+ ```bash
+ $> initdb -D /path/to/data/directory
+ ```
+ 
+ ###### Create Target Database (if needed):
+ 
+ ```bash
+ $> createdb <database>
+ ```
+ 
+ ###### Install ```pg_stat_logmsg``` functions:
+ 
+ Edit postgresql.conf and add ```pg_stat_logmsg``` to the shared_preload_libraries line, and change custom settings as mentioned above.
+ 
+ Finally, restart PostgreSQL (method may vary):
+ 
+ ```
+ $> service postgresql restart
+ ```
+ Install the extension into your database:
+ 
+ ```bash
+ psql <database>
+ CREATE EXTENSION pg_stat_logmsg;
+ ```
+ 
+ ###### Test basic functionality:
+ 
+ Examples:
+ ```
+ select * from pg_stat_logmsg;
+ 
+ 
+ 
+ 
+ ```
+ 
+ ## Notes
+ 
+ ##  Licensing
+ 
+ Please see the [LICENSE](./LICENSE) file.
+ 
+ 
diff --git a/contrib/pg_stat_logmsg/meson.build b/contrib/pg_stat_logmsg/meson.build
index ...25fda63 .
*** a/contrib/pg_stat_logmsg/meson.build
--- b/contrib/pg_stat_logmsg/meson.build
***************
*** 0 ****
--- 1,41 ----
+ # Copyright (c) 2022-2023, PostgreSQL Global Development Group
+ 
+ pg_stat_logmsg_sources = files(
+   'pg_stat_logmsg.c',
+ )
+ 
+ if host_system == 'windows'
+   pg_stat_logmsg_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+     '--NAME', 'pg_stat_logmsg',
+     '--FILEDESC', 'pg_stat_logmsg - execution statistics of log messages',])
+ endif
+ 
+ pg_stat_logmsg = shared_module('pg_stat_logmsg',
+   pg_stat_logmsg_sources,
+   kwargs: contrib_mod_args + {
+     'dependencies': contrib_mod_args['dependencies'],
+   },
+ )
+ contrib_targets += pg_stat_logmsg
+ 
+ install_data(
+   'pg_stat_logmsg.control',
+   'pg_stat_logmsg--1.0.sql',
+   kwargs: contrib_data_args,
+ )
+ 
+ tests += {
+   'name': 'pg_stat_logmsg',
+   'sd': meson.current_source_dir(),
+   'bd': meson.current_build_dir(),
+   'regress': {
+     'sql': [
+       'select',
+     ],
+     'regress_args': ['--temp-config', files('pg_stat_logmsg.conf')],
+     # Disabled because these tests require
+     # "shared_preload_libraries=pg_stat_logmsg", which typical
+     # runningcheck users do not have (e.g. buildfarm clients).
+     'runningcheck': false,
+   },
+ }
diff --git a/contrib/pg_stat_logmsg/pg_stat_logmsg--1.0.sql b/contrib/pg_stat_logmsg/pg_stat_logmsg--1.0.sql
index ...4ccb657 .
*** a/contrib/pg_stat_logmsg/pg_stat_logmsg--1.0.sql
--- b/contrib/pg_stat_logmsg/pg_stat_logmsg--1.0.sql
***************
*** 0 ****
--- 1,49 ----
+ /* contrib/pg_stat_logmsg/pg_stat_logmsg--1.0.sql */
+ 
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pg_stat_logmsg" to load this file. \quit
+ 
+ -- Register functions.
+ CREATE FUNCTION pg_stat_logmsg_reset()
+ RETURNS void
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C PARALLEL SAFE;
+ 
+ CREATE FUNCTION pg_stat_logmsg(
+     OUT pgversion text,
+     OUT filename text,
+     OUT lineno int,
+     OUT elevel text,
+     OUT funcname text,
+     OUT sqlerrcode text,
+     OUT message text,
+     OUT translated_message text,
+     OUT count bigint
+ )
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME', 'pg_stat_logmsg'
+ LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+ 
+ -- Register a view on the function for ease of use.
+ CREATE VIEW pg_stat_logmsg AS
+   SELECT * FROM pg_stat_logmsg();
+ 
+ GRANT SELECT ON pg_stat_logmsg TO PUBLIC;
+ 
+ -- Don't want this to be available to non-superusers.
+ REVOKE ALL ON FUNCTION pg_stat_logmsg_reset() FROM PUBLIC;
+ 
+ --- Define pg_stat_logmsg_info
+ CREATE FUNCTION pg_stat_logmsg_info(
+     OUT dealloc bigint,
+     OUT stats_reset timestamp with time zone
+ )
+ RETURNS record
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+ 
+ CREATE VIEW pg_stat_logmsg_info AS
+   SELECT * FROM pg_stat_logmsg_info();
+ 
+ GRANT SELECT ON pg_stat_logmsg_info TO PUBLIC;
+ 
diff --git a/contrib/pg_stat_logmsg/pg_stat_logmsg.c b/contrib/pg_stat_logmsg/pg_stat_logmsg.c
index ...4692527 .
*** a/contrib/pg_stat_logmsg/pg_stat_logmsg.c
--- b/contrib/pg_stat_logmsg/pg_stat_logmsg.c
***************
*** 0 ****
--- 1,1822 ----
+ /*-------------------------------------------------------------------------
+  *
+  * pg_stat_logmsg.c
+  *		Track log messages across a whole database cluster.
+  *
+  * Starting code cribbed from pg_stat_statements and modified to suit
+  *
+  * Copyright (c) 2023, PostgreSQL Global Development Group
+  *
+  * IDENTIFICATION
+  *	  contrib/pg_stat_logmsg/pg_stat_logmsg.c
+  *
+  *-------------------------------------------------------------------------
+  */
+ #include "postgres.h"
+ 
+ #include <math.h>
+ #include <sys/stat.h>
+ #include <unistd.h>
+ 
+ #include "access/htup_details.h"
+ #include "access/parallel.h"
+ #include "access/xact.h"
+ #include "catalog/pg_authid.h"
+ #if PG_VERSION_NUM >= 130000
+ #include "common/hashfn.h"
+ #else
+ #include "utils/hashutils.h"
+ #endif /* PG_VERSION_NUM >= 130000 */
+ #include "funcapi.h"
+ #include "mb/pg_wchar.h"
+ #include "miscadmin.h"
+ #include "pgstat.h"
+ #include "storage/fd.h"
+ #include "storage/ipc.h"
+ #include "storage/lwlock.h"
+ #include "storage/shmem.h"
+ #include "storage/spin.h"
+ #include "tcop/utility.h"
+ #include "utils/acl.h"
+ #include "utils/builtins.h"
+ #include "utils/memutils.h"
+ #include "utils/timestamp.h"
+ 
+ PG_MODULE_MAGIC;
+ 
+ /* Location of permanent stats file (valid when database is shut down) */
+ #define PGLM_DUMP_FILE	PGSTAT_STAT_PERMANENT_DIRECTORY "/pg_stat_logmsg.stat"
+ 
+ /*
+  * Location of external logmsg text file.
+  */
+ #define PGLM_TEXT_FILE	PG_STAT_TMP_DIR "/pglm_logmsg_texts.stat"
+ 
+ /* Magic number identifying the stats file format */
+ static const uint32 PGLM_FILE_HEADER = 0x20230625;
+ 
+ /* PostgreSQL major version number, changes in which invalidate all entries */
+ static const uint32 PGLM_PG_MAJOR_VERSION = PG_VERSION_NUM / 100;
+ 
+ #define ASSUMED_MEDIAN_INIT		(10.0)	/* initial assumed median usage */
+ #define ASSUMED_LENGTH_INIT		1024	/* initial assumed mean query length */
+ #define USAGE_DECREASE_FACTOR	(0.99)	/* decreased every entry_dealloc */
+ #define USAGE_DEALLOC_PERCENT	5		/* free this % of entries at once */
+ #define USAGE_INIT				(1.0)	/* including initial planning */
+ #define MAXFILENAMELEN			64
+ #define MAXFUNCNAMELEN			64
+ 
+ /*
+  * Hashtable key that defines the identity of a hashtable entry.
+  *
+  * If you add a new key to this struct, make sure to teach pglm_store() to
+  * zero the padding bytes.  Otherwise, things will break, because pglm_hash is
+  * created using HASH_BLOBS, and thus tag_hash is used to hash this.
+  */
+ typedef struct pglmHashKey
+ {
+ 	char	filename[MAXFILENAMELEN];	/* filename */
+ 	int		lineno;						/* line number */
+ 	int		elevel;						/* log level */
+ } pglmHashKey;
+ 
+ /*
+  * The actual stats counters kept within pglmEntry.
+  */
+ typedef struct Counters
+ {
+ 	int64	msgcnt;		/* # of times planned/executed */
+ 	double	usage;		/* usage factor */
+ } Counters;
+ 
+ /*
+  * Global statistics for pg_stat_logmsg
+  */
+ typedef struct pglmGlobalStats
+ {
+ 	int64		dealloc;		/* # of times entries were deallocated */
+ 	TimestampTz stats_reset;	/* timestamp with all stats reset */
+ } pglmGlobalStats;
+ 
+ /*
+  * Statistics per statement
+  *
+  * Note: in event of a failure in garbage collection of the logmsg text file,
+  * we reset logmsg_offset to zero and logmsg_len to -1.  This will be seen as
+  * an invalid state by logmsg_fetch().
+  */
+ typedef struct pglmEntry
+ {
+ 	pglmHashKey key;						/* hash key of entry - MUST BE FIRST */
+ 	Counters	counters;					/* the statistics for this logmsg */
+ 	Size		logmsg_offset;				/* logmsg text offset in external file */
+ 	int			logmsg_len;					/* # of valid bytes in logmsg string, or -1 */
+ 	int			encoding;					/* logmsg text encoding */
+ 	int			sqlerrcode;					/* encoded ERRSTATE */
+ 	char		funcname[MAXFUNCNAMELEN];	/* edata function name */
+ 	slock_t		mutex;						/* protects the counters only */
+ } pglmEntry;
+ 
+ /*
+  * Global shared state
+  */
+ typedef struct pglmSharedState
+ {
+ 	LWLock	   *lock;			/* protects hashtable search/modification */
+ 	double		cur_median_usage;	/* current median usage in hashtable */
+ 	Size		mean_logmsg_len; /* current mean entry text length */
+ 	slock_t		mutex;			/* protects following fields only: */
+ 	Size		extent;			/* current extent of query file */
+ 	int			n_writers;		/* number of active writers to query file */
+ 	int			gc_count;		/* query file garbage collection cycle count */
+ 	pglmGlobalStats stats;		/* global statistics for pglm */
+ } pglmSharedState;
+ 
+ /*---- Local variables ----*/
+ 
+ /* anti recursion */
+ static bool is_in_log_hook = false;
+ 
+ /* Saved hook values in case of unload */
+ #if PG_VERSION_NUM >= 150000
+ static shmem_request_hook_type prev_shmem_request_hook = NULL;
+ #endif /* PG_VERSION_NUM >= 150000 */
+ static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+ static emit_log_hook_type prev_emit_log_hook = NULL;
+ 
+ /* Links to shared memory state */
+ static pglmSharedState *pglm = NULL;
+ static HTAB *pglm_hash = NULL;
+ 
+ /*---- GUC variables ----*/
+ static int	pglm_max = 5000;	/* max # statements to track */
+ 
+ #define record_gc_msgfmts() \
+ 	do { \
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm; \
+ 		SpinLockAcquire(&s->mutex); \
+ 		s->gc_count++; \
+ 		SpinLockRelease(&s->mutex); \
+ 	} while(0)
+ 
+ /*---- Function declarations ----*/
+ void _PG_init(void);
+ PG_FUNCTION_INFO_V1(pg_stat_logmsg_reset);
+ PG_FUNCTION_INFO_V1(pg_stat_logmsg);
+ PG_FUNCTION_INFO_V1(pg_stat_logmsg_info);
+ 
+ #if PG_VERSION_NUM >= 150000
+ static void pglm_shmem_request(void);
+ #endif /* PG_VERSION_NUM >= 150000 */
+ static void pglm_shmem_startup(void);
+ static void pglm_shmem_shutdown(int code, Datum arg);
+ static void pglm_log_hook(ErrorData *edata);
+ static void pglm_store(ErrorData *edata);
+ static void pg_stat_logmsg_internal(FunctionCallInfo fcinfo);
+ static Size pglm_memsize(void);
+ static pglmEntry *entry_alloc(pglmHashKey *key, Size logmsg_offset,
+ 							  int logmsg_len, int encoding,
+ 							  int sqlerrcode, char *funcname);
+ static void entry_dealloc(void);
+ static bool logmsg_store(ErrorData *edata, Size *logmsg_offset,
+ 						 int *logmsg_len, int *gc_count);
+ static char *logmsg_load_file(Size *buffer_size);
+ static char *logmsg_fetch(Size logmsg_offset, int logmsg_len,
+ 						 char *buffer, Size buffer_size);
+ static bool need_gc_logmsgs(void);
+ static void gc_logmsgs(void);
+ static void entry_reset(char *filename, int lineno, int elevel);
+ static void logmsg_xact_callback(XactEvent event, void *arg);
+ #if PG_VERSION_NUM < 150000
+ static void InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags);
+ static const char *error_severity(int elevel);
+ #endif /* PG_VERSION_NUM < 150000 */
+ 
+ /*
+  * Module load callback
+  */
+ void
+ _PG_init(void)
+ {
+ 	/*
+ 	 * In order to create our shared memory area, we have to be loaded via
+ 	 * shared_preload_libraries.  If not, fall out without hooking into any of
+ 	 * the main system.  (We don't throw error here because it seems useful to
+ 	 * allow the pg_stat_logmsg functions to be created even when the
+ 	 * module isn't active.  The functions must protect themselves against
+ 	 * being called then, however.)
+ 	 */
+ 	if (!process_shared_preload_libraries_in_progress)
+ 		return;
+ 
+ 	/*
+ 	 * Define (or redefine) custom GUC variables.
+ 	 */
+ 	DefineCustomIntVariable("pg_stat_logmsg.max",
+ 							"Sets the maximum number of messages tracked by pg_stat_logmsg.",
+ 							NULL,
+ 							&pglm_max,
+ 							5000,
+ 							100,
+ 							INT_MAX / 2,
+ 							PGC_POSTMASTER,
+ 							0,
+ 							NULL,
+ 							NULL,
+ 							NULL);
+ 
+ #if PG_VERSION_NUM >= 150000
+ 	MarkGUCPrefixReserved("pg_stat_logmsg");
+ 
+ 	/*
+ 	 * Install hooks.
+ 	 */
+ 	prev_shmem_request_hook = shmem_request_hook;
+ 	shmem_request_hook = pglm_shmem_request;
+ #else
+ 	EmitWarningsOnPlaceholders("pg_stat_logmsg");
+ 
+ 	/*
+ 	 * Request additional shared resources.  (These are no-ops if we're not in
+ 	 * the postmaster process.)  We'll allocate or attach to the shared
+ 	 * resources in pgss_shmem_startup().
+ 	 */
+ 	RequestAddinShmemSpace(pglm_memsize());
+ 	RequestNamedLWLockTranche("pg_stat_logmsg", 1);
+ 
+ 	/*
+ 	 * Install hooks.
+ 	 */
+ #endif /* PG_VERSION_NUM >= 150000 */
+ 
+ 	prev_shmem_startup_hook = shmem_startup_hook;
+ 	shmem_startup_hook = pglm_shmem_startup;
+ 
+ 	prev_emit_log_hook = emit_log_hook;
+ 	emit_log_hook = pglm_log_hook;
+ 
+ 	/* Arrange to restore state in case of ERROR */
+ 	RegisterXactCallback(logmsg_xact_callback, NULL);
+ }
+ 
+ #if PG_VERSION_NUM >= 150000
+ /*
+  * shmem_request hook: request additional shared resources.  We'll allocate or
+  * attach to the shared resources in pglm_shmem_startup().
+  */
+ static void
+ pglm_shmem_request(void)
+ {
+ 	if (prev_shmem_request_hook)
+ 		prev_shmem_request_hook();
+ 
+ 	RequestAddinShmemSpace(pglm_memsize());
+ 	RequestNamedLWLockTranche("pg_stat_logmsg", 1);
+ }
+ #endif /* PG_VERSION_NUM >= 150000 */
+ 
+ /*
+  * shmem_startup hook: allocate or attach to shared memory,
+  * then load any pre-existing statistics from file.
+  * Also create and load the query-texts file, which is expected to exist
+  * (even if empty) while the module is enabled.
+  */
+ static void
+ pglm_shmem_startup(void)
+ {
+ 	bool		found;
+ 	HASHCTL		info;
+ 	FILE	   *file = NULL;
+ 	FILE	   *lmfile = NULL;
+ 	uint32		header;
+ 	int32		num;
+ 	int32		pgver;
+ 	int32		i;
+ 	int			buffer_size;
+ 	char	   *buffer = NULL;
+ 
+ 	if (prev_shmem_startup_hook)
+ 		prev_shmem_startup_hook();
+ 
+ 	/* reset in case this is a restart within the postmaster */
+ 	pglm = NULL;
+ 	pglm_hash = NULL;
+ 
+ 	/*
+ 	 * Create or attach to the shared memory state, including hash table
+ 	 */
+ 	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+ 
+ 	pglm = ShmemInitStruct("pg_stat_logmsg",
+ 						   sizeof(pglmSharedState),
+ 						   &found);
+ 
+ 	if (!found)
+ 	{
+ 		/* First time through ... */
+ 		pglm->lock = &(GetNamedLWLockTranche("pg_stat_logmsg"))->lock;
+ 		pglm->cur_median_usage = ASSUMED_MEDIAN_INIT;
+ 		pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;
+ 		SpinLockInit(&pglm->mutex);
+ 		pglm->extent = 0;
+ 		pglm->n_writers = 0;
+ 		pglm->gc_count = 0;
+ 		pglm->stats.dealloc = 0;
+ 		pglm->stats.stats_reset = GetCurrentTimestamp();
+ 	}
+ 
+ 	info.keysize = sizeof(pglmHashKey);
+ 	info.entrysize = sizeof(pglmEntry);
+ 	pglm_hash = ShmemInitHash("pg_stat_logmsg hash",
+ 							  pglm_max, pglm_max,
+ 							  &info,
+ 							  HASH_ELEM | HASH_BLOBS);
+ 
+ 	LWLockRelease(AddinShmemInitLock);
+ 
+ 	/*
+ 	 * If we're in the postmaster (or a standalone backend...), set up a shmem
+ 	 * exit hook to dump the statistics to disk.
+ 	 */
+ 	if (!IsUnderPostmaster)
+ 		on_shmem_exit(pglm_shmem_shutdown, (Datum) 0);
+ 
+ 	/*
+ 	 * Done if some other process already completed our initialization.
+ 	 */
+ 	if (found)
+ 		return;
+ 
+ 	/*
+ 	 * Note: we don't bother with locks here, because there should be no other
+ 	 * processes running when this code is reached.
+ 	 */
+ 
+ 	/* Unlink logmsg text file possibly left over from crash */
+ 	unlink(PGLM_TEXT_FILE);
+ 
+ 	/* Allocate new logmsg text temp file */
+ 	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
+ 	if (lmfile == NULL)
+ 		goto write_error;
+ 
+ 	/*
+ 	 * Attempt to load old statistics from the dump file.
+ 	 */
+ 	file = AllocateFile(PGLM_DUMP_FILE, PG_BINARY_R);
+ 	if (file == NULL)
+ 	{
+ 		if (errno != ENOENT)
+ 			goto read_error;
+ 		/* No existing persisted stats file, so we're done */
+ 		FreeFile(lmfile);
+ 		return;
+ 	}
+ 
+ 	buffer_size = 2048;
+ 	buffer = (char *) palloc(buffer_size);
+ 
+ 	if (fread(&header, sizeof(uint32), 1, file) != 1 ||
+ 		fread(&pgver, sizeof(uint32), 1, file) != 1 ||
+ 		fread(&num, sizeof(int32), 1, file) != 1)
+ 		goto read_error;
+ 
+ 	if (header != PGLM_FILE_HEADER ||
+ 		pgver != PGLM_PG_MAJOR_VERSION)
+ 		goto data_error;
+ 
+ 	for (i = 0; i < num; i++)
+ 	{
+ 		pglmEntry	temp;
+ 		pglmEntry  *entry;
+ 		Size		logmsg_offset;
+ 
+ 		if (fread(&temp, sizeof(pglmEntry), 1, file) != 1)
+ 			goto read_error;
+ 
+ 		/* Encoding is the only field we can easily sanity-check */
+ 		if (!PG_VALID_BE_ENCODING(temp.encoding))
+ 			goto data_error;
+ 
+ 		/* Resize buffer as needed */
+ 		if (temp.logmsg_len >= buffer_size)
+ 		{
+ 			buffer_size = Max(buffer_size * 2, temp.logmsg_len + 1);
+ 			buffer = repalloc(buffer, buffer_size);
+ 		}
+ 
+ 		if (fread(buffer, 1, temp.logmsg_len + 1, file) != temp.logmsg_len + 1)
+ 			goto read_error;
+ 
+ 		/* Should have a trailing null, but let's make sure */
+ 		buffer[temp.logmsg_len] = '\0';
+ 
+ 		/* Store the logmsg text */
+ 		logmsg_offset = pglm->extent;
+ 		if (fwrite(buffer, 1, temp.logmsg_len + 1, lmfile) != temp.logmsg_len + 1)
+ 			goto write_error;
+ 		pglm->extent += temp.logmsg_len + 1;
+ 
+ 		/* make the hashtable entry (discards old entries if too many) */
+ 		entry = entry_alloc(&temp.key, logmsg_offset, temp.logmsg_len,
+ 							temp.encoding, temp.sqlerrcode, temp.funcname);
+ 
+ 		/* copy in the actual stats */
+ 		entry->counters = temp.counters;
+ 	}
+ 
+ 	/* Read global statistics for pg_stat_logmsg */
+ 	if (fread(&pglm->stats, sizeof(pglmGlobalStats), 1, file) != 1)
+ 		goto read_error;
+ 
+ 	pfree(buffer);
+ 	FreeFile(file);
+ 	FreeFile(lmfile);
+ 
+ 	/*
+ 	 * Remove the persisted stats file so it's not included in
+ 	 * backups/replication standbys, etc.  A new file will be written on next
+ 	 * shutdown.
+ 	 *
+ 	 * Note: it's okay if the PGLM_TEXT_FILE is included in a basebackup,
+ 	 * because we remove that file on startup; it acts inversely to
+ 	 * PGLM_DUMP_FILE, in that it is only supposed to be around when the
+ 	 * server is running, whereas PGLM_DUMP_FILE is only supposed to be around
+ 	 * when the server is not running.  Leaving the file creates no danger of
+ 	 * a newly restored database having a spurious record of execution costs,
+ 	 * which is what we're really concerned about here.
+ 	 */
+ 	unlink(PGLM_DUMP_FILE);
+ 
+ 	return;
+ 
+ read_error:
+ 	ereport(LOG,
+ 			(errcode_for_file_access(),
+ 			 errmsg("could not read file \"%s\": %m",
+ 					PGLM_DUMP_FILE)));
+ 	goto fail;
+ data_error:
+ 	ereport(LOG,
+ 			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ 			 errmsg("ignoring invalid data in file \"%s\"",
+ 					PGLM_DUMP_FILE)));
+ 	goto fail;
+ write_error:
+ 	ereport(LOG,
+ 			(errcode_for_file_access(),
+ 			 errmsg("could not write file \"%s\": %m",
+ 					PGLM_TEXT_FILE)));
+ fail:
+ 	if (buffer)
+ 		pfree(buffer);
+ 	if (file)
+ 		FreeFile(file);
+ 	if (lmfile)
+ 		FreeFile(lmfile);
+ 	/* If possible, throw away the bogus file; ignore any error */
+ 	unlink(PGLM_DUMP_FILE);
+ 
+ 	/*
+ 	 * Don't unlink PGLM_TEXT_FILE here; it should always be around while the
+ 	 * server is running with pg_stat_logmsg enabled
+ 	 */
+ }
+ 
+ /*
+  * shmem_shutdown hook: Dump statistics into file.
+  *
+  * Note: we don't bother with acquiring lock, because there should be no
+  * other processes running when this is called.
+  */
+ static void
+ pglm_shmem_shutdown(int code, Datum arg)
+ {
+ 	FILE	   *file;
+ 	char	   *mlbuffer = NULL;
+ 	Size		mlbuffer_size = 0;
+ 	HASH_SEQ_STATUS hash_seq;
+ 	int32		num_entries;
+ 	pglmEntry  *entry;
+ 
+ 	/* Don't try to dump during a crash. */
+ 	if (code)
+ 		return;
+ 
+ 	/* Safety check ... shouldn't get here unless shmem is set up. */
+ 	if (!pglm || !pglm_hash)
+ 		return;
+ 
+ 	file = AllocateFile(PGLM_DUMP_FILE ".tmp", PG_BINARY_W);
+ 	if (file == NULL)
+ 		goto error;
+ 
+ 	if (fwrite(&PGLM_FILE_HEADER, sizeof(uint32), 1, file) != 1)
+ 		goto error;
+ 	if (fwrite(&PGLM_PG_MAJOR_VERSION, sizeof(uint32), 1, file) != 1)
+ 		goto error;
+ 	num_entries = hash_get_num_entries(pglm_hash);
+ 	if (fwrite(&num_entries, sizeof(int32), 1, file) != 1)
+ 		goto error;
+ 
+ 	mlbuffer = logmsg_load_file(&mlbuffer_size);
+ 	if (mlbuffer == NULL)
+ 		goto error;
+ 
+ 	/*
+ 	 * When serializing to disk, we store logmsg texts immediately after their
+ 	 * entry data.  Any orphaned logmsg texts are thereby excluded.
+ 	 */
+ 	hash_seq_init(&hash_seq, pglm_hash);
+ 	while ((entry = hash_seq_search(&hash_seq)) != NULL)
+ 	{
+ 		int			len = entry->logmsg_len;
+ 		char	   *qstr = logmsg_fetch(entry->logmsg_offset, len,
+ 									   mlbuffer, mlbuffer_size);
+ 
+ 		if (qstr == NULL)
+ 			continue;			/* Ignore any entries with bogus texts */
+ 
+ 		if (fwrite(entry, sizeof(pglmEntry), 1, file) != 1 ||
+ 			fwrite(qstr, 1, len + 1, file) != len + 1)
+ 		{
+ 			/* note: we assume hash_seq_term won't change errno */
+ 			hash_seq_term(&hash_seq);
+ 			goto error;
+ 		}
+ 	}
+ 
+ 	/* Dump global statistics for pg_stat_logmsg */
+ 	if (fwrite(&pglm->stats, sizeof(pglmGlobalStats), 1, file) != 1)
+ 		goto error;
+ 
+ 	free(mlbuffer);
+ 	mlbuffer = NULL;
+ 
+ 	if (FreeFile(file))
+ 	{
+ 		file = NULL;
+ 		goto error;
+ 	}
+ 
+ 	/*
+ 	 * Rename file into place, so we atomically replace any old one.
+ 	 */
+ 	(void) durable_rename(PGLM_DUMP_FILE ".tmp", PGLM_DUMP_FILE, LOG);
+ 
+ 	/* Unlink logmsg-texts file; it's not needed while shutdown */
+ 	unlink(PGLM_TEXT_FILE);
+ 
+ 	return;
+ 
+ error:
+ 	ereport(LOG,
+ 			(errcode_for_file_access(),
+ 			 errmsg("could not write file \"%s\": %m",
+ 					PGLM_DUMP_FILE ".tmp")));
+ 	free(mlbuffer);
+ 	if (file)
+ 		FreeFile(file);
+ 	unlink(PGLM_DUMP_FILE ".tmp");
+ 	unlink(PGLM_TEXT_FILE);
+ }
+ 
+ /*
+  * log hook: store results if needed
+  */
+ static void
+ pglm_log_hook(ErrorData *edata)
+ {
+ 	if (!is_in_log_hook)
+ 	{
+ 		is_in_log_hook = true;
+ 		pglm_store(edata);
+ 		is_in_log_hook = false;
+ 	}
+ 
+ 	if (prev_emit_log_hook)
+ 		prev_emit_log_hook(edata);
+ }
+ 
+ /*
+  * Store statistics for a log message.
+  */
+ static void
+ pglm_store(ErrorData *edata)
+ {
+ 	pglmHashKey key;
+ 	pglmEntry  *entry;
+ 	int			encoding = GetDatabaseEncoding();
+ 	size_t		filename_len;
+ 
+ 	Assert(edata != NULL);
+ 
+ 	/* Safety check... */
+ 	if (!pglm || !pglm_hash)
+ 		return;
+ 
+ 	/* Set up key for hashtable search */
+ 
+ 	/* memset() is required when pglmHashKey is without padding only */
+ 	memset(&key, 0, sizeof(pglmHashKey));
+ 
+ 	/* make persistent copies of strings */
+ 	if (edata->filename)
+ 	{
+ 		filename_len = strlen(edata->filename);
+ 		memcpy(key.filename, edata->filename,
+ 			   filename_len < MAXFILENAMELEN ?
+ 							  filename_len : MAXFILENAMELEN );
+ 	}
+ 
+ 	key.lineno = edata->lineno;
+ 	key.elevel = edata->elevel;
+ 
+ 	/* Lookup the hash table entry with shared lock. */
+ 	LWLockAcquire(pglm->lock, LW_SHARED);
+ 
+ 	entry = (pglmEntry *) hash_search(pglm_hash, &key, HASH_FIND, NULL);
+ 
+ 	/* Create new entry, if not present */
+ 	if (!entry)
+ 	{
+ 		Size		logmsg_offset;
+ 		int			logmsg_len; 
+ 		int			gc_count;
+ 		bool		stored;
+ 		bool		do_gc;
+ 
+ 		/* Append new logmsg text to file with only shared lock held */
+ 		stored = logmsg_store(edata, &logmsg_offset, &logmsg_len, &gc_count);
+ 
+ 		/*
+ 		 * Determine whether we need to garbage collect external logmsg texts
+ 		 * while the shared lock is still held.  This micro-optimization
+ 		 * avoids taking the time to decide this while holding exclusive lock.
+ 		 */
+ 		do_gc = need_gc_logmsgs();
+ 
+ 		/* Need exclusive lock to make a new hashtable entry - promote */
+ 		LWLockRelease(pglm->lock);
+ 		LWLockAcquire(pglm->lock, LW_EXCLUSIVE);
+ 
+ 		/*
+ 		 * A garbage collection may have occurred while we weren't holding the
+ 		 * lock.  In the unlikely event that this happens, the logmsg text we
+ 		 * stored above will have been garbage collected, so write it again.
+ 		 * This should be infrequent enough that doing it while holding
+ 		 * exclusive lock isn't a performance problem.
+ 		 */
+ 		if (!stored || pglm->gc_count != gc_count)
+ 			stored = logmsg_store(edata, &logmsg_offset, &logmsg_len, NULL);
+ 
+ 		/* If we failed to write to the text file, give up */
+ 		if (!stored)
+ 			goto done;
+ 
+ 		/* OK to create a new hashtable entry */
+ 		entry = entry_alloc(&key, logmsg_offset, logmsg_len,
+ 							encoding, edata->sqlerrcode,
+ 							(char *) edata->funcname);
+ 
+ 		/* If needed, perform garbage collection while exclusive lock held */
+ 		if (do_gc)
+ 			gc_logmsgs();
+ 	}
+ 
+ 	/*
+ 	 * Grab the spinlock while updating the counters (see comment about
+ 	 * locking rules at the head of the file)
+ 	 */
+ 	{
+ 		volatile pglmEntry *e = (volatile pglmEntry *) entry;
+ 
+ 		SpinLockAcquire(&e->mutex);
+ 		e->counters.msgcnt += 1;
+ 		SpinLockRelease(&e->mutex);
+ 	}
+ 
+ done:
+ 	LWLockRelease(pglm->lock);
+ }
+ 
+ /*
+  * Reset statement statistics.
+  */
+ Datum
+ pg_stat_logmsg_reset(PG_FUNCTION_ARGS)
+ {
+ 	entry_reset(NULL, 0, 0);
+ 
+ 	PG_RETURN_VOID();
+ }
+ 
+ /* Number of output arguments (columns) */
+ #define PG_STAT_LOGMSG_COLS			9
+ 
+ Datum
+ pg_stat_logmsg(PG_FUNCTION_ARGS)
+ {
+ 	pg_stat_logmsg_internal(fcinfo);
+ 
+ 	return (Datum) 0;
+ }
+ 
+ /* Common code for all versions of pg_stat_statements() */
+ static void
+ pg_stat_logmsg_internal(FunctionCallInfo fcinfo)
+ {
+ 	ReturnSetInfo  *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ 	Oid				userid = GetUserId();
+ 	char		   *mlbuffer = NULL;
+ 	Size			mlbuffer_size = 0;
+ 	Size			extent = 0;
+ 	int				gc_count = 0;
+ 	HASH_SEQ_STATUS hash_seq;
+ 	pglmEntry	   *entry;
+ 	int				n_writers;
+ 
+ 	/*
+ 	 * Superusers or roles with the privileges of pg_read_all_stats members
+ 	 * are allowed
+ 	 */
+ #if PG_VERSION_NUM >= 140000
+ #define PGLM_STATSROLE		ROLE_PG_READ_ALL_STATS
+ #else
+ #define PGLM_STATSROLE		DEFAULT_ROLE_READ_ALL_STATS
+ #endif /* PG_VERSION_NUM >= 140000 */
+ 	if (!has_privs_of_role(userid, PGLM_STATSROLE))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ 				 errmsg("must be member of %s role",
+ 				 GetUserNameFromId(PGLM_STATSROLE, false))));
+ 
+ 	/* hash table must exist already */
+ 	if (!pglm || !pglm_hash)
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ 				 errmsg("pg_stat_logmsg must be loaded via shared_preload_libraries")));
+ 
+ 	InitMaterializedSRF(fcinfo, 0);
+ 
+ 	/* Check we have the expected number of output arguments. */
+ 	if (rsinfo->setDesc->natts != PG_STAT_LOGMSG_COLS)
+ 		elog(ERROR, "incorrect number of output arguments");
+ 
+ 	/*
+ 	 * We'd like to load the logmsg text file (if needed) while not holding any
+ 	 * lock on pglm->lock.  In the worst case we'll have to do this again
+ 	 * after we have the lock, but it's unlikely enough to make this a win
+ 	 * despite occasional duplicated work.  We need to reload if anybody
+ 	 * writes to the file (either a retail logmsg_store(), or a garbage
+ 	 * collection) between this point and where we've gotten shared lock.  If
+ 	 * a logmsg_store is actually in progress when we look, we might as well
+ 	 * skip the speculative load entirely.
+ 	 */
+ 
+ 	/* Take the mutex so we can examine variables */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		extent = s->extent;
+ 		n_writers = s->n_writers;
+ 		gc_count = s->gc_count;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ 
+ 	/* No point in loading file now if there are active writers */
+ 	if (n_writers == 0)
+ 		mlbuffer = logmsg_load_file(&mlbuffer_size);
+ 
+ 	/*
+ 	 * Get shared lock, load or reload the logmsg text file if we must, and
+ 	 * iterate over the hashtable entries.
+ 	 *
+ 	 * With a large hash table, we might be holding the lock rather longer
+ 	 * than one could wish.  However, this only blocks creation of new hash
+ 	 * table entries, and the larger the hash table the less likely that is to
+ 	 * be needed.  So we can hope this is okay.  Perhaps someday we'll decide
+ 	 * we need to partition the hash table to limit the time spent holding any
+ 	 * one lock.
+ 	 */
+ 	LWLockAcquire(pglm->lock, LW_SHARED);
+ 
+ 	/*
+ 	 * Here it is safe to examine extent and gc_count without taking the
+ 	 * mutex.  Note that although other processes might change
+ 	 * pglm->extent just after we look at it, the strings they then write
+ 	 * into the file cannot yet be referenced in the hashtable, so we
+ 	 * don't care whether we see them or not.
+ 	 *
+ 	 * If logmsg_load_file fails, we just press on; we'll return NULL for
+ 	 * every logmsg text.
+ 	 */
+ 	if (mlbuffer == NULL ||
+ 		pglm->extent != extent ||
+ 		pglm->gc_count != gc_count)
+ 	{
+ 		free(mlbuffer);
+ 		mlbuffer = logmsg_load_file(&mlbuffer_size);
+ 	}
+ 
+ 	hash_seq_init(&hash_seq, pglm_hash);
+ 	while ((entry = hash_seq_search(&hash_seq)) != NULL)
+ 	{
+ 		Datum		values[PG_STAT_LOGMSG_COLS];
+ 		bool		nulls[PG_STAT_LOGMSG_COLS];
+ 		int			i = 0;
+ 		Counters	tmp;
+ 		char	   *mstr;
+ 
+ 		memset(values, 0, sizeof(values));
+ 		memset(nulls, 0, sizeof(nulls));
+ 
+ 		values[i++] = CStringGetTextDatum(PG_VERSION);
+ 		values[i++] = CStringGetTextDatum(entry->key.filename);
+ 		values[i++] = Int32GetDatum(entry->key.lineno);
+ 		values[i++] = CStringGetTextDatum(error_severity(entry->key.elevel));
+ 
+ 		if (entry->funcname[0])
+ 			values[i++] = CStringGetTextDatum(entry->funcname);
+ 		else
+ 			nulls[i++] = true;
+ 
+ 		values[i++] = CStringGetTextDatum(unpack_sql_state(entry->sqlerrcode));
+ 
+ 		mstr = logmsg_fetch(entry->logmsg_offset,
+ 							entry->logmsg_len,
+ 							mlbuffer,
+ 							mlbuffer_size);
+ 
+ 		if (mstr)
+ 		{
+ 			char	   *enc;
+ 
+ 			enc = pg_any_to_server(mstr,
+ 								   entry->logmsg_len,
+ 								   entry->encoding);
+ 			/* first the original format string */
+ 			values[i++] = CStringGetTextDatum(enc);
+ 			/* then the localized string */
+ 			values[i++] = CStringGetTextDatum(_(enc));
+ 
+ 			if (enc != mstr)
+ 				pfree(enc);
+ 		}
+ 		else
+ 		{
+ 			/* Just return a null if we fail to find the text */
+ 			nulls[i++] = true;
+ 			nulls[i++] = true;
+ 		}
+ 
+ 		/* copy counters to a local variable to keep locking time short */
+ 		{
+ 			volatile pglmEntry *e = (volatile pglmEntry *) entry;
+ 
+ 			SpinLockAcquire(&e->mutex);
+ 			tmp = e->counters;
+ 			SpinLockRelease(&e->mutex);
+ 		}
+ 
+ 		values[i++] = Int64GetDatumFast(tmp.msgcnt);
+ 
+ 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ 	}
+ 
+ 	LWLockRelease(pglm->lock);
+ 
+ 	free(mlbuffer);
+ }
+ 
+ /* Number of output arguments (columns) for pg_stat_logmsg_info */
+ #define PG_STAT_LOGMSG_INFO_COLS	2
+ 
+ /*
+  * Return statistics of pg_stat_logmsg.
+  */
+ Datum
+ pg_stat_logmsg_info(PG_FUNCTION_ARGS)
+ {
+ 	pglmGlobalStats stats;
+ 	TupleDesc	tupdesc;
+ 	Datum		values[PG_STAT_LOGMSG_INFO_COLS] = {0};
+ 	bool		nulls[PG_STAT_LOGMSG_INFO_COLS] = {0};
+ 
+ 	if (!pglm || !pglm_hash)
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ 				 errmsg("pg_stat_logmsg must be loaded via shared_preload_libraries")));
+ 
+ 	/* Build a tuple descriptor for our result type */
+ 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ 		elog(ERROR, "return type must be a row type");
+ 
+ 	/* Read global statistics for pg_stat_logmsg */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		stats = s->stats;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ 
+ 	values[0] = Int64GetDatum(stats.dealloc);
+ 	values[1] = TimestampTzGetDatum(stats.stats_reset);
+ 
+ 	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+ }
+ 
+ /*
+  * Estimate shared memory space needed.
+  */
+ static Size
+ pglm_memsize(void)
+ {
+ 	Size		size;
+ 
+ 	size = MAXALIGN(sizeof(pglmSharedState));
+ 	size = add_size(size, hash_estimate_size(pglm_max, sizeof(pglmEntry)));
+ 
+ 	return size;
+ }
+ 
+ /*
+  * Allocate a new hashtable entry.
+  * caller must hold an exclusive lock on pglm->lock
+  *
+  * Note: despite needing exclusive lock, it's not an error for the target
+  * entry to already exist.  This is because pglm_store releases and
+  * reacquires lock after failing to find a match; so someone else could
+  * have made the entry while we waited to get exclusive lock.
+  */
+ static pglmEntry *
+ entry_alloc(pglmHashKey *key,
+ 			Size logmsg_offset,
+ 			int logmsg_len,
+ 			int encoding,
+ 			int sqlerrcode,
+ 			char *efuncname)
+ {
+ 	pglmEntry  *entry;
+ 	bool		found;
+ 
+ 	/* Make space if needed */
+ 	while (hash_get_num_entries(pglm_hash) >= pglm_max)
+ 		entry_dealloc();
+ 
+ 	/* Find or create an entry with desired hash code */
+ 	entry = (pglmEntry *) hash_search(pglm_hash, key, HASH_ENTER, &found);
+ 
+ 	if (!found)
+ 	{
+ 		/* New entry, initialize it */
+ 
+ 		/* reset the statistics */
+ 		memset(&entry->counters, 0, sizeof(Counters));
+ 		/* set the appropriate initial usage count */
+ 		entry->counters.usage = USAGE_INIT;
+ 		/* re-initialize the mutex each time ... we assume no one using it */
+ 		SpinLockInit(&entry->mutex);
+ 		/* ... and don't forget the logmsg text metadata */
+ 		Assert(logmsg_len >= 0);
+ 		entry->logmsg_offset = logmsg_offset;
+ 		entry->logmsg_len = logmsg_len;
+ 		entry->encoding = encoding;
+ 		entry->sqlerrcode = sqlerrcode;
+ 
+ 		if (efuncname)
+ 		{
+ 			size_t		funcname_len = strlen(efuncname);
+ 
+ 			memcpy(entry->funcname, efuncname,
+ 				   funcname_len < MAXFUNCNAMELEN ?
+ 								  funcname_len : MAXFUNCNAMELEN );
+ 		}
+ 	}
+ 
+ 	return entry;
+ }
+ 
+ /*
+  * qsort comparator for sorting into increasing usage order
+  */
+ static int
+ entry_cmp(const void *lhs, const void *rhs)
+ {
+ 	double		l_usage = (*(pglmEntry *const *) lhs)->counters.usage;
+ 	double		r_usage = (*(pglmEntry *const *) rhs)->counters.usage;
+ 
+ 	if (l_usage < r_usage)
+ 		return -1;
+ 	else if (l_usage > r_usage)
+ 		return +1;
+ 	else
+ 		return 0;
+ }
+ 
+ /*
+  * Deallocate least-used entries.
+  *
+  * Caller must hold an exclusive lock on pglm->lock.
+  */
+ static void
+ entry_dealloc(void)
+ {
+ 	HASH_SEQ_STATUS hash_seq;
+ 	pglmEntry **entries;
+ 	pglmEntry  *entry;
+ 	int			nvictims;
+ 	int			i;
+ 	Size		tottextlen;
+ 	int			nvalidtexts;
+ 
+ 	/*
+ 	 * Sort entries by usage and deallocate USAGE_DEALLOC_PERCENT of them.
+ 	 * While we're scanning the table, apply the decay factor to the usage
+ 	 * values, and update the mean query length.
+ 	 *
+ 	 * Note that the mean query length is almost immediately obsolete, since
+ 	 * we compute it before not after discarding the least-used entries.
+ 	 * Hopefully, that doesn't affect the mean too much; it doesn't seem worth
+ 	 * making two passes to get a more current result.  Likewise, the new
+ 	 * cur_median_usage includes the entries we're about to zap.
+ 	 */
+ 
+ 	entries = palloc(hash_get_num_entries(pglm_hash) * sizeof(pglmEntry *));
+ 
+ 	i = 0;
+ 	tottextlen = 0;
+ 	nvalidtexts = 0;
+ 
+ 	hash_seq_init(&hash_seq, pglm_hash);
+ 	while ((entry = hash_seq_search(&hash_seq)) != NULL)
+ 	{
+ 		entries[i++] = entry;
+ 		entry->counters.usage *= USAGE_DECREASE_FACTOR;
+ 		/* In the mean length computation, ignore dropped texts. */
+ 		if (entry->logmsg_len >= 0)
+ 		{
+ 			tottextlen += entry->logmsg_len + 1;
+ 			nvalidtexts++;
+ 		}
+ 	}
+ 
+ 	/* Sort into increasing order by usage */
+ 	qsort(entries, i, sizeof(pglmEntry *), entry_cmp);
+ 
+ 	/* Record the (approximate) median usage */
+ 	if (i > 0)
+ 		pglm->cur_median_usage = entries[i / 2]->counters.usage;
+ 	/* Record the mean query length */
+ 	if (nvalidtexts > 0)
+ 		pglm->mean_logmsg_len = tottextlen / nvalidtexts;
+ 	else
+ 		pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;
+ 
+ 	/* Now zap an appropriate fraction of lowest-usage entries */
+ 	nvictims = Max(10, i * USAGE_DEALLOC_PERCENT / 100);
+ 	nvictims = Min(nvictims, i);
+ 
+ 	for (i = 0; i < nvictims; i++)
+ 		hash_search(pglm_hash, &entries[i]->key, HASH_REMOVE, NULL);
+ 
+ 	pfree(entries);
+ 
+ 	/* Increment the number of times entries are deallocated */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		s->stats.dealloc += 1;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ }
+ 
+ /*
+  * Given a logmsg string (not necessarily null-terminated), allocate a new
+  * entry in the external logmsg text file and store the string there.
+  *
+  * If successful, returns true, and stores the new entry's offset in the file
+  * into *logmsg_offset.  Also, if gc_count isn't NULL, *gc_count is set to the
+  * number of garbage collections that have occurred so far.
+  *
+  * On failure, returns false.
+  *
+  * At least a shared lock on pglm->lock must be held by the caller, so as
+  * to prevent a concurrent garbage collection.  Share-lock-holding callers
+  * should pass a gc_count pointer to obtain the number of garbage collections,
+  * so that they can recheck the count after obtaining exclusive lock to
+  * detect whether a garbage collection occurred (and removed this entry).
+  */
+ static bool
+ logmsg_store(ErrorData *edata, Size *logmsg_offset,
+ 			 int *logmsg_len, int *gc_count)
+ {
+ 	Size		off;
+ 	int			fd;
+ 
+ 	if (edata->message_id)
+ 		*logmsg_len = strlen(edata->message_id);
+ 
+ 	/*
+ 	 * We use a spinlock to protect extent/n_writers/gc_count, so that
+ 	 * multiple processes may execute this function concurrently.
+ 	 */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		off = s->extent;
+ 		s->extent += *logmsg_len + 1;
+ 		s->n_writers++;
+ 		if (gc_count)
+ 			*gc_count = s->gc_count;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ 
+ 	*logmsg_offset = off;
+ 
+ 	/*
+ 	 * Don't allow the file to grow larger than what logmsg_load_file can
+ 	 * (theoretically) handle.  This has been seen to be reachable on 32-bit
+ 	 * platforms.
+ 	 */
+ 	if (unlikely(*logmsg_len >= MaxAllocHugeSize - off))
+ 	{
+ 		errno = EFBIG;			/* not quite right, but it'll do */
+ 		fd = -1;
+ 		goto error;
+ 	}
+ 
+ 	/* Now write the data into the successfully-reserved part of the file */
+ 	fd = OpenTransientFile(PGLM_TEXT_FILE, O_RDWR | O_CREAT | PG_BINARY);
+ 	if (fd < 0)
+ 		goto error;
+ 
+ #if PG_VERSION_NUM >= 120000
+ 	if (pg_pwrite(fd, edata->message_id, *logmsg_len, off) != *logmsg_len)
+ 		goto error;
+ 	if (pg_pwrite(fd, "\0", 1, off + *logmsg_len) != 1)
+ 		goto error;
+ #else
+ 	if (lseek(fd, off, SEEK_SET) != off)
+ 		goto error;
+ 
+ 	if (write(fd, edata->message_id, *logmsg_len) != *logmsg_len)
+ 		goto error;
+ 	if (write(fd, "\0", 1) != 1)
+ 		goto error;
+ #endif /* PG_VERSION_NUM >= 120000 */
+ 
+ 	CloseTransientFile(fd);
+ 
+ 	/* Mark our write complete */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		s->n_writers--;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ 
+ 	return true;
+ 
+ error:
+ 	ereport(LOG,
+ 			(errcode_for_file_access(),
+ 			 errmsg("could not write file \"%s\": %m",
+ 					PGLM_TEXT_FILE)));
+ 
+ 	if (fd >= 0)
+ 		CloseTransientFile(fd);
+ 
+ 	/* Mark our write complete */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		s->n_writers--;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ 
+ 	return false;
+ }
+ 
+ /*
+  * Read the external logmsg text file into a malloc'd buffer.
+  *
+  * Returns NULL (without throwing an error) if unable to read, eg
+  * file not there or insufficient memory.
+  *
+  * On success, the buffer size is also returned into *buffer_size.
+  *
+  * This can be called without any lock on pglm->lock, but in that case
+  * the caller is responsible for verifying that the result is sane.
+  */
+ static char *
+ logmsg_load_file(Size *buffer_size)
+ {
+ 	char	   *buf;
+ 	int			fd;
+ 	struct stat stat;
+ 	Size		nread;
+ 
+ 	fd = OpenTransientFile(PGLM_TEXT_FILE, O_RDONLY | PG_BINARY);
+ 	if (fd < 0)
+ 	{
+ 		if (errno != ENOENT)
+ 			ereport(LOG,
+ 					(errcode_for_file_access(),
+ 					 errmsg("could not read file \"%s\": %m",
+ 							PGLM_TEXT_FILE)));
+ 		return NULL;
+ 	}
+ 
+ 	/* Get file length */
+ 	if (fstat(fd, &stat))
+ 	{
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not stat file \"%s\": %m",
+ 						PGLM_TEXT_FILE)));
+ 		CloseTransientFile(fd);
+ 		return NULL;
+ 	}
+ 
+ 	/* Allocate buffer; beware that off_t might be wider than size_t */
+ 	if (stat.st_size <= MaxAllocHugeSize)
+ 		buf = (char *) malloc(stat.st_size);
+ 	else
+ 		buf = NULL;
+ 
+ 	if (buf == NULL)
+ 	{
+ 		ereport(LOG,
+ 				(errcode(ERRCODE_OUT_OF_MEMORY),
+ 				 errmsg("out of memory"),
+ 				 errdetail("Could not allocate enough memory to read file \"%s\".",
+ 						   PGLM_TEXT_FILE)));
+ 		CloseTransientFile(fd);
+ 		return NULL;
+ 	}
+ 
+ 	/*
+ 	 * OK, slurp in the file.  Windows fails if we try to read more than
+ 	 * INT_MAX bytes at once, and other platforms might not like that either,
+ 	 * so read a very large file in 1GB segments.
+ 	 */
+ 	nread = 0;
+ 	while (nread < stat.st_size)
+ 	{
+ 		int			toread = Min(1024 * 1024 * 1024, stat.st_size - nread);
+ 
+ 		/*
+ 		 * If we get a short read and errno doesn't get set, the reason is
+ 		 * probably that garbage collection truncated the file since we did
+ 		 * the fstat(), so we don't log a complaint --- but we don't return
+ 		 * the data, either, since it's most likely corrupt due to concurrent
+ 		 * writes from garbage collection.
+ 		 */
+ 		errno = 0;
+ 		if (read(fd, buf + nread, toread) != toread)
+ 		{
+ 			if (errno)
+ 				ereport(LOG,
+ 						(errcode_for_file_access(),
+ 						 errmsg("could not read file \"%s\": %m",
+ 								PGLM_TEXT_FILE)));
+ 			free(buf);
+ 			CloseTransientFile(fd);
+ 			return NULL;
+ 		}
+ 		nread += toread;
+ 	}
+ 
+ 	if (CloseTransientFile(fd) != 0)
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not close file \"%s\": %m", PGLM_TEXT_FILE)));
+ 
+ 	*buffer_size = nread;
+ 	return buf;
+ }
+ 
+ /*
+  * Locate a logmsg text in the file image previously read by logmsg_load_file().
+  *
+  * We validate the given offset/length, and return NULL if bogus.  Otherwise,
+  * the result points to a null-terminated string within the buffer.
+  */
+ static char *
+ logmsg_fetch(Size logmsg_offset, int logmsg_len,
+ 			char *buffer, Size buffer_size)
+ {
+ 	/* File read failed? */
+ 	if (buffer == NULL)
+ 		return NULL;
+ 	/* Bogus offset/length? */
+ 	if (logmsg_len < 0 ||
+ 		logmsg_offset + logmsg_len >= buffer_size)
+ 		return NULL;
+ 	/* As a further sanity check, make sure there's a trailing null */
+ 	if (buffer[logmsg_offset + logmsg_len] != '\0')
+ 		return NULL;
+ 	/* Looks OK */
+ 	return buffer + logmsg_offset;
+ }
+ 
+ /*
+  * Do we need to garbage-collect the external logmsg text file?
+  *
+  * Caller should hold at least a shared lock on pglm->lock.
+  */
+ static bool
+ need_gc_logmsgs(void)
+ {
+ 	Size		extent;
+ 
+ 	/* Read shared extent pointer */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		extent = s->extent;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ 
+ 	/*
+ 	 * Don't proceed if file does not exceed 512 bytes per possible entry.
+ 	 *
+ 	 * Here and in the next test, 32-bit machines have overflow hazards if
+ 	 * pglm_max and/or mean_logmsg_len are large.  Force the multiplications
+ 	 * and comparisons to be done in uint64 arithmetic to forestall trouble.
+ 	 */
+ 	if ((uint64) extent < (uint64) 512 * pglm_max)
+ 		return false;
+ 
+ 	/*
+ 	 * Don't proceed if file is less than about 50% bloat.  Nothing can or
+ 	 * should be done in the event of unusually large logmsg texts accounting
+ 	 * for file's large size.  We go to the trouble of maintaining the mean
+ 	 * query length in order to prevent garbage collection from thrashing
+ 	 * uselessly.
+ 	 */
+ 	if ((uint64) extent < (uint64) pglm->mean_logmsg_len * pglm_max * 2)
+ 		return false;
+ 
+ 	return true;
+ }
+ 
+ /*
+  * Garbage-collect orphaned logmsg texts in external file.
+  *
+  * This won't be called often in the typical case, since it's likely that
+  * there won't be too much churn, and besides, a similar compaction process
+  * occurs when serializing to disk at shutdown or as part of resetting.
+  * Despite this, it seems prudent to plan for the edge case where the file
+  * becomes unreasonably large, with no other method of compaction likely to
+  * occur in the foreseeable future.
+  *
+  * The caller must hold an exclusive lock on pglm->lock.
+  *
+  * At the first sign of trouble we unlink the logmsg text file to get a clean
+  * slate (although existing statistics are retained), rather than risk
+  * thrashing by allowing the same problem case to recur indefinitely.
+  */
+ static void
+ gc_logmsgs(void)
+ {
+ 	char	   *mlbuffer;
+ 	Size		mlbuffer_size;
+ 	FILE	   *lmfile = NULL;
+ 	HASH_SEQ_STATUS hash_seq;
+ 	pglmEntry  *entry;
+ 	Size		extent;
+ 	int			nentries;
+ 
+ 	/*
+ 	 * When called from pglm_store, some other session might have proceeded
+ 	 * with garbage collection in the no-lock-held interim of lock strength
+ 	 * escalation.  Check once more that this is actually necessary.
+ 	 */
+ 	if (!need_gc_logmsgs())
+ 		return;
+ 
+ 	/*
+ 	 * Load the old texts file.  If we fail (out of memory, for instance),
+ 	 * invalidate logmsg texts.  Hopefully this is rare.  It might seem better
+ 	 * to leave things alone on an OOM failure, but the problem is that the
+ 	 * file is only going to get bigger; hoping for a future non-OOM result is
+ 	 * risky and can easily lead to complete denial of service.
+ 	 */
+ 	mlbuffer = logmsg_load_file(&mlbuffer_size);
+ 	if (mlbuffer == NULL)
+ 		goto gc_fail;
+ 
+ 	/*
+ 	 * We overwrite the logmsg texts file in place, so as to reduce the risk of
+ 	 * an out-of-disk-space failure.  Since the file is guaranteed not to get
+ 	 * larger, this should always work on traditional filesystems; though we
+ 	 * could still lose on copy-on-write filesystems.
+ 	 */
+ 	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
+ 	if (lmfile == NULL)
+ 	{
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not write file \"%s\": %m",
+ 						PGLM_TEXT_FILE)));
+ 		goto gc_fail;
+ 	}
+ 
+ 	extent = 0;
+ 	nentries = 0;
+ 
+ 	hash_seq_init(&hash_seq, pglm_hash);
+ 	while ((entry = hash_seq_search(&hash_seq)) != NULL)
+ 	{
+ 		int			logmsg_len = entry->logmsg_len;
+ 		char	   *msg = logmsg_fetch(entry->logmsg_offset,
+ 									   logmsg_len,
+ 									   mlbuffer,
+ 									   mlbuffer_size);
+ 
+ 		if (msg == NULL)
+ 		{
+ 			/* Trouble ... drop the text */
+ 			entry->logmsg_offset = 0;
+ 			entry->logmsg_len = -1;
+ 			/* entry will not be counted in mean logmsg length computation */
+ 			continue;
+ 		}
+ 
+ 		if (fwrite(msg, 1, logmsg_len + 1, lmfile) != logmsg_len + 1)
+ 		{
+ 			ereport(LOG,
+ 					(errcode_for_file_access(),
+ 					 errmsg("could not write file \"%s\": %m",
+ 							PGLM_TEXT_FILE)));
+ 			hash_seq_term(&hash_seq);
+ 			goto gc_fail;
+ 		}
+ 
+ 		entry->logmsg_offset = extent;
+ 		extent += logmsg_len + 1;
+ 		nentries++;
+ 	}
+ 
+ 	/*
+ 	 * Truncate away any now-unused space.  If this fails for some odd reason,
+ 	 * we log it, but there's no need to fail.
+ 	 */
+ 	if (ftruncate(fileno(lmfile), extent) != 0)
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not truncate file \"%s\": %m",
+ 						PGLM_TEXT_FILE)));
+ 
+ 	if (FreeFile(lmfile))
+ 	{
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not write file \"%s\": %m",
+ 						PGLM_TEXT_FILE)));
+ 		lmfile = NULL;
+ 		goto gc_fail;
+ 	}
+ 
+ 	elog(DEBUG1, "pglm gc of queries file shrunk size from %zu to %zu",
+ 		 pglm->extent, extent);
+ 
+ 	/* Reset the shared extent pointer */
+ 	pglm->extent = extent;
+ 
+ 	/*
+ 	 * Also update the mean logmsg length, to be sure that need_gc_logmsgs()
+ 	 * won't still think we have a problem.
+ 	 */
+ 	if (nentries > 0)
+ 		pglm->mean_logmsg_len = extent / nentries;
+ 	else
+ 		pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;
+ 
+ 	free(mlbuffer);
+ 
+ 	/*
+ 	 * OK, count a garbage collection cycle.  (Note: even though we have
+ 	 * exclusive lock on pglm->lock, we must take pglm->mutex for this, since
+ 	 * other processes may examine gc_count while holding only the mutex.
+ 	 * Also, we have to advance the count *after* we've rewritten the file,
+ 	 * else other processes might not realize they read a stale file.)
+ 	 */
+ 	record_gc_msgfmts();
+ 
+ 	return;
+ 
+ gc_fail:
+ 	/* clean up resources */
+ 	if (lmfile)
+ 		FreeFile(lmfile);
+ 	free(mlbuffer);
+ 
+ 	/*
+ 	 * Since the contents of the external file are now uncertain, mark all
+ 	 * hashtable entries as having invalid texts.
+ 	 */
+ 	hash_seq_init(&hash_seq, pglm_hash);
+ 	while ((entry = hash_seq_search(&hash_seq)) != NULL)
+ 	{
+ 		entry->logmsg_offset = 0;
+ 		entry->logmsg_len = -1;
+ 	}
+ 
+ 	/*
+ 	 * Destroy the logmsg text file and create a new, empty one
+ 	 */
+ 	(void) unlink(PGLM_TEXT_FILE);
+ 	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
+ 	if (lmfile == NULL)
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not recreate file \"%s\": %m",
+ 						PGLM_TEXT_FILE)));
+ 	else
+ 		FreeFile(lmfile);
+ 
+ 	/* Reset the shared extent pointer */
+ 	pglm->extent = 0;
+ 
+ 	/* Reset mean_logmsg_len to match the new state */
+ 	pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;
+ 
+ 	/*
+ 	 * Bump the GC count even though we failed.
+ 	 *
+ 	 * This is needed to make concurrent readers of file without any lock on
+ 	 * pglm->lock notice existence of new version of file.  Once readers
+ 	 * subsequently observe a change in GC count with pglm->lock held, that
+ 	 * forces a safe reopen of file.  Writers also require that we bump here,
+ 	 * of course.  (As required by locking protocol, readers and writers don't
+ 	 * trust earlier file contents until gc_count is found unchanged after
+ 	 * pglm->lock acquired in shared or exclusive mode respectively.)
+ 	 */
+ 	record_gc_msgfmts();
+ }
+ 
+ /*
+  * Release entries corresponding to parameters passed.
+  */
+ static void
+ entry_reset(char *filename, int lineno, int elevel)
+ {
+ 	HASH_SEQ_STATUS hash_seq;
+ 	pglmEntry  *entry;
+ 	FILE	   *lmfile;
+ 	long		num_entries;
+ 	long		num_remove = 0;
+ 	pglmHashKey key;
+ 
+ 	if (!pglm || !pglm_hash)
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ 				 errmsg("pg_stat_logmsg must be loaded via shared_preload_libraries")));
+ 
+ 	LWLockAcquire(pglm->lock, LW_EXCLUSIVE);
+ 	num_entries = hash_get_num_entries(pglm_hash);
+ 
+ 	if (filename != NULL && lineno != 0 && elevel != 0)
+ 	{
+ 		/* If all the parameters are available, use the fast path. */
+ 		memset(&key, 0, sizeof(pglmHashKey));
+ 
+ 		if (filename)
+ 		{
+ 			size_t		filename_len = strlen(filename);
+ 
+ 			memcpy(key.filename, filename,
+ 				   filename_len < MAXFILENAMELEN ?
+ 								  filename_len : MAXFILENAMELEN );
+ 		}
+ 		else
+ 			memset(key.filename, '\0', MAXFILENAMELEN);
+ 
+ 		key.lineno = lineno;
+ 		key.elevel = elevel;
+ 
+ 		/* Remove the key if exists */
+ 		entry = (pglmEntry *) hash_search(pglm_hash, &key, HASH_REMOVE, NULL);
+ 		if (entry)				/* found */
+ 			num_remove++;
+ 	}
+ 	else if (filename != NULL || lineno != 0 || elevel != 0)
+ 	{
+ 		/* Remove entries corresponding to valid parameters. */
+ 		hash_seq_init(&hash_seq, pglm_hash);
+ 		while ((entry = hash_seq_search(&hash_seq)) != NULL)
+ 		{
+ 			if ((!filename || entry->key.filename == filename) &&
+ 				(!lineno || entry->key.lineno == lineno) &&
+ 				(!elevel || entry->key.elevel == elevel))
+ 			{
+ 				hash_search(pglm_hash, &entry->key, HASH_REMOVE, NULL);
+ 				num_remove++;
+ 			}
+ 		}
+ 	}
+ 	else
+ 	{
+ 		/* Remove all entries. */
+ 		hash_seq_init(&hash_seq, pglm_hash);
+ 		while ((entry = hash_seq_search(&hash_seq)) != NULL)
+ 		{
+ 			hash_search(pglm_hash, &entry->key, HASH_REMOVE, NULL);
+ 			num_remove++;
+ 		}
+ 	}
+ 
+ 	/* All entries are removed? */
+ 	if (num_entries != num_remove)
+ 		goto release_lock;
+ 
+ 	/*
+ 	 * Reset global statistics for pg_stat_logmsg since all entries are
+ 	 * removed.
+ 	 */
+ 	{
+ 		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
+ 		TimestampTz stats_reset = GetCurrentTimestamp();
+ 
+ 		SpinLockAcquire(&s->mutex);
+ 		s->stats.dealloc = 0;
+ 		s->stats.stats_reset = stats_reset;
+ 		SpinLockRelease(&s->mutex);
+ 	}
+ 
+ 	/*
+ 	 * Write new empty logmsg file, perhaps even creating a new one to recover
+ 	 * if the file was missing.
+ 	 */
+ 	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
+ 	if (lmfile == NULL)
+ 	{
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not create file \"%s\": %m",
+ 						PGLM_TEXT_FILE)));
+ 		goto done;
+ 	}
+ 
+ 	/* If ftruncate fails, log it, but it's not a fatal problem */
+ 	if (ftruncate(fileno(lmfile), 0) != 0)
+ 		ereport(LOG,
+ 				(errcode_for_file_access(),
+ 				 errmsg("could not truncate file \"%s\": %m",
+ 						PGLM_TEXT_FILE)));
+ 
+ 	FreeFile(lmfile);
+ 
+ done:
+ 	pglm->extent = 0;
+ 	/* This counts as a logmsg text garbage collection for our purposes */
+ 	record_gc_msgfmts();
+ 
+ release_lock:
+ 	LWLockRelease(pglm->lock);
+ }
+ 
+ /*
+  * logmsg_xact_callback --- cleanup at main-transaction end.
+  */
+ static void
+ logmsg_xact_callback(XactEvent event, void *arg)
+ {
+ 	is_in_log_hook = false;
+ }
+ 
+ #if PG_VERSION_NUM < 150000
+ /* flag bits for InitMaterializedSRF() */
+ #define MAT_SRF_USE_EXPECTED_DESC	0x01	/* use expectedDesc as tupdesc. */
+ #define MAT_SRF_BLESS				0x02	/* "Bless" a tuple descriptor with
+ 											 * BlessTupleDesc(). */
+ 
+ /*
+  * InitMaterializedSRF
+  *
+  * Helper function to build the state of a set-returning function used
+  * in the context of a single call with materialize mode.  This code
+  * includes sanity checks on ReturnSetInfo, creates the Tuplestore and
+  * the TupleDesc used with the function and stores them into the
+  * function's ReturnSetInfo.
+  *
+  * "flags" can be set to MAT_SRF_USE_EXPECTED_DESC, to use the tuple
+  * descriptor coming from expectedDesc, which is the tuple descriptor
+  * expected by the caller.  MAT_SRF_BLESS can be set to complete the
+  * information associated to the tuple descriptor, which is necessary
+  * in some cases where the tuple descriptor comes from a transient
+  * RECORD datatype.
+  */
+ static void
+ InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags)
+ {
+ 	bool		random_access;
+ 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ 	Tuplestorestate *tupstore;
+ 	MemoryContext old_context,
+ 				per_query_ctx;
+ 	TupleDesc	stored_tupdesc;
+ 
+ 	/* check to see if caller supports returning a tuplestore */
+ 	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ 				 errmsg("set-valued function called in context that cannot accept a set")));
+ 	if (!(rsinfo->allowedModes & SFRM_Materialize) ||
+ 		((flags & MAT_SRF_USE_EXPECTED_DESC) != 0 && rsinfo->expectedDesc == NULL))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ 				 errmsg("materialize mode required, but it is not allowed in this context")));
+ 
+ 	/*
+ 	 * Store the tuplestore and the tuple descriptor in ReturnSetInfo.  This
+ 	 * must be done in the per-query memory context.
+ 	 */
+ 	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+ 	old_context = MemoryContextSwitchTo(per_query_ctx);
+ 
+ 	/* build a tuple descriptor for our result type */
+ 	if ((flags & MAT_SRF_USE_EXPECTED_DESC) != 0)
+ 		stored_tupdesc = CreateTupleDescCopy(rsinfo->expectedDesc);
+ 	else
+ 	{
+ 		if (get_call_result_type(fcinfo, NULL, &stored_tupdesc) != TYPEFUNC_COMPOSITE)
+ 			elog(ERROR, "return type must be a row type");
+ 	}
+ 
+ 	/* If requested, bless the tuple descriptor */
+ 	if ((flags & MAT_SRF_BLESS) != 0)
+ 		BlessTupleDesc(stored_tupdesc);
+ 
+ 	random_access = (rsinfo->allowedModes & SFRM_Materialize_Random) != 0;
+ 
+ 	tupstore = tuplestore_begin_heap(random_access, false, work_mem);
+ 	rsinfo->returnMode = SFRM_Materialize;
+ 	rsinfo->setResult = tupstore;
+ 	rsinfo->setDesc = stored_tupdesc;
+ 	MemoryContextSwitchTo(old_context);
+ }
+ 
+ /*
+  * error_severity --- get string representing elevel
+  *
+  * The string is not localized here, but we mark the strings for translation
+  * so that callers can invoke _() on the result.
+  */
+ static const char *
+ error_severity(int elevel)
+ {
+ 	const char *prefix;
+ 
+ 	switch (elevel)
+ 	{
+ 		case DEBUG1:
+ 		case DEBUG2:
+ 		case DEBUG3:
+ 		case DEBUG4:
+ 		case DEBUG5:
+ 			prefix = gettext_noop("DEBUG");
+ 			break;
+ 		case LOG:
+ 		case LOG_SERVER_ONLY:
+ 			prefix = gettext_noop("LOG");
+ 			break;
+ 		case INFO:
+ 			prefix = gettext_noop("INFO");
+ 			break;
+ 		case NOTICE:
+ 			prefix = gettext_noop("NOTICE");
+ 			break;
+ 		case WARNING:
+ #if PG_VERSION_NUM >= 140000
+ 		case WARNING_CLIENT_ONLY:
+ #endif /* PG_VERSION_NUM >= 140000 */
+ 			prefix = gettext_noop("WARNING");
+ 			break;
+ 		case ERROR:
+ 			prefix = gettext_noop("ERROR");
+ 			break;
+ 		case FATAL:
+ 			prefix = gettext_noop("FATAL");
+ 			break;
+ 		case PANIC:
+ 			prefix = gettext_noop("PANIC");
+ 			break;
+ 		default:
+ 			prefix = "???";
+ 			break;
+ 	}
+ 
+ 	return prefix;
+ }
+ #endif /* PG_VERSION_NUM < 150000 */
diff --git a/contrib/pg_stat_logmsg/pg_stat_logmsg.conf b/contrib/pg_stat_logmsg/pg_stat_logmsg.conf
index ...ab6336c .
*** a/contrib/pg_stat_logmsg/pg_stat_logmsg.conf
--- b/contrib/pg_stat_logmsg/pg_stat_logmsg.conf
***************
*** 0 ****
--- 1 ----
+ shared_preload_libraries = 'pg_stat_logmsg'
diff --git a/contrib/pg_stat_logmsg/pg_stat_logmsg.control b/contrib/pg_stat_logmsg/pg_stat_logmsg.control
index ...800b550 .
*** a/contrib/pg_stat_logmsg/pg_stat_logmsg.control
--- b/contrib/pg_stat_logmsg/pg_stat_logmsg.control
***************
*** 0 ****
--- 1,5 ----
+ # pg_stat_logmsg extension
+ comment = 'track statistics of log messages'
+ default_version = '1.0'
+ module_pathname = '$libdir/pg_stat_logmsg'
+ relocatable = true
diff --git a/doc/src/sgml/pgstatlogmsg.sgml b/doc/src/sgml/pgstatlogmsg.sgml
index ...3f52beb .
*** a/doc/src/sgml/pgstatlogmsg.sgml
--- b/doc/src/sgml/pgstatlogmsg.sgml
***************
*** 0 ****
--- 1,595 ----
+ <!-- doc/src/sgml/pgstatlogmsg.sgml -->
+ 
+ <sect1 id="pgstatlogmsg" xreflabel="pg_stat_logmsg">
+  <title>pg_stat_logmsg &mdash; track counts of normalized server log messages</title>
+ 
+  <indexterm zone="pgstatlogmsg">
+   <primary>pg_stat_logmsg</primary>
+  </indexterm>
+ 
+  <para>
+   The <filename>pg_stat_logmsg</filename> module provides a means for
+   tracking the aggregated counts of normalized server log message strings.
+  </para>
+ 
+  <para>
+   The module must be loaded by adding <literal>pg_stat_logmsg</literal> to
+   <xref linkend="guc-shared-preload-libraries"/> in
+   <filename>postgresql.conf</filename>, because it requires additional shared memory.
+   This means that a server restart is needed to add or remove the module.
+  </para>
+ 
+  <para>
+    When <filename>pg_stat_logmsg</filename> is active, it tracks
+    log message counts across all databases of the server. To access and manipulate
+    these statistics, the module provides views
+    <structname>pg_stat_logmsg</structname> and
+    <structname>pg_stat_logmsg_info</structname>,
+    and the utility functions <function>pg_stat_logmsg_reset</function> and
+    <function>pg_stat_logmsg</function>.  These are not available globally but
+    can be enabled for a specific database with
+    <command>CREATE EXTENSION pg_stat_logmsg</command>.
+  </para>
+ 
+  <sect2 id="pgstatlogmsg-pg-stat-logmsg">
+   <title>The <structname>pg_stat_logmsg</structname> View</title>
+ 
+   <para>
+    The counts gathered by the module are made available via a
+    view named <structname>pg_stat_logmsg</structname>.  This view
+    contains one row for each distinct combination of source code
+    filename, source code line number, and log severity level
+    (up to the maximum number of distinct messages that the module
+    can track). The columns of the view are shown in
+    <xref linkend="pgstatlogmsg-columns"/>.
+   </para>
+ 
+   <table id="pgstatlogmsg-columns">
+    <title><structname>pg_stat_logmsg</structname> Columns</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        Column Type
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+ 
+     <tbody>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>pgversion</structfield> <type>text</type>
+       </para>
+       <para>
+        <productname>PostgreSQL</productname> version number. Useful for
+        correlating source code filename and line number with the actual
+        entry source code.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>filename</structfield> <type>text</type>
+       </para>
+       <para>
+        Source code filename where the log message originates.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>lineno</structfield> <type>int</type>
+       </para>
+       <para>
+        Source code line number where the log message originates.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>elevel</structfield> <type>text</type>
+       </para>
+       <para>
+        Log message severity level as called at runtime.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>funcname</structfield> <type>text</type>
+       </para>
+       <para>
+        Source code function name where the log message originates.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>sqlerrcode</structfield> <type>text</type>
+       </para>
+       <para>
+        SQL error code of the log message. See <xref linkend="errcodes-appendix"/>.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>message</structfield> <type>text</type>
+       </para>
+       <para>
+        Normalized log message. Basically the log message text with the
+        replaceable format parameters rather than the actual runtime
+        data.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>translated_message</structfield> <type>text</type>
+       </para>
+       <para>
+        Same as <structfield>message</structfield>, except localized.
+       </para></entry>
+      </row>
+ 
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>count</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Number of times this log message has occurred.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+ 
+   <para>
+    For security reasons, only superusers and roles with privileges of the
+    <literal>pg_read_all_stats</literal> role are allowed to view this data.
+   </para>
+ 
+   <para>
+    Log messages are combined into a single
+    <structname>pg_stat_logmsg</structname> entry whenever they
+    originate from identical source code <structfield>filename</structfield>
+    and line number (<structfield>lineno</structfield>),
+    and they have the same runtime error severity level
+    (<structfield>elevel</structfield>).
+   </para>
+ 
+   <para>
+    As a rule of thumb, <structfield>filename</structfield>,
+    <structfield>lineno</structfield>, and <structfield>elevel</structfield>
+    values can be assumed to be stable and comparable only so long as
+    the underlying server version stays exactly the same.
+   </para>
+ 
+   <para>
+    The representative log messages are kept in an external disk file, and do
+    not consume shared memory. Therefore, even very lengthy log messages can
+    be stored successfully.  However, if many long log messages are
+    accumulated, the external file might grow unmanageably large.  As a
+    recovery method if that happens, <filename>pg_stat_logmsg</filename> may
+    choose to discard the log messages, whereupon all existing entries in
+    the <structname>pg_stat_logmsg</structname> view will show
+    null <structfield>message</structfield> fields, though the counts
+    associated with each row are preserved. If this happens, consider
+    reducing <varname>pg_stat_logmsg.max</varname> to prevent
+    recurrences.
+   </para>
+  </sect2>
+ 
+  <sect2 id="pgstatlogmsg-pg-stat-logmsg-info">
+   <title>The <structname>pg_stat_logmsg_info</structname> View</title>
+ 
+   <indexterm>
+    <primary>pg_stat_logmsg_info</primary>
+   </indexterm>
+ 
+   <para>
+    The statistics of the <filename>pg_stat_logmsg</filename> module
+    itself are tracked and made available via a view named
+    <structname>pg_stat_logmsg_info</structname>.  This view contains
+    only a single row.  The columns of the view are shown in
+    <xref linkend="pgstatlogmsginfo-columns"/>.
+   </para>
+ 
+   <table id="pgstatlogmsginfo-columns">
+    <title><structname>pg_stat_logmsg_info</structname> Columns</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        Column Type
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+ 
+     <tbody>
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>dealloc</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Total number of times <structname>pg_stat_logmsg</structname>
+        entries about the least-executed statements were deallocated
+        because more distinct statements than
+        <varname>pg_stat_logmsg.max</varname> were observed
+       </para></entry>
+      </row>
+      <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+        <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
+       </para>
+       <para>
+        Time at which all statistics in the
+        <structname>pg_stat_logmsg</structname> view were last reset.
+       </para></entry>
+      </row>
+ 
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+ 
+  <sect2 id="pgstatlogmsg-funcs">
+   <title>Functions</title>
+ 
+   <variablelist>
+    <varlistentry>
+     <term>
+      <function>pg_stat_logmsg_reset() returns void</function>
+      <indexterm>
+       <primary>pg_stat_logmsg_reset</primary>
+      </indexterm>
+     </term>
+ 
+     <listitem>
+      <para>
+       <function>pg_stat_logmsg_reset</function> discards all statistics
+       gathered so far by <filename>pg_stat_logmsg</filename>. It will
+       also reset the statistics in the
+       <structname>pg_stat_logmsg_info</structname> view.
+       By default, this function can only be executed by superusers.
+       Access may be granted to others using <command>GRANT</command>.
+      </para>
+     </listitem>
+    </varlistentry>
+ 
+    <varlistentry>
+     <term>
+      <function>pg_stat_logmsg() returns setof record</function>
+      <indexterm>
+       <primary>pg_stat_logmsg</primary>
+       <secondary>function</secondary>
+      </indexterm>
+     </term>
+ 
+     <listitem>
+      <para>
+       The <structname>pg_stat_logmsg</structname> view is defined in
+       terms of a function also named <function>pg_stat_logmsg</function>.
+       It is possible for clients to call
+       the <function>pg_stat_logmsg</function> function directly.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+  </sect2>
+ 
+  <sect2 id="pgstatlogmsg-config-params">
+   <title>Configuration Parameters</title>
+ 
+   <variablelist>
+    <varlistentry>
+     <term>
+      <varname>pg_stat_logmsg.max</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>pg_stat_logmsg.max</varname> configuration parameter</primary>
+      </indexterm>
+     </term>
+ 
+     <listitem>
+      <para>
+       <varname>pg_stat_logmsg.max</varname> is the maximum number of
+       log messages tracked by the module (i.e., the maximum number of rows
+       in the <structname>pg_stat_logmsg</structname> view). If more distinct
+       log messages than that are observed, information about the least-recorded
+       log message is discarded.  The number of times such information was
+       discarded can be seen in the
+       <structname>pg_stat_logmsg_info</structname> view.
+       The default value is 5000.
+       This parameter can only be set at server start.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ 
+   <para>
+    The module requires additional shared memory proportional to
+    <varname>pg_stat_logmsg.max</varname>.  Note that this
+    memory is consumed whenever the module is loaded.
+   </para>
+ 
+   <para>
+    These parameters must be set in <filename>postgresql.conf</filename>.
+    Typical usage might be:
+ 
+ <programlisting>
+ # postgresql.conf
+ shared_preload_libraries = 'pg_stat_logmsg'
+ 
+ pg_stat_logmsg.max = 10000
+ </programlisting>
+   </para>
+  </sect2>
+ 
+  <sect2 id="pgstatlogmsg-sample-output">
+   <title>Sample Output</title>
+ 
+ <screen>
+ $ make installcheck
+ $ psql logmsg
+ logmsg=# \x
+ logmsg=# -- most common messages
+ logmsg=# SELECT * FROM pg_stat_logmsg
+          ORDER BY count DESC LIMIT 5;
+ -[ RECORD 1 ]------+----------------------------------------
+ pgversion          | 17devel
+ filename           | aclchk.c
+ lineno             | 2807
+ elevel             | ERROR
+ funcname           | aclcheck_error
+ sqlerrcode         | 42501
+ message            | permission denied for schema %s
+ translated_message | permission denied for schema %s
+ count              | 282
+ -[ RECORD 2 ]------+----------------------------------------
+ pgversion          | 17devel
+ filename           | mcxt.c
+ lineno             | 913
+ elevel             | LOG
+ funcname           | MemoryContextStatsPrint
+ sqlerrcode         | 00000
+ message            | level: %d; %s: %s%s
+ translated_message | level: %d; %s: %s%s
+ count              | 255
+ -[ RECORD 3 ]------+----------------------------------------
+ pgversion          | 17devel
+ filename           | pl_exec.c
+ lineno             | 3909
+ elevel             | WARNING
+ funcname           | exec_stmt_raise
+ sqlerrcode         | 01000
+ message            | %s
+ translated_message | %s
+ count              | 241
+ -[ RECORD 4 ]------+----------------------------------------
+ pgversion          | 17devel
+ filename           | scan.l
+ lineno             | 1241
+ elevel             | ERROR
+ funcname           | scanner_yyerror
+ sqlerrcode         | 42601
+ message            | %s at or near "%s"
+ translated_message | %s at or near "%s"
+ count              | 133
+ -[ RECORD 5 ]------+----------------------------------------
+ pgversion          | 17devel
+ filename           | datetime.c
+ lineno             | 4002
+ elevel             | ERROR
+ funcname           | DateTimeParseError
+ sqlerrcode         | 22015
+ message            | interval field value out of range: "%s"
+ translated_message | interval field value out of range: "%s"
+ count              | 120
+ 
+ logmsg=# -- internal errors
+ logmsg=# SELECT * FROM pg_stat_logmsg
+          WHERE sqlerrcode = 'XX000'
+          ORDER BY count DESC LIMIT 5;
+ -[ RECORD 1 ]------+-------------------------------------------------------
+ pgversion          | 17devel
+ filename           | user.c
+ lineno             | 2130
+ elevel             | ERROR
+ funcname           | check_role_membership_authorization
+ sqlerrcode         | XX000
+ message            | role "%s" cannot have explicit members
+ translated_message | role "%s" cannot have explicit members
+ count              | 2
+ -[ RECORD 2 ]------+-------------------------------------------------------
+ pgversion          | 17devel
+ filename           | tablecmds.c
+ lineno             | 11216
+ elevel             | ERROR
+ funcname           | ATExecAlterConstraint
+ sqlerrcode         | XX000
+ message            | cannot alter constraint "%s" on relation "%s"
+ translated_message | cannot alter constraint "%s" on relation "%s"
+ count              | 2
+ -[ RECORD 3 ]------+-------------------------------------------------------
+ pgversion          | 17devel
+ filename           | tid.c
+ lineno             | 358
+ elevel             | ERROR
+ funcname           | currtid_for_view
+ sqlerrcode         | XX000
+ message            | currtid cannot handle views with no CTID
+ translated_message | currtid cannot handle views with no CTID
+ count              | 1
+ -[ RECORD 4 ]------+-------------------------------------------------------
+ pgversion          | 17devel
+ filename           | tid.c
+ lineno             | 315
+ elevel             | ERROR
+ funcname           | currtid_internal
+ sqlerrcode         | XX000
+ message            | cannot look at latest visible tid for relation "%s.%s"
+ translated_message | cannot look at latest visible tid for relation "%s.%s"
+ count              | 1
+ -[ RECORD 5 ]------+-------------------------------------------------------
+ pgversion          | 17devel
+ filename           | pg_locale.c
+ lineno             | 2904
+ elevel             | ERROR
+ funcname           | icu_validate_locale
+ sqlerrcode         | XX000
+ message            | ICU locale "%s" has unknown language "%s"
+ translated_message | ICU locale "%s" has unknown language "%s"
+ count              | 1
+ 
+ logmsg=# SELECT sqlerrcode, sum(count) AS total_count
+          FROM pg_stat_logmsg
+          GROUP BY sqlerrcode ORDER BY 1;
+  sqlerrcode | total_count 
+ ------------+-------------
+  00000      |         295
+  01000      |         318
+  01006      |           1
+  01007      |           5
+  08006      |           2
+  08P01      |           1
+  09000      |           5
+  0A000      |         400
+  0LP01      |           9
+  0Z002      |           2
+  21000      |           9
+  22000      |          11
+  22001      |          17
+  22003      |         184
+  22004      |          25
+  22005      |           1
+  22007      |          91
+  22008      |          95
+  22009      |           2
+  2200C      |           1
+  2200F      |           1
+  2200H      |           2
+  2200L      |           5
+  2200M      |           2
+  2200N      |          16
+  2200S      |           2
+  2200T      |           1
+  22011      |           6
+  22012      |          54
+  22013      |           6
+  22014      |           1
+  22015      |         120
+  22016      |           1
+  2201B      |           7
+  2201E      |          21
+  2201F      |          11
+  2201G      |          14
+  2201W      |           1
+  22021      |           1
+  22023      |         332
+  22025      |           3
+  22026      |           5
+  2202E      |          33
+  2202G      |           1
+  2202H      |           5
+  22030      |          14
+  22031      |           8
+  22033      |           7
+  22036      |          13
+  22038      |           9
+  22039      |           3
+  2203A      |           8
+  2203B      |           2
+  2203C      |           4
+  22P02      |         357
+  22P04      |          13
+  22P05      |           8
+  22P06      |          10
+  23502      |          69
+  23503      |         132
+  23505      |          70
+  23514      |         220
+  23P01      |          13
+  24000      |           9
+  25001      |          21
+  25006      |          16
+  25P01      |          33
+  25P02      |          14
+  27000      |           3
+  2BP01      |          86
+  2F003      |           1
+  2F005      |           1
+  34000      |           5
+  39P03      |           1
+  3B001      |           1
+  3D000      |           4
+  3F000      |          16
+  42501      |         525
+  42601      |         387
+  42602      |           8
+  42611      |           7
+  42701      |          18
+  42702      |           7
+  42703      |         140
+  42704      |         154
+  42710      |          67
+  42712      |          12
+  42723      |           4
+  42725      |          14
+  42803      |          38
+  42804      |         171
+  42809      |         135
+  42830      |           2
+  42846      |           4
+  42883      |         138
+  428C9      |          21
+  42P01      |         128
+  42P02      |           1
+  42P05      |           1
+  42P06      |           1
+  42P07      |          20
+  42P09      |           2
+  42P10      |          85
+  42P13      |          79
+  42P15      |          15
+  42P16      |         119
+  42P17      |          98
+  42P18      |           1
+  42P19      |          19
+  42P20      |          14
+  42P21      |          17
+  42P22      |           8
+  44000      |          31
+  54000      |           3
+  54001      |           5
+  54011      |           3
+  55000      |         143
+  55006      |           8
+  55P04      |           5
+  58P01      |           5
+  F0000      |           3
+  P0001      |          42
+  P0002      |           5
+  P0003      |          11
+  P0004      |           4
+  XX000      |          14
+ (126 rows)
+ 
+ </screen>
+  </sect2>
+ 
+  <sect2 id="pgstatlogmsg-authors">
+   <title>Authors</title>
+ 
+   <para>
+    Joe Conway <email>mail@joeconway.com</email>.
+   </para>
+  </sect2>
+ 
+ </sect1>
