Monitoring a Node with the Spock Status Views
Spock keeps the state of a node in shared memory and exposes it through a
set of views in the spock schema. Together they answer the questions an
operator would otherwise take to the server log: is every subscription
replicating, which workers are running, how far behind is each provider,
how much WAL is a slot holding back, and what happened recently.
| View | One row per | Purpose |
|---|---|---|
spock.system_status |
host | Host and instance health: disk, load, connections, long transactions, wraparound age, archiving. |
spock.node_status |
node | The whole node on one row: versions, counts, WAL, lag, last events. |
spock.subscription_status |
subscription | Configuration, status, serving worker, replication position and lag, last error. |
spock.subscription_stats |
subscription | Activity counters: worker starts, connects, transactions, tuples, conflicts, errors. |
spock.worker_status |
spock process | Supervisor, manager, apply and sync workers with their state. |
spock.slot_status |
replication slot | Provider side: walsender state, positions and retained WAL per spock slot. |
spock.table_sync_status |
subscription or table | Initial synchronization state in readable form. |
spock.peer_status |
known node | Every node this node knows, with the subscription that brings its changes here. |
spock.replication_set_status |
replication set | Replication sets of the local node with table, sequence and subscriber counts. |
spock.settings |
parameter | The spock parameters and the core parameters replication depends on. |
spock.events |
event | Recent state changes: enable, disable, restarts, errors, upgrades. |
spock.node_report() runs all of them one after another and returns the
node as a single text report; see The full report.
All views are scoped to the current database. spock.lag_tracker,
spock.progress and the spock.channel_*_stats views remain available and
feed the views above.
System status
spock.system_status is one row about the host and the PostgreSQL
instance. It answers the questions that come before any replication
question: is the disk full, is the machine overloaded, are connections
exhausted, is a long or idle transaction holding back vacuum and the
replication slots, is WAL archiving failing.
| Column | Description |
|---|---|
hostname, os_name, os_release, architecture, cpu_count, memory_bytes |
The host. |
load_average_1min, load_average_5min, load_average_15min |
System load averages. |
data_directory, data_disk_total_bytes, data_disk_free_bytes |
The data directory and the space on its volume. |
wal_disk_total_bytes, wal_disk_free_bytes, wal_size_bytes |
The volume holding pg_wal, measured separately in case it is a symlink, and the size of pg_wal. |
database_size_bytes |
Size of the current database. |
postgres_version, postmaster_pid, postmaster_start_time, uptime, in_recovery |
The instance. |
max_connections, connections, active_connections, idle_in_transaction |
Client connections and their state. |
longest_transaction, longest_idle_in_transaction |
Age of the oldest open transaction and of the oldest idle one; both hold back vacuum and slot catalog_xmin. |
waiting_on_locks |
Sessions currently waiting for a lock. |
prepared_transactions |
Prepared transactions, which hold locks and xmin until resolved. |
physical_standbys |
Streaming replicas other than spock subscribers. |
oldest_xid_age |
Age of the database's oldest transaction id, towards wraparound. |
xact_commit, xact_rollback, deadlocks, recovery_conflicts, temp_files, temp_bytes, checksum_failures, database_stats_reset |
From pg_stat_database for this database. |
archive_mode, archived_count, last_archived_time, archive_failed_count, last_archive_failure |
WAL archiving state; a failing archiver fills the WAL volume. |
SELECT pg_size_pretty(data_disk_free_bytes) AS data_free,
pg_size_pretty(wal_disk_free_bytes) AS wal_free,
load_average_1min, connections, max_connections,
longest_transaction, idle_in_transaction, deadlocks,
archive_failed_count, last_archive_failure
FROM spock.system_status;
Node status
spock.node_status always returns exactly one row, even before a node has
been created.
| Column | Description |
|---|---|
node_id, node_name, location, country |
The local node, NULL when none exists. |
database_name |
The current database. |
hostname, operating_system, postmaster_start_time |
The host and the server start. |
spock_version |
Version of the loaded shared library. |
extension_version |
Version of the installed extension in the catalog. |
extension_update_pending |
True when the library is newer than the extension; the manager runs ALTER EXTENSION UPDATE on its next start. |
postgres_version |
Server version. |
spock_started_at |
When spock shared memory was initialized, normally the postmaster start. |
postmaster_pid, supervisor_pid, manager_pid |
Process ids of the postmaster, the supervisor and this database's manager. |
readonly_mode |
Current value of spock.readonly. |
apply_paused |
True while spock.pause_apply_workers() is in effect. |
worker_slots, worker_slots_used |
Background worker slots spock can use and how many are taken. |
subscriptions, subscriptions_enabled, subscriptions_replicating |
Subscription counts. |
replication_slots, replication_slots_active |
Spock slots of this database and how many have a connected walsender. |
apply_workers, sync_workers |
Running worker counts. |
workers_restart_pending |
Workers that exited with an error and wait for the manager to restart them. |
pending_exceptions |
Transactions currently being retried under spock.exception_behaviour. |
replicated_tables, unreplicated_tables |
Tables of this node in some replication set, and tables in none. |
current_wal_lsn |
Current WAL position, replay position on a standby. |
wal_size_bytes |
Size of pg_wal, read through spock.wal_size_bytes(), which sums the files itself so that a role without pg_monitor can read it. |
data_disk_free_bytes, wal_disk_free_bytes |
Free space on the volumes holding the data directory and pg_wal. |
max_retained_wal_bytes |
WAL held back by the slowest spock slot. |
max_replication_lag_bytes, max_replication_lag |
Largest lag over all subscriptions. |
exceptions_logged |
Rows in spock.exception_log. |
last_event_time, last_error_event_time |
Time of the newest event, and of the newest error event. |
events_retained, events_recorded |
Events kept in the history and recorded since start. |
SELECT node_name, extension_update_pending, subscriptions_replicating,
replication_slots_active, workers_restart_pending,
pg_size_pretty(max_retained_wal_bytes) AS retained_wal,
max_replication_lag, last_error_event_time
FROM spock.node_status;
Subscription status
spock.subscription_status is the first place to look when replication
seems stuck.
| Column | Description |
|---|---|
subscription_id, subscription_name, enabled |
The subscription. |
status |
replicating, initializing, starting (the worker is up but not streaming yet, as during a reconnect loop), down (enabled but no worker), disabled or unknown. |
provider_node, provider_dsn |
The provider; the password in the DSN is replaced with asterisks. |
slot_name, replication_sets, forward_origins, apply_delay, skip_lsn, skip_schema, force_text_transfer, created_at |
Configuration. |
sync_status |
Initial synchronization state of the subscription as a whole. |
worker_pid, worker_status, worker_started_at |
The apply worker. worker_status is restart pending after a failure. |
worker_terminated_at, worker_restart_delay |
When the last worker failed and how long the manager waits before restarting it, in milliseconds. |
worker_paused |
True while the worker sleeps because apply is paused. |
in_exception_handling |
True while the worker replays a failed transaction under spock.exception_behaviour. |
pending_exception_lsn, pending_exception_error |
Commit LSN and first error of the transaction being retried, if any. |
origin_lsn |
Last durably applied provider LSN, from spock.replication_origin_status(). |
remote_commit_lsn, remote_commit_ts, remote_insert_lsn, received_lsn |
Positions from spock.progress. |
replication_lag_bytes |
WAL the provider has written that this node has not yet applied. |
replication_lag |
Time between the last applied commit on the provider and its apply here. |
last_message_received, last_xact_applied |
When data last arrived from the provider and when a transaction was last committed here. |
time_since_last_message |
How long ago data last arrived; a silent provider shows here first. |
worker_blocked_by_pids, worker_blocking_pid, worker_blocking_query |
Set while the worker waits for a lock: the sessions holding it and what the first one runs. |
worker_starts, worker_failures, xacts_applied, apply_errors |
Counters from spock.subscription_stats. |
last_apply_error, last_error_sqlstate, last_error_pid, last_error_message |
The most recent error of the apply worker: when, its SQLSTATE, the process that raised it, and the message with DETAIL and CONTEXT. |
SELECT subscription_name, status, worker_status, replication_lag,
pg_size_pretty(replication_lag_bytes) AS lag, last_error_message
FROM spock.subscription_status;
Subscription statistics
spock.subscription_stats has one row for every subscription with the
counters collected by its apply and sync workers. Counters survive worker
restarts and are reset with spock.reset_subscription_stats(subid), or for
all subscriptions with no argument. Every counter has a matching last_*
timestamp of its most recent change.
| Column | Description |
|---|---|
worker_starts, worker_failures |
Apply worker starts, and exits with an error. |
sync_worker_starts, sync_worker_failures |
The same for the sync workers that copy tables; a resync adds one start per table. |
provider_connects, provider_disconnects, idle_timeouts |
Connections to the provider, connection losses together with connection attempts that failed (so while the provider is down this counts every retry), and reconnects forced by spock.apply_idle_timeout. |
messages_received, bytes_received |
Data messages received from the provider. |
xacts_applied, xacts_skipped, xacts_discarded |
Transactions committed locally, skipped through sub_skip_lsn, and discarded under transdiscard. |
apply_errors, sync_errors |
Errors raised in apply and in sync workers. An apply error counts once per failed transaction. |
deadlocks, lock_timeouts, constraint_violations, resource_errors |
The same errors classified by SQLSTATE: deadlock detected, lock not available, class 23 and class 53. |
tables_synced |
Tables whose initial synchronization completed. |
n_tup_ins, n_tup_upd, n_tup_del, n_conflict, n_dca |
Tuple counters, summed from spock.channel_table_stats. |
confl_* |
Conflict counters by type; NULL before PostgreSQL 18. |
last_error_message, last_error_lsn, last_error_sqlstate, last_error_pid |
The most recent worker error with DETAIL and CONTEXT, the provider LSN it happened at, its SQLSTATE and the process that raised it. |
stats_reset |
When the counters were last reset. |
Workers
spock.worker_status lists the supervisor, the manager of this database,
and every apply and sync worker, including slots of failed workers that are
waiting to be restarted.
| Column | Description |
|---|---|
worker_slot, worker_type, pid |
Slot in shared memory, supervisor, manager, apply or sync, and process id. |
worker_status |
starting, running, idle, stopping, stopped, failed or restart pending. |
subscription_id, subscription_name |
For apply and sync workers. |
sync_schema_name, sync_table_name |
The table a sync worker copies. |
started_at, xact_start, backend_state, wait_event_type, wait_event |
From pg_stat_activity. |
blocked_by_pids, blocking_pid, blocking_xact_start, blocking_query |
Set while the worker waits for a lock held by another session, from pg_blocking_pids(). |
paused, in_exception_handling, sync_pending |
Apply worker flags. |
replay_stop_lsn, remote_wal_insert_lsn |
Positions the worker works towards and has last seen on the provider. |
generation, terminated_at, restart_delay |
Registration counter, time of the last failure, restart delay in milliseconds. |
Replication slots
spock.slot_status is the provider side view: one row per spock
replication slot of the current database, joined with the walsender that
serves it.
| Column | Description |
|---|---|
slot_name, active, active_pid |
The slot and its walsender. |
client_name, client_addr, connected_at, state |
The connected subscriber, from pg_stat_replication. |
restart_lsn, confirmed_flush_lsn, sent_lsn, write_lsn, flush_lsn, replay_lsn |
Positions. |
retained_wal_bytes |
WAL kept on disk for this slot. |
pending_wal_bytes |
WAL the subscriber has not confirmed yet. |
write_lag, flush_lag, replay_lag, last_reply_time |
Feedback timing. |
wal_status, safe_wal_size, catalog_xmin, catalog_xmin_age, temporary, two_phase |
From pg_replication_slots; a growing catalog_xmin_age means the slot holds back catalog vacuum. |
decoded_txns, decoded_bytes, spill_*, stream_*, decode_stats_reset |
Decoding work done for the slot, from pg_stat_replication_slots. Growing spill_bytes means transactions larger than logical_decoding_work_mem. |
slot_group_* |
Shared state of the slot group, for parallel slots only. |
SELECT slot_name, active, state, pg_size_pretty(retained_wal_bytes) AS retained,
pg_size_pretty(pending_wal_bytes) AS pending, replay_lag
FROM spock.slot_status;
Peers, replication sets and settings
spock.peer_status lists every node in spock.node with its interfaces
(passwords hidden), whether it is the local node, and the subscription that
brings its changes here with that subscription's status and lag. A node
with several subscriptions has one row per subscription.
spock.replication_set_status lists the replication sets of the local node
with their replicate flags, the number of tables and sequences in each, and
how many local subscriptions include the set. Tables that are in no set at
all are listed by spock.tables where set_name IS NULL.
spock.settings shows every spock.* parameter and the core parameters
replication depends on (wal_level, max_worker_processes,
max_replication_slots, max_wal_senders, max_slot_wal_keep_size,
track_commit_timestamp, shared_preload_libraries, the walsender and
walreceiver timeouts, synchronous commit settings and
logical_decoding_work_mem), with unit, source and whether a restart is
pending. Connection strings in parameters have their password hidden.
Events
spock.events is a ring of recent state changes kept in shared memory,
oldest first. The number of events kept is set by
spock.event_history_size. The history is cleared by a postmaster restart,
and spock.reset_events() clears the events of the current database.
| Column | Description |
|---|---|
event_id |
Increases by one per event and is never reused. |
event_time, event_type, severity |
When, what, and info, warning or error. |
sqlstate |
The SQLSTATE of a worker_error or stream_error event. |
subscription_id, subscription_name, provider_node |
The subscription concerned, if any, and the node its changes come from. |
pid |
The reporting process. |
lsn |
The provider LSN the event refers to: the start position of a stream, the commit LSN of a skipped, discarded or failed transaction, the position a subscription was disabled at. NULL for lifecycle events such as worker starts. |
detail |
A short description. |
Event types:
| Type | Recorded when |
|---|---|
node_created, node_dropped |
spock.node_create() and spock.node_drop() complete. |
subscription_created, subscription_dropped |
A subscription is created or dropped. |
subscription_enabled, subscription_disabled |
A subscription is enabled or disabled, by an operator or by exception handling. |
subscription_altered |
Interface, replication sets, skip_lsn, options or a synchronization request changed. |
worker_started, worker_stopped, worker_failed |
A manager, apply or sync worker starts, exits cleanly, or exits with an error. |
worker_error |
An error is raised inside an apply or sync worker. The detail carries the message with DETAIL and CONTEXT. |
provider_connected, provider_disconnected |
The replication connection to the named provider is established, with the start LSN, or lost, with the error. |
transaction_skipped, transaction_discarded |
A transaction is skipped through sub_skip_lsn or discarded under transdiscard. |
sync_started, sync_finished, sync_failed |
Initial copy of a table. sync_failed carries the error that ended the copy; the next sync worker for that table records a second sync_failed saying the table was abandoned after the earlier failure, and no further attempt is made. |
slot_created, stream_started, stream_stopped, stream_error |
Provider side: a spock slot is created, a walsender starts streaming to a subscriber (its address, spock version and protocol in the detail, the slot position in lsn), stops (recorded as the walsender exits, so a subscriber that disconnects or restarts produces one), or fails with an error while decoding. |
apply_paused, apply_resumed |
spock.pause_apply_workers() and spock.resume_apply_workers(). |
repair_mode_enabled, repair_mode_disabled |
spock.repair_mode(). |
extension_upgraded |
The manager updated the extension to the library version. |
Events are recorded when the reporting code runs, whether or not the surrounding transaction commits.
SELECT event_time, event_type, subscription_name, detail
FROM spock.events
WHERE severity <> 'info'
ORDER BY event_id DESC
LIMIT 20;
The full report
spock.node_report() returns the whole node as text, section by section:
system status, installed extensions, settings, node status, peers,
subscriptions, subscription statistics, workers, pending exceptions,
replication slots, replication sets, tables in no replication set, table
synchronization, lag by node pair, recent exceptions, recent conflict
resolutions and recent events. Wide views are printed one row per block,
lists as aligned tables. A section whose query fails, for example
node_status for a role without EXECUTE on the functions behind it,
prints one error line and the report continues.
SELECT * FROM spock.node_report();
SELECT * FROM spock.node_report(200); -- list up to 200 events, exceptions and resolutions
In psql, \pset format unaligned and \o report.txt before the call
produce a plain file that can be attached to a ticket. The report contains
no passwords.
spock.report_section(title, query, expanded) is the formatter behind
the report and can render any query the same way.
Common tasks
The recipes below cover the questions that used to require the server log.
Run them in psql with expanded output (\x) when a row is wide.
Is the node healthy?
One row answers it. Anything other than zero in workers_restart_pending,
a difference between subscriptions_enabled and subscriptions_replicating,
or a last_error_event_time that is more recent than you expect deserves a
look.
SELECT node_name, extension_update_pending,
subscriptions_enabled, subscriptions_replicating,
workers_restart_pending, apply_paused, readonly_mode,
pg_size_pretty(max_retained_wal_bytes) AS retained_wal,
max_replication_lag, last_error_event_time
FROM spock.node_status;
Why is a subscription not replicating?
Start with the subscription row. status tells whether a worker runs at
all, worker_status and worker_terminated_at show a failed worker
waiting for its restart delay, and last_error_message is the error the
worker last raised, without opening the log.
SELECT subscription_name, status, enabled, worker_status,
worker_terminated_at, worker_restart_delay,
in_exception_handling, last_apply_error, last_error_message
FROM spock.subscription_status
WHERE status <> 'replicating';
Then read the events of that subscription, newest first:
SELECT event_time, event_type, severity, detail
FROM spock.events
WHERE subscription_name = 'sub_n2_n1'
ORDER BY event_id DESC
LIMIT 20;
A subscription that was disabled by exception handling shows a
subscription_disabled event with the detail disabled by exception
handling and the origin LSN of the failing transaction in lsn; the row in
spock.exception_log has the failing tuple.
Is a worker blocked or deadlocking?
A worker waiting for a lock shows wait_event_type = 'Lock' and the
session holding the lock in blocked_by_pids and blocking_query. A
deadlock or lock timeout aborts the transaction; the worker exits and is
restarted, and the error is classified.
SELECT worker_type, subscription_name, wait_event_type, wait_event,
blocked_by_pids, blocking_pid, blocking_xact_start, blocking_query
FROM spock.worker_status
WHERE blocked_by_pids IS NOT NULL;
SELECT subscription_name, deadlocks, lock_timeouts,
constraint_violations, resource_errors,
last_error_sqlstate, last_apply_error, last_error_message
FROM spock.subscription_stats;
Is a worker restarting in a loop?
worker_starts and worker_failures climb together when the apply worker
crashes on every attempt. The events show the interval and the reason. Sync
workers have their own pair of counters, so a resync of many tables does
not look like a restart loop here.
SELECT subscription_name, worker_starts, worker_failures,
last_worker_failure, last_error_message
FROM spock.subscription_stats
WHERE worker_failures > 0;
SELECT event_time, event_type, detail
FROM spock.events
WHERE event_type IN ('worker_started', 'worker_failed', 'worker_error')
ORDER BY event_id DESC
LIMIT 30;
What changed overnight?
Everything that is not routine has a severity above info.
SELECT event_time, event_type, severity, subscription_name, detail
FROM spock.events
WHERE event_time > now() - interval '12 hours'
AND severity <> 'info'
ORDER BY event_id;
Enable and disable calls, interface switches, replication set changes,
sub_skip_lsn, pause and resume, repair mode and extension upgrades are
all info or warning events with the operator's backend pid in pid.
How far behind is each provider?
SELECT subscription_name, provider_node,
pg_size_pretty(replication_lag_bytes) AS lag_bytes,
replication_lag, remote_commit_ts, received_lsn, origin_lsn
FROM spock.subscription_status
ORDER BY replication_lag_bytes DESC NULLS LAST;
replication_lag_bytes is WAL the provider has written that this node has
not applied yet. replication_lag is the time between the last applied
commit on the provider and its apply here. Both come from
spock.progress and are also available per node pair in
spock.lag_tracker.
Is a slot holding back WAL on the provider?
Run this on the provider. A slot with active = false or a growing
retained_wal_bytes points at a subscriber that is down or lagging.
SELECT slot_name, active, client_name, state,
pg_size_pretty(retained_wal_bytes) AS retained,
pg_size_pretty(pending_wal_bytes) AS pending,
replay_lag, last_reply_time, wal_status
FROM spock.slot_status
ORDER BY retained_wal_bytes DESC;
Is the initial synchronization finished?
SELECT subscription_name, schema_name, table_name, sync_kind, sync_status
FROM spock.table_sync_status
WHERE sync_status NOT IN ('ready', 'synchronized');
The row without a table is the subscription itself. Sync workers in flight
are listed in spock.worker_status with worker_type = 'sync' and the
table they copy.
Did the extension get upgraded?
spock.node_status compares the loaded library with the installed
extension. After a binary upgrade the manager updates the extension on its
next start and records the change.
SELECT spock_version, extension_version, extension_update_pending
FROM spock.node_status;
SELECT event_time, detail
FROM spock.events
WHERE event_type = 'extension_upgraded';
Starting a fresh measurement
Counters and events accumulate since the postmaster started. Reset them before a benchmark or after fixing an incident so that the next numbers are clean.
SELECT spock.reset_subscription_stats(); -- all subscriptions
SELECT spock.reset_subscription_stats(sub_id)
FROM spock.subscription WHERE sub_name = 'sub_n2_n1';
SELECT spock.reset_events();
spock.reset_channel_stats() clears the per-table tuple counters
separately.
Watching from outside
The views are plain SQL, so any monitoring agent can poll them. Poll
spock.events by event_id to fetch only new rows, and treat a gap in
ids as history that was dropped because spock.event_history_size was too
small for the polling interval. spock.get_monitor_summary() reports how
full the event ring and the counters table are. The read-only views and
the functions they call are granted to pg_monitor; the role also needs
USAGE on the schema:
GRANT pg_monitor TO monitoring_role;
GRANT USAGE ON SCHEMA spock TO monitoring_role;
For now, USAGE on the spock schema also allows calling spock's
management functions. Grant it only to trusted roles, and treat every
function that changes something as needing a superuser. Members of
pg_monitor cannot reset counters or events: spock.reset_events() and
spock.reset_subscription_stats() stay with the superuser. The
node_report() sections on spock.tables, spock.lag_tracker,
spock.exception_log and spock.resolutions need SELECT on those,
which the grant does not include; without it each shows as one error line.
Underlying functions
The views are built on functions that return the raw shared memory contents. They are useful for tooling that wants unfiltered rows.
| Function | Returns |
|---|---|
spock.get_worker_status() |
Every worker slot of the cluster, with database_id. |
spock.get_events() |
Every event in the history, with database_id. Superuser or pg_monitor. |
spock.get_apply_stats() |
The counters of every subscription of the current database. Superuser or pg_monitor. |
spock.get_slot_groups() |
Slot groups with attached walsenders. |
spock.get_monitor_summary() |
Capacity and usage of the shared memory structures. |
spock.get_system_info() |
Host name, operating system, CPU, memory, load averages, disk space of the data and WAL volumes, postmaster start, PostgreSQL and spock versions. Superuser or pg_monitor; the data directory only with pg_read_all_settings. |
spock.get_pending_exceptions() |
Transactions being retried under exception handling, with their first error and the position of the failing change in the transaction. Superuser or pg_monitor. |
spock.replication_origin_status() |
Every replication origin with its remote progress, as pg_replication_origin_status shows a superuser. Superuser or pg_monitor. |
spock.reset_events() |
Clears the event history of the current database. Superuser only. |
spock.reset_subscription_stats(subid) |
Resets the activity and conflict counters of one or all subscriptions. Superuser only. |
spock.redact_dsn(text) |
Returns a connection string with its password hidden. |