From f7cc6280fb6f24892f5d60aaedf4a99cd38af427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADzio=20de=20Royes=20Mello?= Date: Mon, 28 Sep 2026 16:50:14 -0300 Subject: [PATCH v2] Add cumulative statistics about server log messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new builtin cumulative statistics kind that counts messages emitted to the server log, grouped by backend type, database, user, severity level, and SQLSTATE code. The counts are exposed through the new pg_stat_log_messages system view, giving monitoring systems a cheap, always-on, SQL-queryable signal for questions that previously required parsing the text server log: how many errors of which SQLSTATE is this instance producing, per database, per user? Error counters by SQLSTATE also make it possible to alert on events such as internal errors (XX000) or data corruption (XX001, XX002) without granting monitoring access to the log files, which mix diagnostics with statements and potentially sensitive data. Messages are counted in EmitErrorReport() when they are written to the server log, so log_min_messages acts as a floor on what can be tracked. Counting is disabled by default: it is opt-in, enabled by setting the new track_log_messages GUC to the minimum message severity level to count (for example, warning). The capacity is sized proportionally to the number of SQLSTATE error codes known to the server: up to 64 distinct combinations per named errcode (about 16,800 entries in total, or roughly 0.6 MB) are tracked in an index-based separate-chaining hash table laid out entirely inside the fixed-size stats block: chain links are array indices rather than pointers, so the block stays valid across the raw-memcpy snapshots and verbatim file persistence used by fixed-amount statistics. Lookups, inserts, and drops are O(1) expected even when the table is full. Once full, already-tracked combinations keep counting; messages for new combinations are counted in a separate counter exposed by the pg_stat_get_log_messages_dropped() function. Counters follow the usual cumulative-stats lifecycle: they persist across clean restarts, are discarded after crash recovery, and can be reset with pg_stat_reset_shared('log_messages'). The SQLSTATE condition names shown in the view's sqlerrcode_name column come from a lookup table generated from errcodes.txt by the new generate-errcodes-names.pl script, which also emits the errcode count used to size the entry table. Access to the view and its underlying functions requires the pg_read_all_stats role. This supersedes the earlier pg_stat_log contrib module proposal, following the feedback that new functionality of this kind belongs in core rather than contrib. Includes documentation, regression tests, and TAP coverage of the persistence behavior around restarts. Signed-off-by: Fabrízio de Royes Mello --- doc/src/sgml/config.sgml | 32 +++ doc/src/sgml/monitoring.sgml | 170 ++++++++++++ src/backend/catalog/system_views.sql | 25 ++ src/backend/utils/Makefile | 11 +- src/backend/utils/activity/Makefile | 1 + src/backend/utils/activity/meson.build | 1 + src/backend/utils/activity/pgstat.c | 17 ++ src/backend/utils/activity/pgstat_logmsg.c | 259 ++++++++++++++++++ src/backend/utils/adt/pgstatfuncs.c | 68 ++++- src/backend/utils/error/elog.c | 5 + src/backend/utils/generate-errcodes-names.pl | 93 +++++++ src/backend/utils/misc/guc_parameters.dat | 7 + src/backend/utils/misc/guc_tables.c | 18 ++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_proc.dat | 14 + src/include/pgstat.h | 63 ++++- src/include/utils/meson.build | 11 + src/include/utils/pgstat_internal.h | 19 ++ src/include/utils/pgstat_kind.h | 3 +- src/test/recovery/t/029_stats_restart.pl | 44 +++ src/test/regress/expected/rules.out | 13 + src/test/regress/expected/stats.out | 95 ++++++- src/test/regress/sql/stats.sql | 49 ++++ src/tools/pgindent/typedefs.list | 4 + 25 files changed, 1016 insertions(+), 10 deletions(-) create mode 100644 src/backend/utils/activity/pgstat_logmsg.c create mode 100644 src/backend/utils/generate-errcodes-names.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index f36fbb60101..3e6f3edee81 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -9265,6 +9265,38 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; + + track_log_messages (enum) + + track_log_messages configuration parameter + + + + + Enables counting of messages emitted to the server log, grouped by + backend type, database, user, severity level, and SQLSTATE code. + The value is the minimum message severity level to count, using the + same severity levels as ; + for example, warning counts all messages of + severity WARNING and above. The default is + none, which disables counting entirely. The + collected counts are shown in the + + pg_stat_log_messages view. + Only superusers and users with the appropriate SET + privilege can change this setting. + + + + + Only messages that are actually written to the server log are + counted, so acts as a floor + on what this setting can track. + + + + + stats_fetch_consistency (enum) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 0d038de1a23..9cb8a66ae59 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -538,6 +538,17 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser + + pg_stat_log_messagespg_stat_log_messages + + One row for each combination of backend type, database, user, + severity level, and SQLSTATE code, showing counts of messages + emitted to the server log. + See + pg_stat_log_messages for details. + + + pg_stat_replication_slotspg_stat_replication_slots One row per replication slot, showing statistics about the @@ -3689,6 +3700,159 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + <structname>pg_stat_log_messages</structname> + + + pg_stat_log_messages + + + + The pg_stat_log_messages view has one row for + each distinct combination of backend type, database, user, severity + level, and SQLSTATE code, showing how many messages with that signature + have been emitted to the server log. Message counting is disabled by + default and can be enabled by setting the + parameter. + + + + Only messages that are actually written to the server log are counted; + therefore acts as a floor on what + can be tracked. The database_name and + user_name columns are NULL + for messages emitted before a process has bound to a database or role, + for example authentication failures or messages from auxiliary + processes. + + + + The number of distinct combinations tracked is capped at 64 times the + number of SQLSTATE error codes known to the server (about 16,800 in + total); once that limit has been reached, messages for already-tracked + combinations continue to be counted while messages for new combinations + are silently dropped. The number of messages dropped that way is + reported by the + pg_stat_get_log_messages_dropped() function. + Resetting these statistics with + pg_stat_reset_shared('log_messages') reclaims all + entries. + + + + <structname>pg_stat_log_messages</structname> View + + + + + Column Type + + + Description + + + + + + + + backend_type text + + + Type of backend that emitted the message (e.g. + client backend, autovacuum worker) + + + + + + database_oid oid + + + OID of the database the emitting process was connected to, or NULL + + + + + + database_name name + + + Name of the database the emitting process was connected to, or NULL + + + + + + user_oid oid + + + OID of the user the emitting process was running as, or NULL + + + + + + user_name name + + + Name of the user the emitting process was running as, or NULL + + + + + + elevel text + + + Severity level of the message (e.g. WARNING, + ERROR, FATAL) + + + + + + sqlerrcode text + + + SQLSTATE error code of the message (e.g. 40P01); + see + + + + + + sqlerrcode_name text + + + Condition name of the SQLSTATE error code (e.g. + deadlock_detected), or NULL if the code has no + standard name + + + + + + count bigint + + + Number of messages emitted with this combination + + + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + + + +
+
+ <structname>pg_stat_bgwriter</structname> @@ -6013,6 +6177,12 @@ description | Waiting for a newly initialized WAL file to reach durable storage pg_stat_lock view. + + + log_messages: Reset all the counters shown in the + pg_stat_log_messages view. + + recovery_prefetch: Reset all the counters shown in diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index ad340887f54..30b30eacfe1 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1293,6 +1293,31 @@ CREATE VIEW pg_stat_kind_info AS k.entry_count FROM pg_stat_get_kind_info() k; +CREATE VIEW pg_stat_log_messages AS +SELECT + s.backend_type, + s.database_oid, + d.datname AS database_name, + s.user_oid, + u.rolname AS user_name, + s.elevel, + s.sqlerrcode, + s.sqlerrcode_name, + s.count, + s.stats_reset +FROM pg_stat_get_log_messages() s + LEFT JOIN pg_database d ON d.oid = s.database_oid + LEFT JOIN pg_authid u ON u.oid = s.user_oid; + +REVOKE ALL ON pg_stat_log_messages FROM PUBLIC; +GRANT SELECT ON pg_stat_log_messages TO pg_read_all_stats; + +REVOKE EXECUTE ON FUNCTION pg_stat_get_log_messages() FROM PUBLIC; +GRANT EXECUTE ON FUNCTION pg_stat_get_log_messages() TO pg_read_all_stats; + +REVOKE EXECUTE ON FUNCTION pg_stat_get_log_messages_dropped() FROM PUBLIC; +GRANT EXECUTE ON FUNCTION pg_stat_get_log_messages_dropped() TO pg_read_all_stats; + CREATE VIEW pg_stat_wal AS SELECT w.wal_records, diff --git a/src/backend/utils/Makefile b/src/backend/utils/Makefile index 81b4a956bda..a32adde274d 100644 --- a/src/backend/utils/Makefile +++ b/src/backend/utils/Makefile @@ -43,7 +43,7 @@ generated-header-symlinks: $(top_builddir)/src/include/utils/header-stamp submak submake-adt-headers: $(MAKE) -C adt jsonpath_gram.h -$(SUBDIRS:%=%-recursive): fmgr-stamp errcodes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h +$(SUBDIRS:%=%-recursive): fmgr-stamp errcodes.h errcodes_names.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h # fmgr-stamp records the last time we ran Gen_fmgrtab.pl. We don't rely on # the timestamps of the individual output files, because the Perl script @@ -55,6 +55,9 @@ fmgr-stamp: Gen_fmgrtab.pl $(catalogdir)/Catalog.pm $(top_srcdir)/src/include/ca errcodes.h: $(top_srcdir)/src/backend/utils/errcodes.txt generate-errcodes.pl $(PERL) $(srcdir)/generate-errcodes.pl --outfile $@ $< +errcodes_names.h: $(top_srcdir)/src/backend/utils/errcodes.txt generate-errcodes-names.pl + $(PERL) $(srcdir)/generate-errcodes-names.pl --outfile $@ $< + guc_tables.inc.c: $(top_srcdir)/src/backend/utils/misc/guc_parameters.dat $(top_srcdir)/src/backend/utils/misc/gen_guc_tables.pl $(PERL) $(top_srcdir)/src/backend/utils/misc/gen_guc_tables.pl $< $@ @@ -79,8 +82,8 @@ endif # These generated headers must be symlinked into src/include/. # We use header-stamp to record that we've done this because the symlinks # themselves may appear older than fmgr-stamp. -$(top_builddir)/src/include/utils/header-stamp: fmgr-stamp errcodes.h probes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h - cd '$(dir $@)' && for file in fmgroids.h fmgrprotos.h errcodes.h probes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h; do \ +$(top_builddir)/src/include/utils/header-stamp: fmgr-stamp errcodes.h errcodes_names.h probes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h + cd '$(dir $@)' && for file in fmgroids.h fmgrprotos.h errcodes.h errcodes_names.h probes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h; do \ rm -f $$file && $(LN_S) "../../../$(subdir)/$$file" . ; \ done touch $@ @@ -98,5 +101,5 @@ uninstall-data: clean: rm -f probes.h probes.h.tmp - rm -f fmgroids.h fmgrprotos.h fmgrtab.c fmgr-stamp errcodes.h guc_tables.inc.c + rm -f fmgroids.h fmgrprotos.h fmgrtab.c fmgr-stamp errcodes.h errcodes_names.h guc_tables.inc.c rm -f wait_event_types.h pgstat_wait_event.c wait_event_funcs_data.c diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile index 2e32d1485d6..551e537ea9f 100644 --- a/src/backend/utils/activity/Makefile +++ b/src/backend/utils/activity/Makefile @@ -29,6 +29,7 @@ OBJS = \ pgstat_io.o \ pgstat_kind.o \ pgstat_lock.o \ + pgstat_logmsg.o \ pgstat_relation.o \ pgstat_replslot.o \ pgstat_shmem.o \ diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build index e6dcb2e26fc..bc4dec334b4 100644 --- a/src/backend/utils/activity/meson.build +++ b/src/backend/utils/activity/meson.build @@ -14,6 +14,7 @@ backend_sources += files( 'pgstat_io.c', 'pgstat_kind.c', 'pgstat_lock.c', + 'pgstat_logmsg.c', 'pgstat_relation.c', 'pgstat_replslot.c', 'pgstat_shmem.c', diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index 6dd13ab9dec..5af0baff33e 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -84,6 +84,7 @@ * - pgstat_function.c * - pgstat_io.c * - pgstat_lock.c + * - pgstat_logmsg.c * - pgstat_relation.c * - pgstat_replslot.c * - pgstat_slru.c @@ -516,6 +517,22 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .reset_all_cb = pgstat_wal_reset_all_cb, .snapshot_cb = pgstat_wal_snapshot_cb, }, + + [PGSTAT_KIND_LOGMSG] = { + .name = "log_messages", + + .fixed_amount = true, + .write_to_file = true, + + .snapshot_ctl_off = offsetof(PgStat_Snapshot, logmsg), + .shared_ctl_off = offsetof(PgStat_ShmemControl, logmsg), + .shared_data_off = offsetof(PgStatShared_LogMsg, stats), + .shared_data_len = sizeof(((PgStatShared_LogMsg *) 0)->stats), + + .init_shmem_cb = pgstat_logmsg_init_shmem_cb, + .reset_all_cb = pgstat_logmsg_reset_all_cb, + .snapshot_cb = pgstat_logmsg_snapshot_cb, + }, }; /* diff --git a/src/backend/utils/activity/pgstat_logmsg.c b/src/backend/utils/activity/pgstat_logmsg.c new file mode 100644 index 00000000000..95f48105693 --- /dev/null +++ b/src/backend/utils/activity/pgstat_logmsg.c @@ -0,0 +1,259 @@ +/* ------------------------------------------------------------------------- + * + * pgstat_logmsg.c + * Implementation of log message statistics. + * + * This file contains the implementation of log message statistics. It is + * kept separate from pgstat.c to enforce the line between the statistics + * access / storage implementation and the details about individual types of + * statistics. + * + * Counts of messages emitted to the server log are kept grouped by + * (backend type, database, user, severity level, SQLSTATE), capped at + * PGSTAT_LOGMSG_MAX_ENTRIES distinct combinations. Once the table is full, + * already-tracked combinations keep counting while messages for new + * combinations are accounted for in n_dropped. + * + * The entries are kept in an index-based separate-chaining hash table laid + * out entirely inside PgStat_LogMsgStats (see pgstat.h). Because chain + * links are array indices rather than pointers, the struct stays valid when + * the fixed-amount stats machinery copies it around: snapshots are taken + * with a raw memcpy under the changecount protocol and the stats file is + * written/restored verbatim. This is also why none of the standard hash + * table implementations (dynahash, simplehash, dshash) are used here: they + * all rely on process-local pointers or external allocations. + * + * Copyright (c) 2001-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/utils/activity/pgstat_logmsg.c + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +/* + * Expose the errcodes_names[] lookup table when errcodes_names.h is pulled + * in via pgstat.h below; other files only see ERRCODES_NAMES_COUNT. + */ +#define ERRCODES_NAMES_INCLUDE_TABLE + +#include "common/hashfn.h" +#include "miscadmin.h" +#include "storage/proc.h" +#include "utils/pgstat_internal.h" +#include "utils/timestamp.h" + +/* the table and the count come from the same generated header */ +StaticAssertDecl(lengthof(errcodes_names) == ERRCODES_NAMES_COUNT + 1, + "errcodes_names table does not match ERRCODES_NAMES_COUNT"); + +/* Minimum message severity level to track; see also guc_parameters.dat */ +int pgstat_track_logmsg = PGSTAT_LOGMSG_TRACK_NONE; + +/* + * Prevent recursion should updating the stats itself cause a message to be + * logged. + */ +static bool pgstat_logmsg_counting = false; + + +/* + * Hash a log message signature into a uint32, used to pick a chain bucket. + */ +static inline uint32 +pgstat_logmsg_hash(BackendType backend_type, Oid dboid, Oid userid, + int elevel, int sqlerrcode) +{ + uint32 h; + + h = murmurhash32((uint32) backend_type); + h = hash_combine(h, murmurhash32((uint32) dboid)); + h = hash_combine(h, murmurhash32((uint32) userid)); + h = hash_combine(h, murmurhash32((uint32) elevel)); + h = hash_combine(h, murmurhash32((uint32) sqlerrcode)); + return h; +} + +/* + * Count one message emitted to the server log. + * + * Called by EmitErrorReport() for messages with output_to_server set. This + * runs in any process type, possibly very early or very late in its + * lifetime, so be careful about what infrastructure is relied upon here. + * + * The chain walk is O(1) expected (average chain length equals the load + * factor) for lookup, insert, and drop alike, even when the table is full. + */ +void +pgstat_count_logmsg(ErrorData *edata) +{ + PgStatShared_LogMsg *stats_shmem; + PgStat_LogMsgStats *stats; + Oid dboid; + Oid userid; + int sec_context; + uint32 hash; + uint32 bucket; + int32 idx; + bool found; + + if (pgstat_track_logmsg == PGSTAT_LOGMSG_TRACK_NONE || + edata->elevel < pgstat_track_logmsg) + return; + + /* stats shared memory might not be set up yet, or already torn down */ + if (pgStatLocal.shmem == NULL || pgStatLocal.shmem->is_shutdown) + return; + + /* cannot take LWLocks without a PGPROC (e.g. in the postmaster) */ + if (!MyProc) + return; + + if (pgstat_logmsg_counting) + return; + pgstat_logmsg_counting = true; + + stats_shmem = &pgStatLocal.shmem->logmsg; + stats = &stats_shmem->stats; + + dboid = MyDatabaseId; + + /* + * Use GetUserIdAndSecContext() rather than GetUserId(): the latter + * asserts that a user ID has been set, which is not the case for messages + * emitted before authentication completes or in auxiliary processes. + * Here InvalidOid is fine and simply means "no user". + */ + GetUserIdAndSecContext(&userid, &sec_context); + + hash = pgstat_logmsg_hash(MyBackendType, dboid, userid, + edata->elevel, edata->sqlerrcode); + bucket = hash % PGSTAT_LOGMSG_MAX_ENTRIES; + + LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); + + found = false; + for (idx = stats->heads[bucket]; idx != -1; idx = stats->entries[idx].next) + { + PgStat_LogMsgEntry *entry = &stats->entries[idx]; + + if (entry->backend_type == MyBackendType && + entry->dboid == dboid && + entry->userid == userid && + entry->elevel == edata->elevel && + entry->sqlerrcode == edata->sqlerrcode) + { + pgstat_begin_changecount_write(&stats_shmem->changecount); + entry->count++; + pgstat_end_changecount_write(&stats_shmem->changecount); + found = true; + break; + } + } + + if (!found) + { + if (stats->num_entries < PGSTAT_LOGMSG_MAX_ENTRIES) + { + int32 newidx = stats->num_entries; + PgStat_LogMsgEntry *entry = &stats->entries[newidx]; + + pgstat_begin_changecount_write(&stats_shmem->changecount); + entry->backend_type = MyBackendType; + entry->dboid = dboid; + entry->userid = userid; + entry->elevel = edata->elevel; + entry->sqlerrcode = edata->sqlerrcode; + entry->count = 1; + entry->next = stats->heads[bucket]; + stats->heads[bucket] = newidx; + stats->num_entries++; + pgstat_end_changecount_write(&stats_shmem->changecount); + } + else + { + pgstat_begin_changecount_write(&stats_shmem->changecount); + stats->n_dropped++; + pgstat_end_changecount_write(&stats_shmem->changecount); + } + } + + LWLockRelease(&stats_shmem->lock); + + pgstat_logmsg_counting = false; +} + +/* + * Return the condition name for a SQLSTATE error code as listed in + * errcodes.txt, or NULL if the code has no name (e.g. custom codes raised + * from user code). + */ +const char * +pgstat_get_logmsg_errcode_name(int sqlerrcode) +{ + for (int i = 0; errcodes_names[i].name != NULL; i++) + { + if (errcodes_names[i].sqlerrcode == sqlerrcode) + return errcodes_names[i].name; + } + return NULL; +} + +/* + * Support function for the SQL-callable pgstat* functions. Returns a + * pointer to the log message statistics struct. + */ +PgStat_LogMsgStats * +pgstat_fetch_stat_logmsg(void) +{ + pgstat_snapshot_fixed(PGSTAT_KIND_LOGMSG); + + return &pgStatLocal.snapshot.logmsg; +} + +void +pgstat_logmsg_init_shmem_cb(void *stats) +{ + PgStatShared_LogMsg *stats_shmem = (PgStatShared_LogMsg *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); + + /* mark all bucket chains as empty */ + memset(stats_shmem->stats.heads, 0xFF, + sizeof(stats_shmem->stats.heads)); +} + +void +pgstat_logmsg_reset_all_cb(TimestampTz ts) +{ + PgStatShared_LogMsg *stats_shmem = &pgStatLocal.shmem->logmsg; + PgStat_LogMsgStats *stats = &stats_shmem->stats; + + LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); + + /* + * Empty all bucket chains and reset num_entries so that entries are + * reclaimed for reuse; otherwise, once the table fills up, a reset would + * not free capacity for new distinct combinations. + */ + pgstat_begin_changecount_write(&stats_shmem->changecount); + stats->num_entries = 0; + stats->n_dropped = 0; + memset(stats->heads, 0xFF, sizeof(stats->heads)); + stats->stat_reset_timestamp = ts; + pgstat_end_changecount_write(&stats_shmem->changecount); + + LWLockRelease(&stats_shmem->lock); +} + +void +pgstat_logmsg_snapshot_cb(void) +{ + PgStatShared_LogMsg *stats_shmem = &pgStatLocal.shmem->logmsg; + + pgstat_copy_changecounted_stats(&pgStatLocal.snapshot.logmsg, + &stats_shmem->stats, + sizeof(stats_shmem->stats), + &stats_shmem->changecount); +} diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 64b6f60516c..113b0435eea 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -1664,6 +1664,69 @@ pg_stat_get_io(PG_FUNCTION_ARGS) return (Datum) 0; } +/* + * Returns cumulative log message statistics. + */ +Datum +pg_stat_get_log_messages(PG_FUNCTION_ARGS) +{ +#define PG_STAT_GET_LOG_MESSAGES_COLS 8 + ReturnSetInfo *rsinfo; + PgStat_LogMsgStats *stats; + + InitMaterializedSRF(fcinfo, 0); + rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + stats = pgstat_fetch_stat_logmsg(); + + for (int i = 0; i < stats->num_entries; i++) + { + PgStat_LogMsgEntry *entry = &stats->entries[i]; + Datum values[PG_STAT_GET_LOG_MESSAGES_COLS] = {0}; + bool nulls[PG_STAT_GET_LOG_MESSAGES_COLS] = {0}; + const char *errcode_name; + + values[0] = CStringGetTextDatum(GetBackendTypeDesc(entry->backend_type)); + + if (OidIsValid(entry->dboid)) + values[1] = ObjectIdGetDatum(entry->dboid); + else + nulls[1] = true; + + if (OidIsValid(entry->userid)) + values[2] = ObjectIdGetDatum(entry->userid); + else + nulls[2] = true; + + values[3] = CStringGetTextDatum(error_severity(entry->elevel)); + values[4] = CStringGetTextDatum(unpack_sql_state(entry->sqlerrcode)); + + errcode_name = pgstat_get_logmsg_errcode_name(entry->sqlerrcode); + if (errcode_name) + values[5] = CStringGetTextDatum(errcode_name); + else + nulls[5] = true; + + values[6] = Int64GetDatum(entry->count); + values[7] = TimestampTzGetDatum(stats->stat_reset_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + + return (Datum) 0; +} + +/* + * Returns the number of log messages not counted in the log message + * statistics because the entry table was full. + */ +Datum +pg_stat_get_log_messages_dropped(PG_FUNCTION_ARGS) +{ + PG_RETURN_INT64(pgstat_fetch_stat_logmsg()->n_dropped); +} + /* * Returns I/O statistics for a backend with given PID. */ @@ -2096,6 +2159,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER); pgstat_reset_of_kind(PGSTAT_KIND_IO); pgstat_reset_of_kind(PGSTAT_KIND_LOCK); + pgstat_reset_of_kind(PGSTAT_KIND_LOGMSG); XLogPrefetchResetStats(); pgstat_reset_of_kind(PGSTAT_KIND_SLRU); pgstat_reset_of_kind(PGSTAT_KIND_WAL); @@ -2115,6 +2179,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) pgstat_reset_of_kind(PGSTAT_KIND_IO); else if (strcmp(target, "lock") == 0) pgstat_reset_of_kind(PGSTAT_KIND_LOCK); + else if (strcmp(target, "log_messages") == 0) + pgstat_reset_of_kind(PGSTAT_KIND_LOGMSG); else if (strcmp(target, "recovery_prefetch") == 0) XLogPrefetchResetStats(); else if (strcmp(target, "slru") == 0) @@ -2125,7 +2191,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized reset target: \"%s\"", target), - errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"lock\", \"recovery_prefetch\", \"slru\", or \"wal\"."))); + errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"lock\", \"log_messages\", \"recovery_prefetch\", \"slru\", or \"wal\"."))); PG_RETURN_VOID(); } diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index b9d2c96b97a..5f42fb63358 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -1921,7 +1921,12 @@ EmitErrorReport(void) /* Send to server log, if enabled */ if (edata->output_to_server) + { + /* count the message in the cumulative log message statistics */ + pgstat_count_logmsg(edata); + send_message_to_server_log(edata); + } /* Send to client, if enabled */ if (edata->output_to_client) diff --git a/src/backend/utils/generate-errcodes-names.pl b/src/backend/utils/generate-errcodes-names.pl new file mode 100644 index 00000000000..b3fe3ddef3a --- /dev/null +++ b/src/backend/utils/generate-errcodes-names.pl @@ -0,0 +1,93 @@ +#!/usr/bin/perl +# +# Generate the errcodes_names.h header from errcodes.txt +# +# Produces ERRCODES_NAMES_COUNT, the number of SQLSTATE error codes with a +# condition name in errcodes.txt, and a lookup table mapping those codes to +# their human-readable condition names (e.g. ERRCODE_DIVISION_BY_ZERO -> +# "division_by_zero"). Codes without a condition name are omitted. +# +# The count is always visible, but the table (a static array) is only +# exposed to files that define ERRCODES_NAMES_INCLUDE_TABLE before this +# header is first included, so that widely included headers can use the +# count without dragging a copy of the table into every translation unit. +# +# Copyright (c) 2000-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use Getopt::Long; + +my $outfile = ''; + +GetOptions('outfile=s' => \$outfile) or die "$0: wrong arguments"; + +open my $errcodes, '<', $ARGV[0] + or die "$0: could not open input file '$ARGV[0]': $!\n"; + +# Collect the named errcodes +my @names; + +while (<$errcodes>) +{ + chomp; + + # Skip comments, empty lines and section headers + next if /^#/; + next if /^\s*$/; + next if /^Section:/; + + die "unable to parse errcodes.txt" + unless /^([^\s]{5})\s+[EWS]\s+([^\s]+)(?:\s+([^\s]+))?/; + + (my $sqlstate, my $errcode_macro, my $condition_name) = ($1, $2, $3); + + # Skip lines without a condition name (duplicate codes) + next unless defined $condition_name && $condition_name ne ''; + + push @names, [ $errcode_macro, $condition_name ]; +} + +close $errcodes; + +my $outfh; +if ($outfile) +{ + open $outfh, '>', $outfile + or die "$0: could not open output file '$outfile': $!\n"; +} +else +{ + $outfh = *STDOUT; +} + +print $outfh + "/* autogenerated from src/backend/utils/errcodes.txt, do not edit */\n"; +print $outfh "#ifndef ERRCODES_NAMES_H\n"; +print $outfh "#define ERRCODES_NAMES_H\n"; +print $outfh "\n"; +printf $outfh "#define ERRCODES_NAMES_COUNT %d\n", scalar(@names); +print $outfh "\n"; +print $outfh "#ifdef ERRCODES_NAMES_INCLUDE_TABLE\n"; +print $outfh "\n"; +print $outfh "typedef struct ErrCodeName\n"; +print $outfh "{\n"; +print $outfh "\tint\t\t\tsqlerrcode;\n"; +print $outfh "\tconst char *name;\n"; +print $outfh "} ErrCodeName;\n"; +print $outfh "\n"; +print $outfh "static const ErrCodeName errcodes_names[] = {\n"; + +foreach my $entry (@names) +{ + printf $outfh "\t{%s, \"%s\"},\n", $entry->[0], $entry->[1]; +} + +print $outfh "\t{0, NULL}\n"; +print $outfh "};\n"; +print $outfh "\n"; +print $outfh "#endif\t\t\t\t\t\t\t/* ERRCODES_NAMES_INCLUDE_TABLE */\n"; +print $outfh "\n"; +print $outfh "#endif\t\t\t\t\t\t\t/* ERRCODES_NAMES_H */\n"; + +close $outfh if ($outfile); diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index c57441f7d98..459f34900c1 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -3242,6 +3242,13 @@ boot_val => 'false', }, +{ name => 'track_log_messages', type => 'enum', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Sets the minimum message severity level to count in log message statistics.', + variable => 'pgstat_track_logmsg', + boot_val => 'PGSTAT_LOGMSG_TRACK_NONE', + options => 'track_log_messages_options', +}, + { name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', short_desc => 'Collects timing statistics for WAL I/O activity.', variable => 'track_wal_io_timing', diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 342aaeef59a..5acf5e1feab 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -259,6 +259,24 @@ static const struct config_enum_entry track_function_options[] = { StaticAssertDecl(lengthof(track_function_options) == (TRACK_FUNC_ALL + 2), "array length mismatch"); +static const struct config_enum_entry track_log_messages_options[] = { + {"none", PGSTAT_LOGMSG_TRACK_NONE, false}, + {"debug5", DEBUG5, false}, + {"debug4", DEBUG4, false}, + {"debug3", DEBUG3, false}, + {"debug2", DEBUG2, false}, + {"debug1", DEBUG1, false}, + {"debug", DEBUG2, true}, + {"info", INFO, false}, + {"notice", NOTICE, false}, + {"warning", WARNING, false}, + {"error", ERROR, false}, + {"log", LOG, false}, + {"fatal", FATAL, false}, + {"panic", PANIC, false}, + {NULL, 0, false} +}; + static const struct config_enum_entry stats_fetch_consistency[] = { {"none", PGSTAT_FETCH_CONSISTENCY_NONE, false}, {"cache", PGSTAT_FETCH_CONSISTENCY_CACHE, false}, diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e759f06b50f..2a00544b3e6 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -708,6 +708,8 @@ #track_io_timing = off #track_wal_io_timing = off #track_functions = none # none, pl, all +#track_log_messages = none # none, or minimum severity level to + # count in log message statistics #stats_fetch_consistency = cache # cache, none, snapshot diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 6f3e526de96..5ca10501b65 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202609152 +#define CATALOG_VERSION_NO 202609281 #endif diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f46427258e3..80280a55870 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6063,6 +6063,20 @@ proargnames => '{locktype,waits,wait_time,fastpath_exceeded,stats_reset}', prosrc => 'pg_stat_get_lock' }, +{ oid => '8796', descr => 'statistics: cumulative log message statistics', + proname => 'pg_stat_get_log_messages', prorows => '100', proretset => 't', + provolatile => 'v', proparallel => 'r', prorettype => 'record', + proargtypes => '', + proallargtypes => '{text,oid,oid,text,text,text,int8,timestamptz}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{backend_type,database_oid,user_oid,elevel,sqlerrcode,sqlerrcode_name,count,stats_reset}', + prosrc => 'pg_stat_get_log_messages' }, +{ oid => '8797', + descr => 'statistics: log messages dropped because the entry table was full', + proname => 'pg_stat_get_log_messages_dropped', provolatile => 'v', + proparallel => 'r', prorettype => 'int8', proargtypes => '', + prosrc => 'pg_stat_get_log_messages_dropped' }, + { oid => '6386', descr => 'statistics: backend IO statistics', proname => 'pg_stat_get_backend_io', prorows => '5', proretset => 't', provolatile => 'v', proparallel => 'r', prorettype => 'record', diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 187d82c96fe..4cce13d96d8 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -18,6 +18,9 @@ #include "storage/locktag.h" #include "utils/backend_progress.h" /* for backward compatibility */ /* IWYU pragma: export */ #include "utils/backend_status.h" /* for backward compatibility */ /* IWYU pragma: export */ +#include "utils/errcodes_names.h" /* for ERRCODES_NAMES_COUNT; the table + * itself is only exposed under + * ERRCODES_NAMES_INCLUDE_TABLE */ #include "utils/pgstat_kind.h" @@ -275,7 +278,7 @@ typedef struct PgStat_TableXactStatus * ------------------------------------------------------------ */ -#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBD +#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBE typedef struct PgStat_ArchiverStats { @@ -421,6 +424,52 @@ typedef struct PgStat_Lock PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1]; } PgStat_Lock; +/* ---------- + * PgStat_LogMsgStats Log message statistics + * + * Counts of server log messages grouped by (backend_type, database, user, + * severity level, SQLSTATE), capped at PGSTAT_LOGMSG_MAX_ENTRIES distinct + * combinations. Messages for combinations that do not fit are counted in + * n_dropped. + * + * The entries form an index-based separate-chaining hash table living + * entirely inside this struct: heads[bucket] points to the first entry of a + * chain and each entry's "next" links the rest (-1 terminates). Entries are + * allocated sequentially (entry i is live iff i < num_entries) and never + * evicted. Links are array indices rather than pointers, so the struct + * remains valid when copied around by the fixed-amount stats machinery + * (snapshots and stats file persistence are raw copies). + * + * The capacity is sized proportionally to the number of SQLSTATE error + * codes known to the server (ERRCODES_NAMES_COUNT, generated from + * errcodes.txt), leaving room for several dozen (backend type, database, + * user, severity) combinations per code. Note that the shared memory + * block, the per-backend snapshot, and the on-disk stats file all scale + * with this constant. + * ---------- + */ +#define PGSTAT_LOGMSG_MAX_ENTRIES (ERRCODES_NAMES_COUNT * 64) + +typedef struct PgStat_LogMsgEntry +{ + int32 next; /* next entry in bucket chain, or -1 */ + BackendType backend_type; + Oid dboid; + Oid userid; + int32 elevel; + int32 sqlerrcode; + PgStat_Counter count; +} PgStat_LogMsgEntry; + +typedef struct PgStat_LogMsgStats +{ + int32 num_entries; + PgStat_Counter n_dropped; + TimestampTz stat_reset_timestamp; + PgStat_LogMsgEntry entries[PGSTAT_LOGMSG_MAX_ENTRIES]; + int32 heads[PGSTAT_LOGMSG_MAX_ENTRIES]; +} PgStat_LogMsgStats; + typedef struct PgStat_StatDBEntry { PgStat_Counter xact_commit; @@ -725,6 +774,17 @@ extern void pgstat_count_lock_waits(uint8 locktag_type, PgStat_Counter usecs); extern PgStat_Lock *pgstat_fetch_stat_lock(void); +/* + * Functions in pgstat_logmsg.c + */ + +/* value of pgstat_track_logmsg disabling log message tracking */ +#define PGSTAT_LOGMSG_TRACK_NONE INT32_MAX + +extern void pgstat_count_logmsg(ErrorData *edata); +extern const char *pgstat_get_logmsg_errcode_name(int sqlerrcode); +extern PgStat_LogMsgStats *pgstat_fetch_stat_logmsg(void); + /* * Functions in pgstat_database.c */ @@ -955,6 +1015,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void); /* GUC parameters */ extern PGDLLIMPORT bool pgstat_track_counts; extern PGDLLIMPORT int pgstat_track_functions; +extern PGDLLIMPORT int pgstat_track_logmsg; extern PGDLLIMPORT int pgstat_fetch_consistency; diff --git a/src/include/utils/meson.build b/src/include/utils/meson.build index fd3a2352df5..99fd87ba37a 100644 --- a/src/include/utils/meson.build +++ b/src/include/utils/meson.build @@ -30,6 +30,17 @@ errcodes = custom_target('errcodes', ) generated_headers += errcodes +errcodes_names = custom_target('errcodes_names', + input: files('../../backend/utils/errcodes.txt'), + output: ['errcodes_names.h'], + command: [ + perl, files('../../backend/utils/generate-errcodes-names.pl'), + '--outfile', '@OUTPUT@', + '@INPUT@', + ], +) +generated_headers += errcodes_names + guc_tables = custom_target('guc_tables', input: files('../../backend/utils/misc/guc_parameters.dat'), output: ['guc_tables.inc.c'], diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 201e57279b1..5501f40ba5e 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -472,6 +472,14 @@ typedef struct PgStatShared_Lock PgStat_Lock stats; } PgStatShared_Lock; +typedef struct PgStatShared_LogMsg +{ + /* lock protects concurrent updates of ->stats */ + LWLock lock; + uint32 changecount; + PgStat_LogMsgStats stats; +} PgStatShared_LogMsg; + typedef struct PgStatShared_SLRU { /* lock protects ->stats */ @@ -585,6 +593,7 @@ typedef struct PgStat_ShmemControl PgStatShared_Checkpointer checkpointer; PgStatShared_IO io; PgStatShared_Lock lock; + PgStatShared_LogMsg logmsg; PgStatShared_SLRU slru; PgStatShared_Wal wal; @@ -619,6 +628,8 @@ typedef struct PgStat_Snapshot PgStat_Lock lock; + PgStat_LogMsgStats logmsg; + PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS]; PgStat_WalStats wal; @@ -781,6 +792,14 @@ extern void pgstat_lock_init_shmem_cb(void *stats); extern void pgstat_lock_reset_all_cb(TimestampTz ts); extern void pgstat_lock_snapshot_cb(void); +/* + * Functions in pgstat_logmsg.c + */ + +extern void pgstat_logmsg_init_shmem_cb(void *stats); +extern void pgstat_logmsg_reset_all_cb(TimestampTz ts); +extern void pgstat_logmsg_snapshot_cb(void); + /* * Functions in pgstat_relation.c */ diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h index 45ca599d0dd..efa56a53d71 100644 --- a/src/include/utils/pgstat_kind.h +++ b/src/include/utils/pgstat_kind.h @@ -40,9 +40,10 @@ #define PGSTAT_KIND_LOCK 12 #define PGSTAT_KIND_SLRU 13 #define PGSTAT_KIND_WAL 14 +#define PGSTAT_KIND_LOGMSG 15 /* log message statistics */ #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE -#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL +#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_LOGMSG #define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1) /* Custom stats kinds */ diff --git a/src/test/recovery/t/029_stats_restart.pl b/src/test/recovery/t/029_stats_restart.pl index cdc427dbc78..85fe152b35c 100644 --- a/src/test/recovery/t/029_stats_restart.pl +++ b/src/test/recovery/t/029_stats_restart.pl @@ -13,6 +13,7 @@ use File::Copy; my $node = PostgreSQL::Test::Cluster->new('primary'); $node->init(allows_streaming => 1); $node->append_conf('postgresql.conf', "track_functions = 'all'"); +$node->append_conf('postgresql.conf', "track_log_messages = 'warning'"); $node->start; my $connect_db = 'postgres'; @@ -293,6 +294,35 @@ cmp_ok( $wal_restart_immediate->{reset}, "$sect: reset timestamp is new"); + +## checks related to log message stats persistence around restarts + +# emit a log message with a distinctive SQLSTATE and check it is counted +$node->safe_psql($connect_db, + q{DO $$ BEGIN RAISE WARNING 'stats restart test' USING ERRCODE = 'ZZ777'; END $$;} +); + +$sect = "logmsg"; +my $logmsg = logmsg_stats(); +is($logmsg->{count}, '1', "$sect: emitted message counted"); + +# counts survive a clean restart +$node->restart; + +$sect = "logmsg post restart"; +my $logmsg_restart = logmsg_stats(); +is($logmsg_restart->{count}, '1', + "$sect: count persisted across clean restart"); +is($logmsg_restart->{reset}, $logmsg->{reset}, "$sect: stats_reset equal"); + +# counts are discarded after a crash +$node->stop('immediate'); +$node->start; + +$sect = "logmsg post immediate restart"; +my $logmsg_crash = logmsg_stats(); +is($logmsg_crash->{count}, '', "$sect: counts discarded after crash"); + $node->stop; done_testing(); @@ -374,3 +404,17 @@ sub io_stats return \%results; } + +sub logmsg_stats +{ + my %results; + + $results{count} = $node->safe_psql( + $connect_db, q{SELECT count FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ777' AND elevel = 'WARNING'}); + $results{reset} = $node->safe_psql( + $connect_db, q{SELECT stats_reset FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ777' AND elevel = 'WARNING'}); + + return \%results; +} diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 4a8cc759d7b..d17462c9c12 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1981,6 +1981,19 @@ pg_stat_lock| SELECT locktype, fastpath_exceeded, stats_reset FROM pg_stat_get_lock() l(locktype, waits, wait_time, fastpath_exceeded, stats_reset); +pg_stat_log_messages| SELECT s.backend_type, + s.database_oid, + d.datname AS database_name, + s.user_oid, + u.rolname AS user_name, + s.elevel, + s.sqlerrcode, + s.sqlerrcode_name, + s.count, + s.stats_reset + FROM ((pg_stat_get_log_messages() s(backend_type, database_oid, user_oid, elevel, sqlerrcode, sqlerrcode_name, count, stats_reset) + LEFT JOIN pg_database d ON ((d.oid = s.database_oid))) + LEFT JOIN pg_authid u ON ((u.oid = s.user_oid))); pg_stat_progress_analyze| SELECT s.pid, s.datid, d.datname, diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index 8b15471248b..afa67a5f3e8 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -128,7 +128,8 @@ SELECT id, name, fixed_amount, 12 | lock | t | f | t 13 | slru | t | f | t 14 | wal | t | f | t -(14 rows) + 15 | log_messages | t | f | t +(15 rows) -- ensure that both seqscan and indexscan plans are allowed SET enable_seqscan TO on; @@ -1263,10 +1264,100 @@ SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal; t (1 row) +-- Test log message statistics +-- Counting is disabled by default, so enable it first. +SET track_log_messages TO warning; +-- A message emitted to the server log must be counted, grouped by +-- backend type, database, user, severity level, and SQLSTATE. +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'ZZ042'; END $$; +WARNING: logmsg stats test +SELECT count >= 1 AS has_count, backend_type, elevel, sqlerrcode_name + FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ042' AND user_name = current_user + AND database_name = current_database(); + has_count | backend_type | elevel | sqlerrcode_name +-----------+----------------+---------+----------------- + t | client backend | WARNING | +(1 row) + +-- Known SQLSTATEs must resolve to their condition name +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'division_by_zero'; END $$; +WARNING: logmsg stats test +SELECT count >= 1 AS has_count, sqlerrcode, sqlerrcode_name + FROM pg_stat_log_messages + WHERE sqlerrcode = '22012' AND elevel = 'WARNING' + AND user_name = current_user AND backend_type = 'client backend'; + has_count | sqlerrcode | sqlerrcode_name +-----------+------------+------------------ + t | 22012 | division_by_zero +(1 row) + +-- track_log_messages = none disables counting +SET track_log_messages TO none; +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'ZZ043'; END $$; +WARNING: logmsg stats test +SELECT count(*) FROM pg_stat_log_messages WHERE sqlerrcode = 'ZZ043'; + count +------- + 0 +(1 row) + +SET track_log_messages TO warning; +-- Test that reset_shared with log_messages specified as the stats type +-- works and that it reclaims entries for reuse +SELECT stats_reset AS logmsg_reset_ts FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ042' AND user_name = current_user \gset +SELECT pg_stat_reset_shared('log_messages'); + pg_stat_reset_shared +---------------------- + +(1 row) + +SELECT count(*) FROM pg_stat_log_messages WHERE sqlerrcode = 'ZZ042'; + count +------- + 0 +(1 row) + +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'ZZ042'; END $$; +WARNING: logmsg stats test +SELECT count, stats_reset > :'logmsg_reset_ts'::timestamptz AS reset_ok + FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ042' AND user_name = current_user; + count | reset_ok +-------+---------- + 1 | t +(1 row) + +-- Test log message statistics privileges: pg_read_all_stats is required +CREATE ROLE regress_stats_logmsg; +SET ROLE regress_stats_logmsg; +SELECT count(*) FROM pg_stat_log_messages; +ERROR: permission denied for view pg_stat_log_messages +SELECT pg_stat_get_log_messages_dropped(); +ERROR: permission denied for function pg_stat_get_log_messages_dropped +RESET ROLE; +GRANT pg_read_all_stats TO regress_stats_logmsg; +SET ROLE regress_stats_logmsg; +SELECT count(*) >= 0 AS ok FROM pg_stat_log_messages; + ok +---- + t +(1 row) + +SELECT pg_stat_get_log_messages_dropped() >= 0 AS ok; + ok +---- + t +(1 row) + +RESET ROLE; +DROP ROLE regress_stats_logmsg; +RESET track_log_messages; -- Test error case for reset_shared with unknown stats type SELECT pg_stat_reset_shared('unknown'); ERROR: unrecognized reset target: "unknown" -HINT: Target must be "archiver", "bgwriter", "checkpointer", "io", "lock", "recovery_prefetch", "slru", or "wal". +HINT: Target must be "archiver", "bgwriter", "checkpointer", "io", "lock", "log_messages", "recovery_prefetch", "slru", or "wal". -- Test that reset works for pg_stat_database and pg_stat_database_conflicts -- Since pg_stat_database stats_reset starts out as NULL, reset it once first so that we -- have a baseline for comparison. The same for pg_stat_database_conflicts as it shares diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql index 674637e172b..7ef91d8dfbc 100644 --- a/src/test/regress/sql/stats.sql +++ b/src/test/regress/sql/stats.sql @@ -581,6 +581,55 @@ SELECT stats_reset AS wal_reset_ts FROM pg_stat_wal \gset SELECT pg_stat_reset_shared('wal'); SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal; +-- Test log message statistics +-- Counting is disabled by default, so enable it first. +SET track_log_messages TO warning; +-- A message emitted to the server log must be counted, grouped by +-- backend type, database, user, severity level, and SQLSTATE. +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'ZZ042'; END $$; +SELECT count >= 1 AS has_count, backend_type, elevel, sqlerrcode_name + FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ042' AND user_name = current_user + AND database_name = current_database(); + +-- Known SQLSTATEs must resolve to their condition name +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'division_by_zero'; END $$; +SELECT count >= 1 AS has_count, sqlerrcode, sqlerrcode_name + FROM pg_stat_log_messages + WHERE sqlerrcode = '22012' AND elevel = 'WARNING' + AND user_name = current_user AND backend_type = 'client backend'; + +-- track_log_messages = none disables counting +SET track_log_messages TO none; +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'ZZ043'; END $$; +SELECT count(*) FROM pg_stat_log_messages WHERE sqlerrcode = 'ZZ043'; +SET track_log_messages TO warning; + +-- Test that reset_shared with log_messages specified as the stats type +-- works and that it reclaims entries for reuse +SELECT stats_reset AS logmsg_reset_ts FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ042' AND user_name = current_user \gset +SELECT pg_stat_reset_shared('log_messages'); +SELECT count(*) FROM pg_stat_log_messages WHERE sqlerrcode = 'ZZ042'; +DO $$ BEGIN RAISE WARNING 'logmsg stats test' USING ERRCODE = 'ZZ042'; END $$; +SELECT count, stats_reset > :'logmsg_reset_ts'::timestamptz AS reset_ok + FROM pg_stat_log_messages + WHERE sqlerrcode = 'ZZ042' AND user_name = current_user; + +-- Test log message statistics privileges: pg_read_all_stats is required +CREATE ROLE regress_stats_logmsg; +SET ROLE regress_stats_logmsg; +SELECT count(*) FROM pg_stat_log_messages; +SELECT pg_stat_get_log_messages_dropped(); +RESET ROLE; +GRANT pg_read_all_stats TO regress_stats_logmsg; +SET ROLE regress_stats_logmsg; +SELECT count(*) >= 0 AS ok FROM pg_stat_log_messages; +SELECT pg_stat_get_log_messages_dropped() >= 0 AS ok; +RESET ROLE; +DROP ROLE regress_stats_logmsg; +RESET track_log_messages; + -- Test error case for reset_shared with unknown stats type SELECT pg_stat_reset_shared('unknown'); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 656f1f60862..763ea959287 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -756,6 +756,7 @@ EphemeralNamedRelationMetadataData EquivalenceClass EquivalenceMember EquivalenceMemberIterator +ErrCodeName ErrorContextCallback ErrorData ErrorSaveContext @@ -2303,6 +2304,7 @@ PgStatShared_HashEntry PgStatShared_Index PgStatShared_IO PgStatShared_Lock +PgStatShared_LogMsg PgStatShared_Relation PgStatShared_ReplSlot PgStatShared_SLRU @@ -2328,6 +2330,8 @@ PgStat_KindInfo PgStat_LocalState PgStat_Lock PgStat_LockEntry +PgStat_LogMsgEntry +PgStat_LogMsgStats PgStat_PendingDroppedStatsItem PgStat_PendingIO PgStat_PendingLock -- 2.55.0