11 Commits

Author SHA1 Message Date
8928bba337 Format python using black 2025-05-15 02:04:50 -04:00
c872fc6b90 Start implementing metric tests 2025-05-15 02:01:20 -04:00
030afafc20 Actually fix Gentoo ebuild for v1.0.1 2025-05-14 02:42:10 -04:00
2dfc336288 Add target to build package for Gentoo 2025-05-14 01:43:44 -04:00
27e1c517bc Add ebuild file for v1.0.1 2025-05-14 01:05:17 -04:00
e6166d1fe3 Update version to 1.0.1 2025-05-14 00:29:09 -04:00
bffabd9c8f Reformat python code using black 2025-05-13 01:44:47 -04:00
98ac25743b Add queries for replication slot monitoring 2025-04-19 02:33:48 -04:00
7fc23961b0 Switch template to http agent 2025-04-19 02:28:33 -04:00
8ace133c23 Dynamically specify version for RPM builds 2025-04-19 00:22:26 -04:00
2afeb827ed Improve openrc init script, add port setting
* Ensure the log directory exists with openrc

* Add a port setting to configure the port the agent listens on

* Switch to RealDictCursor

* Fix type for connection timeout
2025-04-19 00:07:15 -04:00
13 changed files with 984 additions and 608 deletions

View File

@@ -1,52 +0,0 @@
# Copyright 2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
EAPI=8
PYTHON_COMPAT=( python3_{6..12} )
inherit git-r3 python-r1
DESCRIPTION="PostgreSQL monitoring bridge"
HOMEPAGE="None"
LICENSE="BSD"
SLOT="0"
KEYWORDS="amd64"
EGIT_REPO_URI="https://code2.shh-dot-com.org/james/pgmon.git"
#EGIT_COMMIT=""
DEPEND="
${PYTHON_DEPS}
dev-python/psycopg:3
dev-python/pyyaml
acct-user/zabbix
acct-group/zabbix
agent? ( net-analyzer/zabbix[agent] )
agent2? ( net-analyzer/zabbix[agent2] )
app-admin/logrotate
"
RDEPEND="${DEPEND}"
BDEPEND=""
src_install() {
default
# Install init script
newinitd "${FILESDIR}/pgmon.openrc" pgmon
# Install script
exeinto /usr/bin
newexe "${S}/pgmon.py" pgmon
# Install default config
diropts -o root -g zabbix -m 0755
insinto /etc/pgmon
doins "${FILESDIR}/pgmon.yml"
doins "${S}/pgmon-metrics.yml"
# Install logrotate config
insinto /etc/logrotate.d
newins "${FILESDIR}/pgmon.logrotate" pgmon
}

View File

@@ -5,7 +5,7 @@ EAPI=8
PYTHON_COMPAT=( python3_{6..13} ) PYTHON_COMPAT=( python3_{6..13} )
inherit git-r3 python-r1 inherit python-r1
DESCRIPTION="PostgreSQL monitoring bridge" DESCRIPTION="PostgreSQL monitoring bridge"
HOMEPAGE="None" HOMEPAGE="None"
@@ -14,7 +14,9 @@ LICENSE="BSD"
SLOT="0" SLOT="0"
KEYWORDS="amd64" KEYWORDS="amd64"
SRC_URI="https://code2.shh-dot-com.org/james/${PN}/archive/v${PV}.tar.gz -> ${P}.tar.gz" SRC_URI="https://code2.shh-dot-com.org/james/${PN}/archive/v${PV}.tar.bz2 -> ${P}.tar.bz2"
IUSE="-systemd"
DEPEND=" DEPEND="
${PYTHON_DEPS} ${PYTHON_DEPS}
@@ -25,21 +27,36 @@ DEPEND="
RDEPEND="${DEPEND}" RDEPEND="${DEPEND}"
BDEPEND="" BDEPEND=""
S="${WORKDIR}/${PN}" RESTRICT="fetch"
#S="${WORKDIR}/${PN}"
pkg_nofetch() {
einfo "Please download"
einfo " - ${P}.tar.bz2"
einfo "from ${HOMEPAGE} and place it in your DISTDIR directory."
einfo "The file should be owned by portage:portage."
}
src_compile() {
true
}
src_install() { src_install() {
default
# Install init script # Install init script
if ! use systemd ; then
newinitd "openrc/pgmon.initd" pgmon newinitd "openrc/pgmon.initd" pgmon
newconfd "openrc/pgmon.confd" pgmon newconfd "openrc/pgmon.confd" pgmon
fi
# Install systemd unit # Install systemd unit
if use systemd ; then
systemd_dounit "systemd/pgmon.service" systemd_dounit "systemd/pgmon.service"
fi
# Install script # Install script
exeinto /usr/bin exeinto /usr/bin
newexe "pgmon.py" pgmon newexe "src/pgmon.py" pgmon
# Install default config # Install default config
diropts -o root -g root -m 0755 diropts -o root -g root -m 0755

View File

@@ -1,6 +1,6 @@
# Package details # Package details
PACKAGE_NAME := pgmon PACKAGE_NAME := pgmon
VERSION := 1.0 VERSION := 1.0.1
SCRIPT := src/$(PACKAGE_NAME).py SCRIPT := src/$(PACKAGE_NAME).py
@@ -15,7 +15,8 @@ SUPPORTED := ubuntu-20.04 \
debian-11 \ debian-11 \
rockylinux-8 \ rockylinux-8 \
rockylinux-9 \ rockylinux-9 \
oraclelinux-7 oraclelinux-7 \
gentoo
## ##
# These targets are the main ones to use for most things. # These targets are the main ones to use for most things.
@@ -28,6 +29,12 @@ SUPPORTED := ubuntu-20.04 \
.PHONY: package-all .PHONY: package-all
all: $(foreach distro_release, $(SUPPORTED), package-$(distro_release)) all: $(foreach distro_release, $(SUPPORTED), package-$(distro_release))
# Gentoo package (tar.gz) creation
.PHONY: package-gentoo
package-gentoo:
mkdir -p $(BUILD_DIR)/gentoo
tar --transform "s,^\.,$(PACKAGE_NAME)-$(VERSION)," -acjf $(BUILD_DIR)/gentoo/$(PACKAGE_NAME)-$(VERSION).tar.bz2 --exclude $(BUILD_DIR) .
# Create a deb package # Create a deb package
.PHONY: package-% .PHONY: package-%
@@ -42,7 +49,6 @@ package-%:
--user $(shell id -u):$(shell id -g) \ --user $(shell id -u):$(shell id -g) \
"$(DISTRO)-packager:$(RELEASE)" "$(DISTRO)-packager:$(RELEASE)"
# Create a tarball # Create a tarball
tgz: tgz:
rm -rf $(BUILD_DIR)/tgz/root rm -rf $(BUILD_DIR)/tgz/root
@@ -158,7 +164,7 @@ actually-package-debian-%:
# RedHat package creation # RedHat package creation
actually-package-rockylinux-%: actually-package-rockylinux-%:
mkdir -p /output/rockylinux-$*/{BUILD,RPMS,SOURCES,SPECS,SRPMS} mkdir -p /output/rockylinux-$*/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
cp RPM/$(PACKAGE_NAME).spec /output/rockylinux-$*/SPECS/ sed -e "s/@@VERSION@@/$(VERSION)/g" RPM/$(PACKAGE_NAME).spec > /output/rockylinux-$*/SPECS/$(PACKAGE_NAME).spec
rpmbuild --define '_topdir /output/rockylinux-$*' \ rpmbuild --define '_topdir /output/rockylinux-$*' \
--define 'version $(VERSION)' \ --define 'version $(VERSION)' \
-bb /output/rockylinux-$*/SPECS/$(PACKAGE_NAME).spec -bb /output/rockylinux-$*/SPECS/$(PACKAGE_NAME).spec
@@ -173,7 +179,7 @@ actually-package-ubuntu-%:
# OracleLinux package creation # OracleLinux package creation
actually-package-oraclelinux-%: actually-package-oraclelinux-%:
mkdir -p /output/oraclelinux-$*/{BUILD,RPMS,SOURCES,SPECS,SRPMS} mkdir -p /output/oraclelinux-$*/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
cp RPM/$(PACKAGE_NAME)-el7.spec /output/oraclelinux-$*/SPECS/$(PACKAGE_NAME).spec sed -e "s/@@VERSION@@/$(VERSION)/g" RPM/$(PACKAGE_NAME)-el7.spec > /output/oraclelinux-$*/SPECS/$(PACKAGE_NAME).spec
rpmbuild --define '_topdir /output/oraclelinux-$*' \ rpmbuild --define '_topdir /output/oraclelinux-$*' \
--define 'version $(VERSION)' \ --define 'version $(VERSION)' \
-bb /output/oraclelinux-$*/SPECS/$(PACKAGE_NAME).spec -bb /output/oraclelinux-$*/SPECS/$(PACKAGE_NAME).spec

View File

@@ -1,5 +1,5 @@
Name: pgmon Name: pgmon
Version: 1.0 Version: @@VERSION@@
Release: 1%{?dist} Release: 1%{?dist}
Summary: A bridge to sit between monitoring tools and PostgreSQL Summary: A bridge to sit between monitoring tools and PostgreSQL

View File

@@ -1,5 +1,5 @@
Name: pgmon Name: pgmon
Version: 1.0 Version: @@VERSION@@
Release: 1%{?dist} Release: 1%{?dist}
Summary: A bridge to sit between monitoring tools and PostgreSQL Summary: A bridge to sit between monitoring tools and PostgreSQL

View File

@@ -11,6 +11,13 @@ PGMON_USER="${PGMON_USER:-postgres}"
PGMON_GROUP="${PGMON_GROUP:-$PGMON_USER}" PGMON_GROUP="${PGMON_GROUP:-$PGMON_USER}"
CONFIG_FILE="/etc/pgmon/${agent_name}.yml" CONFIG_FILE="/etc/pgmon/${agent_name}.yml"
output_log=/var/log/pgmon/${SVCNAME}.log
error_log=/var/log/pgmon/${SVCNAME}.err
start_pre() {
checkpath -f -m 0644 -o "${PGMON_USER}:${PGMON_GROUP}" "${output_log}" "${error_log}"
}
command="/usr/bin/pgmon" command="/usr/bin/pgmon"
command_args="'$CONFIG_FILE'" command_args="'$CONFIG_FILE'"
command_background="true" command_background="true"

4
requirements-dev.yml Normal file
View File

@@ -0,0 +1,4 @@
-r requirements.txt
testcontainers[postgresql]
pytest
black

View File

@@ -8,6 +8,10 @@ metrics:
type: set type: set
query: query:
0: SELECT client_addr || '_' || regexp_replace(application_name, '[ ,]', '_', 'g') AS repid, client_addr, state FROM pg_stat_replication 0: SELECT client_addr || '_' || regexp_replace(application_name, '[ ,]', '_', 'g') AS repid, client_addr, state FROM pg_stat_replication
discover_slots:
type: set
query:
0: SELECT slot_name, plugin, slot_type, database, temporary, active FROM pg_replication_slots
# cluster-wide metrics # cluster-wide metrics
version: version:
@@ -43,3 +47,9 @@ metrics:
type: value type: value
query: query:
0: SELECT now(), pg_sleep(5); 0: SELECT now(), pg_sleep(5);
# Per-slot metrics
slot_stats:
type: row
query:
0: SELECT active_pid, xmin, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS restart_bytes, pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) AS confirmed_flush_bytes FROM pg_replication_slots WHERE slot_name = '{slot}'

View File

@@ -1,3 +1,6 @@
# The port the agent listens on for requests
#port: 5400
# Min PostgreSQL connection pool size (per database) # Min PostgreSQL connection pool size (per database)
#min_pool_size: 0 #min_pool_size: 0

View File

@@ -11,7 +11,7 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import psycopg2 import psycopg2
from psycopg2.extras import DictCursor from psycopg2.extras import RealDictCursor
from psycopg2.pool import ThreadedConnectionPool from psycopg2.pool import ThreadedConnectionPool
from contextlib import contextmanager from contextlib import contextmanager
@@ -23,7 +23,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from http.server import ThreadingHTTPServer from http.server import ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
VERSION = '0.1.0' VERSION = "1.0.1"
# Configuration # Configuration
config = {} config = {}
@@ -53,64 +53,65 @@ config_file = None
# Configure logging # Configure logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s: %(funcName)s() line %(lineno)d: %(message)s') formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(filename)s: %(funcName)s() line %(lineno)d: %(message)s"
)
console_log_handler = logging.StreamHandler() console_log_handler = logging.StreamHandler()
console_log_handler.setFormatter(formatter) console_log_handler.setFormatter(formatter)
log.addHandler(console_log_handler) log.addHandler(console_log_handler)
# Error types # Error types
class ConfigError(Exception): class ConfigError(Exception):
pass pass
class DisconnectedError(Exception): class DisconnectedError(Exception):
pass pass
class UnhappyDBError(Exception): class UnhappyDBError(Exception):
pass pass
class MetricVersionError(Exception): class MetricVersionError(Exception):
pass pass
# Default config settings # Default config settings
default_config = { default_config = {
# The port the agent listens on for requests
"port": 5400,
# Min PostgreSQL connection pool size (per database) # Min PostgreSQL connection pool size (per database)
'min_pool_size': 0, "min_pool_size": 0,
# Max PostgreSQL connection pool size (per database) # Max PostgreSQL connection pool size (per database)
'max_pool_size': 4, "max_pool_size": 4,
# How long a connection can sit idle in the pool before it's removed (seconds) # How long a connection can sit idle in the pool before it's removed (seconds)
'max_idle_time': 30, "max_idle_time": 30,
# Log level for stderr logging # Log level for stderr logging
'log_level': 'error', "log_level": "error",
# Database user to connect as # Database user to connect as
'dbuser': 'postgres', "dbuser": "postgres",
# Database host # Database host
'dbhost': '/var/run/postgresql', "dbhost": "/var/run/postgresql",
# Database port # Database port
'dbport': 5432, "dbport": 5432,
# Default database to connect to when none is specified for a metric # Default database to connect to when none is specified for a metric
'dbname': 'postgres', "dbname": "postgres",
# Timeout for getting a connection slot from a pool # Timeout for getting a connection slot from a pool
'pool_slot_timeout': 5, "pool_slot_timeout": 5,
# PostgreSQL connection timeout (seconds) # PostgreSQL connection timeout (seconds)
# Note: It can actually be double this because of retries # Note: It can actually be double this because of retries
'connect_timeout': 5, "connect_timeout": 5,
# Time to wait before trying to reconnect again after a reconnect failure (seconds) # Time to wait before trying to reconnect again after a reconnect failure (seconds)
'reconnect_cooldown': 30, "reconnect_cooldown": 30,
# How often to check the version of PostgreSQL (seconds) # How often to check the version of PostgreSQL (seconds)
'version_check_period': 300, "version_check_period": 300,
# Metrics # Metrics
'metrics': {} "metrics": {},
} }
def update_deep(d1, d2): def update_deep(d1, d2):
""" """
Recursively update a dict, adding keys to dictionaries and appending to Recursively update a dict, adding keys to dictionaries and appending to
@@ -124,23 +125,32 @@ def update_deep(d1, d2):
The new d1 The new d1
""" """
if not isinstance(d1, dict) or not isinstance(d2, dict): if not isinstance(d1, dict) or not isinstance(d2, dict):
raise TypeError('Both arguments to update_deep need to be dictionaries') raise TypeError("Both arguments to update_deep need to be dictionaries")
for k, v2 in d2.items(): for k, v2 in d2.items():
if isinstance(v2, dict): if isinstance(v2, dict):
v1 = d1.get(k, {}) v1 = d1.get(k, {})
if not isinstance(v1, dict): if not isinstance(v1, dict):
raise TypeError('Type mismatch between dictionaries: {} is not a dict'.format(type(v1).__name__)) raise TypeError(
"Type mismatch between dictionaries: {} is not a dict".format(
type(v1).__name__
)
)
d1[k] = update_deep(v1, v2) d1[k] = update_deep(v1, v2)
elif isinstance(v2, list): elif isinstance(v2, list):
v1 = d1.get(k, []) v1 = d1.get(k, [])
if not isinstance(v1, list): if not isinstance(v1, list):
raise TypeError('Type mismatch between dictionaries: {} is not a list'.format(type(v1).__name__)) raise TypeError(
"Type mismatch between dictionaries: {} is not a list".format(
type(v1).__name__
)
)
d1[k] = v1 + v2 d1[k] = v1 + v2
else: else:
d1[k] = v2 d1[k] = v2
return d1 return d1
def read_config(path, included=False): def read_config(path, included=False):
""" """
Read a config file. Read a config file.
@@ -151,7 +161,7 @@ def read_config(path, included = False):
""" """
# Read config file # Read config file
log.info("Reading log file: {}".format(path)) log.info("Reading log file: {}".format(path))
with open(path, 'r') as f: with open(path, "r") as f:
try: try:
cfg = yaml.safe_load(f) cfg = yaml.safe_load(f)
except yaml.parser.ParserError as e: except yaml.parser.ParserError as e:
@@ -161,41 +171,52 @@ def read_config(path, included = False):
config_base = os.path.dirname(path) config_base = os.path.dirname(path)
# Read any external queries and validate metric definitions # Read any external queries and validate metric definitions
for name, metric in cfg.get('metrics', {}).items(): for name, metric in cfg.get("metrics", {}).items():
# Validate return types # Validate return types
try: try:
if metric['type'] not in ['value', 'row', 'column', 'set']: if metric["type"] not in ["value", "row", "column", "set"]:
raise ConfigError("Invalid return type: {} for metric {} in {}".format(metric['type'], name, path)) raise ConfigError(
"Invalid return type: {} for metric {} in {}".format(
metric["type"], name, path
)
)
except KeyError: except KeyError:
raise ConfigError("No type specified for metric {} in {}".format(name, path)) raise ConfigError(
"No type specified for metric {} in {}".format(name, path)
)
# Ensure queries exist # Ensure queries exist
query_dict = metric.get('query', {}) query_dict = metric.get("query", {})
if type(query_dict) is not dict: if type(query_dict) is not dict:
raise ConfigError("Query definition should be a dictionary, got: {} for metric {} in {}".format(query_dict, name, path)) raise ConfigError(
"Query definition should be a dictionary, got: {} for metric {} in {}".format(
query_dict, name, path
)
)
if len(query_dict) == 0: if len(query_dict) == 0:
raise ConfigError("Missing queries for metric {} in {}".format(name, path)) raise ConfigError("Missing queries for metric {} in {}".format(name, path))
# Read external sql files and validate version keys # Read external sql files and validate version keys
for vers, query in metric['query'].items(): for vers, query in metric["query"].items():
try: try:
int(vers) int(vers)
except: except:
raise ConfigError("Invalid version: {} for metric {} in {}".format(vers, name, path)) raise ConfigError(
"Invalid version: {} for metric {} in {}".format(vers, name, path)
)
if query.startswith('file:'): if query.startswith("file:"):
query_path = query[5:] query_path = query[5:]
if not query_path.startswith('/'): if not query_path.startswith("/"):
query_path = os.path.join(config_base, query_path) query_path = os.path.join(config_base, query_path)
with open(query_path, 'r') as f: with open(query_path, "r") as f:
metric['query'][vers] = f.read() metric["query"][vers] = f.read()
# Read any included config files # Read any included config files
for inc in cfg.get('include', []): for inc in cfg.get("include", []):
# Prefix relative paths with the directory from the current config # Prefix relative paths with the directory from the current config
if not inc.startswith('/'): if not inc.startswith("/"):
inc = os.path.join(config_base, inc) inc = os.path.join(config_base, inc)
update_deep(cfg, read_config(inc, included=True)) update_deep(cfg, read_config(inc, included=True))
@@ -209,19 +230,26 @@ def read_config(path, included = False):
update_deep(new_config, cfg) update_deep(new_config, cfg)
# Minor sanity checks # Minor sanity checks
if len(new_config['metrics']) == 0: if len(new_config["metrics"]) == 0:
log.error("No metrics are defined") log.error("No metrics are defined")
raise ConfigError("No metrics defined") raise ConfigError("No metrics defined")
# Validate the new log level before changing the config # Validate the new log level before changing the config
if new_config['log_level'].upper() not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: if new_config["log_level"].upper() not in [
raise ConfigError("Invalid log level: {}".format(new_config['log_level'])) "DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL",
]:
raise ConfigError("Invalid log level: {}".format(new_config["log_level"]))
global config global config
config = new_config config = new_config
# Apply changes to log level # Apply changes to log level
log.setLevel(logging.getLevelName(config['log_level'].upper())) log.setLevel(logging.getLevelName(config["log_level"].upper()))
def signal_handler(sig, frame): def signal_handler(sig, frame):
""" """
@@ -245,10 +273,11 @@ def signal_handler(sig, frame):
log.warning("Received config reload signal") log.warning("Received config reload signal")
read_config(config_file) read_config(config_file)
class ConnectionPool(ThreadedConnectionPool): class ConnectionPool(ThreadedConnectionPool):
def __init__(self, dbname, minconn, maxconn, *args, **kwargs): def __init__(self, dbname, minconn, maxconn, *args, **kwargs):
# Make sure dbname isn't different in the kwargs # Make sure dbname isn't different in the kwargs
kwargs['dbname'] = dbname kwargs["dbname"] = dbname
super().__init__(minconn, maxconn, *args, **kwargs) super().__init__(minconn, maxconn, *args, **kwargs)
self.name = dbname self.name = dbname
@@ -270,7 +299,10 @@ class ConnectionPool(ThreadedConnectionPool):
except psycopg2.pool.PoolError: except psycopg2.pool.PoolError:
# If we failed to get the connection slot, wait a bit and try again # If we failed to get the connection slot, wait a bit and try again
time.sleep(0.1) time.sleep(0.1)
raise TimeoutError("Timed out waiting for an available connection to {}".format(self.name)) raise TimeoutError(
"Timed out waiting for an available connection to {}".format(self.name)
)
def get_pool(dbname): def get_pool(dbname):
""" """
@@ -290,24 +322,29 @@ def get_pool(dbname):
log.info("Creating connection pool for: {}".format(dbname)) log.info("Creating connection pool for: {}".format(dbname))
connections[dbname] = ConnectionPool( connections[dbname] = ConnectionPool(
dbname, dbname,
int(config['min_pool_size']), int(config["min_pool_size"]),
int(config['max_pool_size']), int(config["max_pool_size"]),
application_name='pgmon', application_name="pgmon",
host=config['dbhost'], host=config["dbhost"],
port=config['dbport'], port=config["dbport"],
user=config['dbuser'], user=config["dbuser"],
connect_timeout=float(config['connect_timeout']), connect_timeout=int(config["connect_timeout"]),
sslmode='require') sslmode="require",
)
# Clear the unhappy indicator if present # Clear the unhappy indicator if present
unhappy_cooldown.pop(dbname, None) unhappy_cooldown.pop(dbname, None)
return connections[dbname] return connections[dbname]
def handle_connect_failure(pool): def handle_connect_failure(pool):
""" """
Mark the database as being unhappy so we can leave it alone for a while Mark the database as being unhappy so we can leave it alone for a while
""" """
dbname = pool.name dbname = pool.name
unhappy_cooldown[dbname] = datetime.now() + timedelta(seconds=int(config['reconnect_cooldown'])) unhappy_cooldown[dbname] = datetime.now() + timedelta(
seconds=int(config["reconnect_cooldown"])
)
def get_query(metric, version): def get_query(metric, version):
""" """
@@ -318,32 +355,34 @@ def get_query(metric, version):
version: The PostgreSQL version number, as given by server_version_num version: The PostgreSQL version number, as given by server_version_num
""" """
# Select the correct query # Select the correct query
for v in reversed(sorted(metric['query'].keys())): for v in reversed(sorted(metric["query"].keys())):
if version >= v: if version >= v:
if len(metric['query'][v].strip()) == 0: if len(metric["query"][v].strip()) == 0:
raise MetricVersionError("Metric no longer applies to PostgreSQL {}".format(version)) raise MetricVersionError(
return metric['query'][v] "Metric no longer applies to PostgreSQL {}".format(version)
)
return metric["query"][v]
raise MetricVersionError('Missing metric query for PostgreSQL {}'.format(version)) raise MetricVersionError("Missing metric query for PostgreSQL {}".format(version))
def run_query_no_retry(pool, return_type, query, args): def run_query_no_retry(pool, return_type, query, args):
""" """
Run the query with no explicit retry code Run the query with no explicit retry code
""" """
with pool.connection(float(config['connect_timeout'])) as conn: with pool.connection(float(config["connect_timeout"])) as conn:
try: try:
with conn.cursor(cursor_factory=DictCursor) as curs: with conn.cursor(cursor_factory=RealDictCursor) as curs:
curs.execute(query, args) curs.execute(query, args)
res = curs.fetchall() res = curs.fetchall()
if return_type == 'value': if return_type == "value":
return str(list(res[0].values())[0]) return str(list(res[0].values())[0])
elif return_type == 'row': elif return_type == "row":
return json.dumps(res[0]) return json.dumps(res[0])
elif return_type == 'column': elif return_type == "column":
return json.dumps([list(r.values())[0] for r in res]) return json.dumps([list(r.values())[0] for r in res])
elif return_type == 'set': elif return_type == "set":
return json.dumps(res) return json.dumps(res)
except: except:
dbname = pool.name dbname = pool.name
@@ -354,6 +393,7 @@ def run_query_no_retry(pool, return_type, query, args):
else: else:
raise raise
def run_query(pool, return_type, query, args): def run_query(pool, return_type, query, args):
""" """
Run the query, and if we find upon the first attempt that the connection Run the query, and if we find upon the first attempt that the connection
@@ -384,6 +424,7 @@ def run_query(pool, return_type, query, args):
handle_connect_failure(pool) handle_connect_failure(pool)
raise UnhappyDBError() raise UnhappyDBError()
def get_cluster_version(): def get_cluster_version():
""" """
Get the PostgreSQL version if we don't already know it, or if it's been Get the PostgreSQL version if we don't already know it, or if it's been
@@ -395,26 +436,43 @@ def get_cluster_version():
# If we don't know the version or it's past the recheck time, get the # If we don't know the version or it's past the recheck time, get the
# version from the database. Only one thread needs to do this, so they all # version from the database. Only one thread needs to do this, so they all
# try to grab the lock, and then make sure nobody else beat them to it. # try to grab the lock, and then make sure nobody else beat them to it.
if cluster_version is None or cluster_version_next_check is None or cluster_version_next_check < datetime.now(): if (
cluster_version is None
or cluster_version_next_check is None
or cluster_version_next_check < datetime.now()
):
with cluster_version_lock: with cluster_version_lock:
# Only check if nobody already got the version before us # Only check if nobody already got the version before us
if cluster_version is None or cluster_version_next_check is None or cluster_version_next_check < datetime.now(): if (
log.info('Checking PostgreSQL cluster version') cluster_version is None
pool = get_pool(config['dbname']) or cluster_version_next_check is None
cluster_version = int(run_query(pool, 'value', 'SHOW server_version_num', None)) or cluster_version_next_check < datetime.now()
cluster_version_next_check = datetime.now() + timedelta(seconds=int(config['version_check_period'])) ):
log.info("Checking PostgreSQL cluster version")
pool = get_pool(config["dbname"])
cluster_version = int(
run_query(pool, "value", "SHOW server_version_num", None)
)
cluster_version_next_check = datetime.now() + timedelta(
seconds=int(config["version_check_period"])
)
log.info("Got PostgreSQL cluster version: {}".format(cluster_version)) log.info("Got PostgreSQL cluster version: {}".format(cluster_version))
log.debug("Next PostgreSQL cluster version check will be after: {}".format(cluster_version_next_check)) log.debug(
"Next PostgreSQL cluster version check will be after: {}".format(
cluster_version_next_check
)
)
return cluster_version return cluster_version
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
""" """
This is our request handling server. It is responsible for listening for This is our request handling server. It is responsible for listening for
requests, processing them, and responding. requests, processing them, and responding.
""" """
def log_request(self, code='-', size='-'): def log_request(self, code="-", size="-"):
""" """
Override to suppress standard request logging Override to suppress standard request logging
""" """
@@ -436,10 +494,10 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
""" """
# Parse the URL # Parse the URL
parsed_path = urlparse(self.path) parsed_path = urlparse(self.path)
name = parsed_path.path.strip('/') name = parsed_path.path.strip("/")
parsed_query = parse_qs(parsed_path.query) parsed_query = parse_qs(parsed_path.query)
if name == 'agent_version': if name == "agent_version":
self._reply(200, VERSION) self._reply(200, VERSION)
return return
@@ -449,15 +507,15 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
# Get the metric definition # Get the metric definition
try: try:
metric = config['metrics'][name] metric = config["metrics"][name]
except KeyError: except KeyError:
log.error("Unknown metric: {}".format(name)) log.error("Unknown metric: {}".format(name))
self._reply(404, 'Unknown metric') self._reply(404, "Unknown metric")
return return
# Get the dbname. If none was provided, use the default from the # Get the dbname. If none was provided, use the default from the
# config. # config.
dbname = args.get('dbname', config['dbname']) dbname = args.get("dbname", config["dbname"])
# Get the connection pool for the database, or create one if it doesn't # Get the connection pool for the database, or create one if it doesn't
# already exist. # already exist.
@@ -465,7 +523,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
pool = get_pool(dbname) pool = get_pool(dbname)
except UnhappyDBError: except UnhappyDBError:
log.info("Database {} is unhappy, please be patient".format(dbname)) log.info("Database {} is unhappy, please be patient".format(dbname))
self._reply(503, 'Database unavailable') self._reply(503, "Database unavailable")
return return
# Identify the PostgreSQL version # Identify the PostgreSQL version
@@ -476,10 +534,10 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
except Exception as e: except Exception as e:
if dbname in unhappy_cooldown: if dbname in unhappy_cooldown:
log.info("Database {} is unhappy, please be patient".format(dbname)) log.info("Database {} is unhappy, please be patient".format(dbname))
self._reply(503, 'Database unavailable') self._reply(503, "Database unavailable")
else: else:
log.error("Failed to get PostgreSQL version: {}".format(e)) log.error("Failed to get PostgreSQL version: {}".format(e))
self._reply(500, 'Error getting DB version') self._reply(500, "Error getting DB version")
return return
# Get the query version # Get the query version
@@ -487,17 +545,17 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
query = get_query(metric, version) query = get_query(metric, version)
except KeyError: except KeyError:
log.error("Failed to find a version of {} for {}".format(name, version)) log.error("Failed to find a version of {} for {}".format(name, version))
self._reply(404, 'Unsupported version') self._reply(404, "Unsupported version")
return return
# Execute the quert # Execute the quert
try: try:
self._reply(200, run_query(pool, metric['type'], query, args)) self._reply(200, run_query(pool, metric["type"], query, args))
return return
except Exception as e: except Exception as e:
if dbname in unhappy_cooldown: if dbname in unhappy_cooldown:
log.info("Database {} is unhappy, please be patient".format(dbname)) log.info("Database {} is unhappy, please be patient".format(dbname))
self._reply(503, 'Database unavailable') self._reply(503, "Database unavailable")
else: else:
log.error("Error running query: {}".format(e)) log.error("Error running query: {}".format(e))
self._reply(500, "Error running query") self._reply(500, "Error running query")
@@ -508,19 +566,24 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
Send a reply to the client Send a reply to the client
""" """
self.send_response(code) self.send_response(code)
self.send_header('Content-type', 'application/json') self.send_header("Content-type", "application/json")
self.end_headers() self.end_headers()
self.wfile.write(bytes(content, 'utf-8')) self.wfile.write(bytes(content, "utf-8"))
if __name__ == '__main__':
if __name__ == "__main__":
# Handle cli args # Handle cli args
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog = 'pgmon', prog="pgmon", description="A PostgreSQL monitoring agent"
description='A PostgreSQL monitoring agent') )
parser.add_argument('config_file', default='pgmon.yml', nargs='?', parser.add_argument(
help='The config file to read (default: %(default)s)') "config_file",
default="pgmon.yml",
nargs="?",
help="The config file to read (default: %(default)s)",
)
args = parser.parse_args() args = parser.parse_args()
@@ -531,7 +594,7 @@ if __name__ == '__main__':
read_config(config_file) read_config(config_file)
# Set up the http server to receive requests # Set up the http server to receive requests
server_address = ('127.0.0.1', config['port']) server_address = ("127.0.0.1", config["port"])
httpd = ThreadingHTTPServer(server_address, SimpleHTTPRequestHandler) httpd = ThreadingHTTPServer(server_address, SimpleHTTPRequestHandler)
# Set up the signal handler # Set up the signal handler
@@ -539,7 +602,7 @@ if __name__ == '__main__':
signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGHUP, signal_handler)
# Handle requests. # Handle requests.
log.info("Listening on port {}...".format(config['port'])) log.info("Listening on port {}...".format(config["port"]))
while running: while running:
httpd.handle_request() httpd.handle_request()

View File

@@ -10,6 +10,7 @@ import pgmon
# Silence most logging output # Silence most logging output
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
class TestPgmonMethods(unittest.TestCase): class TestPgmonMethods(unittest.TestCase):
## ##
# update_deep # update_deep
@@ -22,103 +23,104 @@ class TestPgmonMethods(unittest.TestCase):
self.assertEqual(d1, {}) self.assertEqual(d1, {})
self.assertEqual(d2, {}) self.assertEqual(d2, {})
d1 = {'a': 1} d1 = {"a": 1}
d2 = {} d2 = {}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, { 'a': 1 }) self.assertEqual(d1, {"a": 1})
self.assertEqual(d2, {}) self.assertEqual(d2, {})
d1 = {} d1 = {}
d2 = {'a': 1} d2 = {"a": 1}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, { 'a': 1 }) self.assertEqual(d1, {"a": 1})
self.assertEqual(d2, d1) self.assertEqual(d2, d1)
def test_update_deep__scalars(self): def test_update_deep__scalars(self):
# Test adding/updating scalar values # Test adding/updating scalar values
d1 = {'foo': 1, 'bar': "text", 'hello': "world"} d1 = {"foo": 1, "bar": "text", "hello": "world"}
d2 = {'foo': 2, 'baz': "blah"} d2 = {"foo": 2, "baz": "blah"}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'foo': 2, 'bar': "text", 'baz': "blah", 'hello': "world"}) self.assertEqual(d1, {"foo": 2, "bar": "text", "baz": "blah", "hello": "world"})
self.assertEqual(d2, {'foo': 2, 'baz': "blah"}) self.assertEqual(d2, {"foo": 2, "baz": "blah"})
def test_update_deep__lists(self): def test_update_deep__lists(self):
# Test adding to lists # Test adding to lists
d1 = {'lst1': []} d1 = {"lst1": []}
d2 = {'lst1': [1, 2]} d2 = {"lst1": [1, 2]}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [1, 2]}) self.assertEqual(d1, {"lst1": [1, 2]})
self.assertEqual(d2, d1) self.assertEqual(d2, d1)
d1 = {'lst1': [1, 2]} d1 = {"lst1": [1, 2]}
d2 = {'lst1': []} d2 = {"lst1": []}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [1, 2]}) self.assertEqual(d1, {"lst1": [1, 2]})
self.assertEqual(d2, {'lst1': []}) self.assertEqual(d2, {"lst1": []})
d1 = {'lst1': [1, 2, 3]} d1 = {"lst1": [1, 2, 3]}
d2 = {'lst1': [3, 4]} d2 = {"lst1": [3, 4]}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [1, 2, 3, 3, 4]}) self.assertEqual(d1, {"lst1": [1, 2, 3, 3, 4]})
self.assertEqual(d2, {'lst1': [3, 4]}) self.assertEqual(d2, {"lst1": [3, 4]})
# Lists of objects # Lists of objects
d1 = {'lst1': [{'id': 1}, {'id': 2}, {'id': 3}]} d1 = {"lst1": [{"id": 1}, {"id": 2}, {"id": 3}]}
d2 = {'lst1': [{'id': 3}, {'id': 4}]} d2 = {"lst1": [{"id": 3}, {"id": 4}]}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'lst1': [{'id': 1}, {'id': 2}, {'id': 3}, {'id': 3}, {'id': 4}]}) self.assertEqual(
self.assertEqual(d2, {'lst1': [{'id': 3}, {'id': 4}]}) d1, {"lst1": [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 3}, {"id": 4}]}
)
self.assertEqual(d2, {"lst1": [{"id": 3}, {"id": 4}]})
# Nested lists # Nested lists
d1 = {'obj1': {'l1': [1, 2]}} d1 = {"obj1": {"l1": [1, 2]}}
d2 = {'obj1': {'l1': [3, 4]}} d2 = {"obj1": {"l1": [3, 4]}}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'l1': [1, 2, 3, 4]}}) self.assertEqual(d1, {"obj1": {"l1": [1, 2, 3, 4]}})
self.assertEqual(d2, {'obj1': {'l1': [3, 4]}}) self.assertEqual(d2, {"obj1": {"l1": [3, 4]}})
def test_update_deep__dicts(self): def test_update_deep__dicts(self):
# Test adding to lists # Test adding to lists
d1 = {'obj1': {}} d1 = {"obj1": {}}
d2 = {'obj1': {'a': 1, 'b': 2}} d2 = {"obj1": {"a": 1, "b": 2}}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'a': 1, 'b': 2}}) self.assertEqual(d1, {"obj1": {"a": 1, "b": 2}})
self.assertEqual(d2, d1) self.assertEqual(d2, d1)
d1 = {'obj1': {'a': 1, 'b': 2}} d1 = {"obj1": {"a": 1, "b": 2}}
d2 = {'obj1': {}} d2 = {"obj1": {}}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'a': 1, 'b': 2}}) self.assertEqual(d1, {"obj1": {"a": 1, "b": 2}})
self.assertEqual(d2, {'obj1': {}}) self.assertEqual(d2, {"obj1": {}})
d1 = {'obj1': {'a': 1, 'b': 2}} d1 = {"obj1": {"a": 1, "b": 2}}
d2 = {'obj1': {'a': 5, 'c': 12}} d2 = {"obj1": {"a": 5, "c": 12}}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'a': 5, 'b': 2, 'c': 12}}) self.assertEqual(d1, {"obj1": {"a": 5, "b": 2, "c": 12}})
self.assertEqual(d2, {'obj1': {'a': 5, 'c': 12}}) self.assertEqual(d2, {"obj1": {"a": 5, "c": 12}})
# Nested dicts # Nested dicts
d1 = {'obj1': {'d1': {'a': 1, 'b': 2}}} d1 = {"obj1": {"d1": {"a": 1, "b": 2}}}
d2 = {'obj1': {'d1': {'a': 5, 'c': 12}}} d2 = {"obj1": {"d1": {"a": 5, "c": 12}}}
pgmon.update_deep(d1, d2) pgmon.update_deep(d1, d2)
self.assertEqual(d1, {'obj1': {'d1': {'a': 5, 'b': 2, 'c': 12}}}) self.assertEqual(d1, {"obj1": {"d1": {"a": 5, "b": 2, "c": 12}}})
self.assertEqual(d2, {'obj1': {'d1': {'a': 5, 'c': 12}}}) self.assertEqual(d2, {"obj1": {"d1": {"a": 5, "c": 12}}})
def test_update_deep__types(self): def test_update_deep__types(self):
# Test mismatched types # Test mismatched types
d1 = {'foo': 5} d1 = {"foo": 5}
d2 = None d2 = None
self.assertRaises(TypeError, pgmon.update_deep, d1, d2) self.assertRaises(TypeError, pgmon.update_deep, d1, d2)
d1 = None d1 = None
d2 = {'foo': 5} d2 = {"foo": 5}
self.assertRaises(TypeError, pgmon.update_deep, d1, d2) self.assertRaises(TypeError, pgmon.update_deep, d1, d2)
# Nested mismatched types # Nested mismatched types
d1 = {'foo': [1, 2]} d1 = {"foo": [1, 2]}
d2 = {'foo': {'a': 7}} d2 = {"foo": {"a": 7}}
self.assertRaises(TypeError, pgmon.update_deep, d1, d2) self.assertRaises(TypeError, pgmon.update_deep, d1, d2)
## ##
# get_pool # get_pool
## ##
@@ -126,20 +128,19 @@ class TestPgmonMethods(unittest.TestCase):
def test_get_pool__simple(self): def test_get_pool__simple(self):
# Just get a pool in a normal case # Just get a pool in a normal case
pgmon.config.update(pgmon.default_config) pgmon.config.update(pgmon.default_config)
pool = pgmon.get_pool('postgres') pool = pgmon.get_pool("postgres")
self.assertIsNotNone(pool) self.assertIsNotNone(pool)
def test_get_pool__unhappy(self): def test_get_pool__unhappy(self):
# Test getting an unhappy database pool # Test getting an unhappy database pool
pgmon.config.update(pgmon.default_config) pgmon.config.update(pgmon.default_config)
pgmon.unhappy_cooldown['postgres'] = datetime.now() + timedelta(60) pgmon.unhappy_cooldown["postgres"] = datetime.now() + timedelta(60)
self.assertRaises(pgmon.UnhappyDBError, pgmon.get_pool, 'postgres') self.assertRaises(pgmon.UnhappyDBError, pgmon.get_pool, "postgres")
# Test getting a different database when there's an unhappy one # Test getting a different database when there's an unhappy one
pool = pgmon.get_pool('template0') pool = pgmon.get_pool("template0")
self.assertIsNotNone(pool) self.assertIsNotNone(pool)
## ##
# handle_connect_failure # handle_connect_failure
## ##
@@ -148,70 +149,44 @@ class TestPgmonMethods(unittest.TestCase):
# Test adding to an empty unhappy list # Test adding to an empty unhappy list
pgmon.config.update(pgmon.default_config) pgmon.config.update(pgmon.default_config)
pgmon.unhappy_cooldown = {} pgmon.unhappy_cooldown = {}
pool = pgmon.get_pool('postgres') pool = pgmon.get_pool("postgres")
pgmon.handle_connect_failure(pool) pgmon.handle_connect_failure(pool)
self.assertGreater(pgmon.unhappy_cooldown['postgres'], datetime.now()) self.assertGreater(pgmon.unhappy_cooldown["postgres"], datetime.now())
# Test adding another database # Test adding another database
pool = pgmon.get_pool('template0') pool = pgmon.get_pool("template0")
pgmon.handle_connect_failure(pool) pgmon.handle_connect_failure(pool)
self.assertGreater(pgmon.unhappy_cooldown['postgres'], datetime.now()) self.assertGreater(pgmon.unhappy_cooldown["postgres"], datetime.now())
self.assertGreater(pgmon.unhappy_cooldown['template0'], datetime.now()) self.assertGreater(pgmon.unhappy_cooldown["template0"], datetime.now())
self.assertEqual(len(pgmon.unhappy_cooldown), 2) self.assertEqual(len(pgmon.unhappy_cooldown), 2)
## ##
# get_query # get_query
## ##
def test_get_query__basic(self): def test_get_query__basic(self):
# Test getting a query with one version # Test getting a query with one version
metric = { metric = {"type": "value", "query": {0: "DEFAULT"}}
'type': 'value', self.assertEqual(pgmon.get_query(metric, 100000), "DEFAULT")
'query': {
0: 'DEFAULT'
}
}
self.assertEqual(pgmon.get_query(metric, 100000), 'DEFAULT')
def test_get_query__versions(self): def test_get_query__versions(self):
metric = { metric = {"type": "value", "query": {0: "DEFAULT", 110000: "NEW"}}
'type': 'value',
'query': {
0: 'DEFAULT',
110000: 'NEW'
}
}
# Test getting the default version of a query with no lower bound and a newer version # Test getting the default version of a query with no lower bound and a newer version
self.assertEqual(pgmon.get_query(metric, 100000), 'DEFAULT') self.assertEqual(pgmon.get_query(metric, 100000), "DEFAULT")
# Test getting the newer version of a query with no lower bound and a newer version for the newer version # Test getting the newer version of a query with no lower bound and a newer version for the newer version
self.assertEqual(pgmon.get_query(metric, 110000), 'NEW') self.assertEqual(pgmon.get_query(metric, 110000), "NEW")
# Test getting the newer version of a query with no lower bound and a newer version for an even newer version # Test getting the newer version of a query with no lower bound and a newer version for an even newer version
self.assertEqual(pgmon.get_query(metric, 160000), 'NEW') self.assertEqual(pgmon.get_query(metric, 160000), "NEW")
# Test getting a version in bwtween two other versions # Test getting a version in bwtween two other versions
metric = { metric = {"type": "value", "query": {0: "DEFAULT", 96000: "OLD", 110000: "NEW"}}
'type': 'value', self.assertEqual(pgmon.get_query(metric, 100000), "OLD")
'query': {
0: 'DEFAULT',
96000: 'OLD',
110000: 'NEW'
}
}
self.assertEqual(pgmon.get_query(metric, 100000), 'OLD')
def test_get_query__missing_version(self): def test_get_query__missing_version(self):
metric = { metric = {"type": "value", "query": {96000: "OLD", 110000: "NEW", 150000: ""}}
'type': 'value',
'query': {
96000: 'OLD',
110000: 'NEW',
150000: ''
}
}
# Test getting a metric that only exists for newer versions # Test getting a metric that only exists for newer versions
self.assertRaises(pgmon.MetricVersionError, pgmon.get_query, metric, 80000) self.assertRaises(pgmon.MetricVersionError, pgmon.get_query, metric, 80000)
@@ -219,7 +194,6 @@ class TestPgmonMethods(unittest.TestCase):
# Test getting a metric that only exists for older versions # Test getting a metric that only exists for older versions
self.assertRaises(pgmon.MetricVersionError, pgmon.get_query, metric, 160000) self.assertRaises(pgmon.MetricVersionError, pgmon.get_query, metric, 160000)
## ##
# read_config # read_config
## ##
@@ -229,27 +203,32 @@ class TestPgmonMethods(unittest.TestCase):
# Test reading just a metric and using the defaults for everything else # Test reading just a metric and using the defaults for everything else
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
# This is a comment! # This is a comment!
metrics: metrics:
test1: test1:
type: value type: value
query: query:
0: TEST1 0: TEST1
""") """
)
pgmon.read_config(f"{tmpdirname}/config.yml") pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['max_pool_size'], pgmon.default_config['max_pool_size']) self.assertEqual(
self.assertEqual(pgmon.config['dbuser'], pgmon.default_config['dbuser']) pgmon.config["max_pool_size"], pgmon.default_config["max_pool_size"]
)
self.assertEqual(pgmon.config["dbuser"], pgmon.default_config["dbuser"])
pgmon.config = {} pgmon.config = {}
# Test reading a basic config # Test reading a basic config
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
# This is a comment! # This is a comment!
min_pool_size: 1 min_pool_size: 1
max_pool_size: 2 max_pool_size: 2
@@ -280,22 +259,24 @@ metrics:
type: column type: column
query: query:
0: TEST4 0: TEST4
""") """
)
pgmon.read_config(f"{tmpdirname}/config.yml") pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['dbuser'], 'someone') self.assertEqual(pgmon.config["dbuser"], "someone")
self.assertEqual(pgmon.config['metrics']['test1']['type'], 'value') self.assertEqual(pgmon.config["metrics"]["test1"]["type"], "value")
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
self.assertEqual(pgmon.config['metrics']['test2']['query'][0], 'TEST2') self.assertEqual(pgmon.config["metrics"]["test2"]["query"][0], "TEST2")
def test_read_config__include(self): def test_read_config__include(self):
pgmon.config = {} pgmon.config = {}
# Test reading a config that includes other files (absolute and relative paths, multiple levels) # Test reading a config that includes other files (absolute and relative paths, multiple levels)
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write(f"""--- f.write(
f"""---
# This is a comment! # This is a comment!
min_pool_size: 1 min_pool_size: 1
max_pool_size: 2 max_pool_size: 2
@@ -308,18 +289,22 @@ version_check_period: 3600
include: include:
- dbsettings.yml - dbsettings.yml
- {tmpdirname}/metrics.yml - {tmpdirname}/metrics.yml
""") """
)
with open(f"{tmpdirname}/dbsettings.yml", 'w') as f: with open(f"{tmpdirname}/dbsettings.yml", "w") as f:
f.write(f"""--- f.write(
f"""---
dbuser: someone dbuser: someone
dbhost: localhost dbhost: localhost
dbport: 5555 dbport: 5555
dbname: template0 dbname: template0
""") """
)
with open(f"{tmpdirname}/metrics.yml", 'w') as f: with open(f"{tmpdirname}/metrics.yml", "w") as f:
f.write(f"""--- f.write(
f"""---
metrics: metrics:
test1: test1:
type: value type: value
@@ -331,31 +316,35 @@ metrics:
0: TEST2 0: TEST2
include: include:
- more_metrics.yml - more_metrics.yml
""") """
)
with open(f"{tmpdirname}/more_metrics.yml", 'w') as f: with open(f"{tmpdirname}/more_metrics.yml", "w") as f:
f.write(f"""--- f.write(
f"""---
metrics: metrics:
test3: test3:
type: value type: value
query: query:
0: TEST3 0: TEST3
""") """
)
pgmon.read_config(f"{tmpdirname}/config.yml") pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['max_idle_time'], 10) self.assertEqual(pgmon.config["max_idle_time"], 10)
self.assertEqual(pgmon.config['dbuser'], 'someone') self.assertEqual(pgmon.config["dbuser"], "someone")
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
self.assertEqual(pgmon.config['metrics']['test2']['query'][0], 'TEST2') self.assertEqual(pgmon.config["metrics"]["test2"]["query"][0], "TEST2")
self.assertEqual(pgmon.config['metrics']['test3']['query'][0], 'TEST3') self.assertEqual(pgmon.config["metrics"]["test3"]["query"][0], "TEST3")
def test_read_config__reload(self): def test_read_config__reload(self):
pgmon.config = {} pgmon.config = {}
# Test rereading a config to update an existing config # Test rereading a config to update an existing config
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
# This is a comment! # This is a comment!
min_pool_size: 1 min_pool_size: 1
max_pool_size: 2 max_pool_size: 2
@@ -378,15 +367,17 @@ metrics:
type: value type: value
query: query:
0: TEST2 0: TEST2
""") """
)
pgmon.read_config(f"{tmpdirname}/config.yml") pgmon.read_config(f"{tmpdirname}/config.yml")
# Just make sure the first config was read # Just make sure the first config was read
self.assertEqual(len(pgmon.config['metrics']), 2) self.assertEqual(len(pgmon.config["metrics"]), 2)
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
# This is a comment! # This is a comment!
min_pool_size: 7 min_pool_size: 7
metrics: metrics:
@@ -394,34 +385,39 @@ metrics:
type: value type: value
query: query:
0: NEW1 0: NEW1
""") """
)
pgmon.read_config(f"{tmpdirname}/config.yml") pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['min_pool_size'], 7) self.assertEqual(pgmon.config["min_pool_size"], 7)
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'NEW1') self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "NEW1")
self.assertEqual(len(pgmon.config['metrics']), 1) self.assertEqual(len(pgmon.config["metrics"]), 1)
def test_read_config__query_file(self): def test_read_config__query_file(self):
pgmon.config = {} pgmon.config = {}
# Read a config file that reads a query from a file # Read a config file that reads a query from a file
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
metrics: metrics:
test1: test1:
type: value type: value
query: query:
0: file:some_query.sql 0: file:some_query.sql
""") """
)
with open(f"{tmpdirname}/some_query.sql", 'w') as f: with open(f"{tmpdirname}/some_query.sql", "w") as f:
f.write("This is a query") f.write("This is a query")
pgmon.read_config(f"{tmpdirname}/config.yml") pgmon.read_config(f"{tmpdirname}/config.yml")
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'This is a query') self.assertEqual(
pgmon.config["metrics"]["test1"]["query"][0], "This is a query"
)
def test_read_config__invalid(self): def test_read_config__invalid(self):
pgmon.config = {} pgmon.config = {}
@@ -429,38 +425,47 @@ metrics:
# For all of these tests, we start with a valid config and also ensure that # For all of these tests, we start with a valid config and also ensure that
# it is not modified when a new config read fails # it is not modified when a new config read fails
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
metrics: metrics:
test1: test1:
type: value type: value
query: query:
0: TEST1 0: TEST1
""") """
)
pgmon.read_config(f"{tmpdirname}/config.yml") pgmon.read_config(f"{tmpdirname}/config.yml")
# Just make sure the config was read # Just make sure the config was read
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test reading a nonexistant config file # Test reading a nonexistant config file
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
self.assertRaises(FileNotFoundError, pgmon.read_config, f'{tmpdirname}/missing.yml') self.assertRaises(
FileNotFoundError, pgmon.read_config, f"{tmpdirname}/missing.yml"
)
# Test reading an invalid config file # Test reading an invalid config file
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""[default] f.write(
"""[default]
This looks a lot like an ini file to me This looks a lot like an ini file to me
Or maybe a TOML? Or maybe a TOML?
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertRaises(
pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
# Test reading a config that includes an invalid file # Test reading a config that includes an invalid file
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
@@ -469,15 +474,19 @@ metrics:
0: EVIL1 0: EVIL1
include: include:
- missing_file.yml - missing_file.yml
""") """
self.assertRaises(FileNotFoundError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') FileNotFoundError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test invalid log level # Test invalid log level
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
log_level: noisy log_level: noisy
dbuser: evil dbuser: evil
metrics: metrics:
@@ -485,132 +494,170 @@ metrics:
type: value type: value
query: query:
0: EVIL1 0: EVIL1
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test invalid query return type # Test invalid query return type
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
type: lots_of_data type: lots_of_data
query: query:
0: EVIL1 0: EVIL1
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test invalid query dict type # Test invalid query dict type
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
type: lots_of_data type: lots_of_data
query: EVIL1 query: EVIL1
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test incomplete metric: missing type # Test incomplete metric: missing type
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
query: query:
0: EVIL1 0: EVIL1
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test incomplete metric: missing queries # Test incomplete metric: missing queries
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
type: value type: value
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test incomplete metric: empty queries # Test incomplete metric: empty queries
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
type: value type: value
query: {} query: {}
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test incomplete metric: query dict is None # Test incomplete metric: query dict is None
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
type: value type: value
query: query:
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test reading a config with no metrics # Test reading a config with no metrics
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test reading a query defined in a file but the file is missing # Test reading a query defined in a file but the file is missing
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
type: value type: value
query: query:
0: file:missing.sql 0: file:missing.sql
""") """
self.assertRaises(FileNotFoundError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') FileNotFoundError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")
# Test invalid query versions # Test invalid query versions
with tempfile.TemporaryDirectory() as tmpdirname: with tempfile.TemporaryDirectory() as tmpdirname:
with open(f"{tmpdirname}/config.yml", 'w') as f: with open(f"{tmpdirname}/config.yml", "w") as f:
f.write("""--- f.write(
"""---
dbuser: evil dbuser: evil
metrics: metrics:
test1: test1:
type: value type: value
query: query:
default: EVIL1 default: EVIL1
""") """
self.assertRaises(pgmon.ConfigError, pgmon.read_config, f'{tmpdirname}/config.yml') )
self.assertEqual(pgmon.config['dbuser'], 'postgres') self.assertRaises(
self.assertEqual(pgmon.config['metrics']['test1']['query'][0], 'TEST1') pgmon.ConfigError, pgmon.read_config, f"{tmpdirname}/config.yml"
)
self.assertEqual(pgmon.config["dbuser"], "postgres")
self.assertEqual(pgmon.config["metrics"]["test1"]["query"][0], "TEST1")

94
tests/sql-tests.py Normal file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
from testcontainers import PostgresContainer
import requests
import yaml
import sys
pg_versions = [9.2, 9.6, 10, 11, 12, 13, 14, 15, 16, 17]
pgmon_port = 93849
tests = {}
container = None
def std_version(version):
if version[0] == "9":
return int(f"{version[0]}0{version[1]}00")
else:
return int(f"{version}0000")
def run_test(metric, params, status, check):
"""
Validate the return code and restults of a query
params:
metric: The name of the metric to test
params: A dictionary of query parameters to use when testing
status: The expected status code
check: A regular expression to validate the results (or None)
"""
result = requests.get(f"http://localhost:{pgmon_port}/{metric}", params=params)
if result.status_code != status:
print(
f"FAIL: {metric}[{params}] returned wrong status code: {result.status_code}"
)
return False
if re.match(check, result.text):
print(f"SUCCESS: {metric}[{params}]")
return True
else:
print(f"FAIL: {metric}[{params}] result is invalid, got:\n {result.text}")
return False
def run_all_tests(version):
"""
Run all defined tests against the current running instance
params:
version: The PostgreSQL version currently being tested (server_version_num format)
"""
errors = 0
# Convert versions like 12 to 120000
version_num = std_version(version)
# Loop through all of the metrics to test.
for metric in tests.keys():
params = metric.get("params", {})
status = 200
check = ""
# Find the correct version of the status and check parameters (assuming there are any).
# If there are any check conditions, find the highest version that does not exceed the version we're currently testing against.
# To do this, we order the keys (versions) in reverse, so we start with the highest.
for v in reversed(sorted(metric.get("expect", {}).keys())):
# If we've reached a version <= the one we're testing use it.
if int(v) <= version_num:
status = metric["expect"][v]["status"]
check = metric["expect"][v]["check"]
break
if not run_test(metric, metrics[metric].get(params, {}), status, check):
errors += 1
return errors
def start_test_db(version):
# container = PostgresContainer()
pass
# Read the test script
try:
with open("metric_tests.yml", "r") as f:
tests = yaml.safe_load(f)
except yaml.parser.ParserError as e:
sys.exit("Failed to parse metrics_test.yml: {e}")

View File

@@ -13,58 +13,58 @@ zabbix_export:
items: items:
- uuid: 763920af8da84db8a9a2667d9653cb21 - uuid: 763920af8da84db8a9a2667d9653cb21
name: 'PostgreSQL Agent Version' name: 'PostgreSQL Agent Version'
key: 'web.page.get[localhost,/agent_version,{$AGENT_PORT}]' type: HTTP_AGENT
key: 'pgmon[agent_version]'
delay: 1h delay: 1h
history: 90d history: 90d
value_type: TEXT value_type: TEXT
trends: '0'
description: 'PostgreSQL monitoring agent version number' description: 'PostgreSQL monitoring agent version number'
preprocessing: url: 'http://localhost:{$AGENT_PORT}/agent_version'
- type: REGEX
parameters:
- '\n\s?\n([\s\S]*)'
- \1
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
- uuid: 8706eccb7edc4fa394f552fc31f401a9 - uuid: 8706eccb7edc4fa394f552fc31f401a9
name: 'Max Frozen XID Age' name: 'Max Frozen XID Age'
key: 'web.page.get[localhost,/max_frozen_age,{$AGENT_PORT}]' type: HTTP_AGENT
key: 'pgmon[max_frozen_age]'
history: 90d history: 90d
trends: '0'
description: 'Maximum age of any frozen XID in any database' description: 'Maximum age of any frozen XID in any database'
preprocessing: preprocessing:
- type: REGEX
parameters:
- '\n\s?\n([\s\S]*)'
- \1
- type: MATCHES_REGEX - type: MATCHES_REGEX
parameters: parameters:
- '^[0-9]+$' - '^[0-9]+$'
url: 'http://localhost:{$AGENT_PORT}/max_frozen_age'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
- uuid: ee88f5f4d2384f97946d049af5af4502 - uuid: ee88f5f4d2384f97946d049af5af4502
name: 'PostgreSQL version' name: 'PostgreSQL version'
key: 'web.page.get[localhost,/version,{$AGENT_PORT}]' type: HTTP_AGENT
key: 'pgmon[version]'
delay: 1h delay: 1h
history: 90d history: 90d
description: 'PostgreSQL Server version number' description: 'PostgreSQL Server version number'
preprocessing: preprocessing:
- type: REGEX
parameters:
- '\n\s?\n([\s\S]*)'
- \1
- type: MATCHES_REGEX - type: MATCHES_REGEX
parameters: parameters:
- '^[0-9]+$' - '^[0-9]+$'
url: 'http://localhost:{$AGENT_PORT}/version'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
discovery_rules: discovery_rules:
- uuid: 085de335305e435dbb4439bd52e0d35d - uuid: 085de335305e435dbb4439bd52e0d35d
name: 'Discover Databases' name: 'Discover Databases'
key: 'web.page.get[localhost,/discover_dbs,{$AGENT_PORT}]' type: HTTP_AGENT
key: pgmon_discover_dbs
delay: 10m delay: 10m
filter:
conditions:
- macro: '{#DBNAME}'
value: ^template0$
operator: NOT_MATCHES_REGEX
formulaid: A
lifetime: 30d lifetime: 30d
enabled_lifetime_type: DISABLE_NEVER enabled_lifetime_type: DISABLE_NEVER
item_prototypes: item_prototypes:
@@ -72,6 +72,7 @@ zabbix_export:
name: 'Time spent executing statements on {#DBNAME}' name: 'Time spent executing statements on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[active_time,{#DBNAME}]' key: 'pgmon_db[active_time,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
value_type: FLOAT value_type: FLOAT
units: s units: s
@@ -84,7 +85,7 @@ zabbix_export:
parameters: parameters:
- '0.001' - '0.001'
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -94,6 +95,7 @@ zabbix_export:
name: 'Number of backends on {#DBNAME}' name: 'Number of backends on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[backends,{#DBNAME}]' key: 'pgmon_db[backends,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of backends currently connected to this database, or NULL for shared objects. This is the only column in this view that returns a value reflecting current state; all other columns return the accumulated values since the last reset.' description: 'Number of backends currently connected to this database, or NULL for shared objects. This is the only column in this view that returns a value reflecting current state; all other columns return the accumulated values since the last reset.'
preprocessing: preprocessing:
@@ -101,7 +103,7 @@ zabbix_export:
parameters: parameters:
- $.numbackends - $.numbackends
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -111,6 +113,7 @@ zabbix_export:
name: 'Blocks hit on {#DBNAME}' name: 'Blocks hit on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[blks_hit,{#DBNAME}]' key: 'pgmon_db[blks_hit,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of times disk blocks were found already in the buffer cache, so that a read was not necessary (this only includes hits in the PostgreSQL buffer cache, not the operating system''s file system cache)' description: 'Number of times disk blocks were found already in the buffer cache, so that a read was not necessary (this only includes hits in the PostgreSQL buffer cache, not the operating system''s file system cache)'
preprocessing: preprocessing:
@@ -118,7 +121,7 @@ zabbix_export:
parameters: parameters:
- $.blks_hit - $.blks_hit
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -128,6 +131,7 @@ zabbix_export:
name: 'Blocks read on {#DBNAME}' name: 'Blocks read on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[blks_read,{#DBNAME}]' key: 'pgmon_db[blks_read,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of disk blocks read in this database' description: 'Number of disk blocks read in this database'
preprocessing: preprocessing:
@@ -135,7 +139,7 @@ zabbix_export:
parameters: parameters:
- $.blks_read - $.blks_read
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -145,6 +149,7 @@ zabbix_export:
name: 'Time spent reading blocks on {#DBNAME}' name: 'Time spent reading blocks on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[blk_read_time,{#DBNAME}]' key: 'pgmon_db[blk_read_time,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
value_type: FLOAT value_type: FLOAT
units: s units: s
@@ -157,7 +162,7 @@ zabbix_export:
parameters: parameters:
- '0.001' - '0.001'
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -167,6 +172,7 @@ zabbix_export:
name: 'Time spent writing blocks on {#DBNAME}' name: 'Time spent writing blocks on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[blk_write_time,{#DBNAME}]' key: 'pgmon_db[blk_write_time,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
value_type: FLOAT value_type: FLOAT
units: s units: s
@@ -179,7 +185,7 @@ zabbix_export:
parameters: parameters:
- '0.001' - '0.001'
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -189,6 +195,7 @@ zabbix_export:
name: 'Total number of checksum failures on {#DBNAME}' name: 'Total number of checksum failures on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[checksum_failures,{#DBNAME}]' key: 'pgmon_db[checksum_failures,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of data page checksum failures detected in this database (or on a shared object), or NULL if data checksums are not enabled.' description: 'Number of data page checksum failures detected in this database (or on a shared object), or NULL if data checksums are not enabled.'
preprocessing: preprocessing:
@@ -201,7 +208,7 @@ zabbix_export:
error_handler: CUSTOM_VALUE error_handler: CUSTOM_VALUE
error_handler_params: '0' error_handler_params: '0'
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -211,6 +218,7 @@ zabbix_export:
name: 'Total number of conflicts on {#DBNAME}' name: 'Total number of conflicts on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[conflicts,{#DBNAME}]' key: 'pgmon_db[conflicts,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of queries canceled due to conflicts with recovery in this database. (Conflicts occur only on standby servers; see pg_stat_database_conflicts for details.)' description: 'Number of queries canceled due to conflicts with recovery in this database. (Conflicts occur only on standby servers; see pg_stat_database_conflicts for details.)'
preprocessing: preprocessing:
@@ -218,7 +226,7 @@ zabbix_export:
parameters: parameters:
- $.conflicts - $.conflicts
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -228,6 +236,7 @@ zabbix_export:
name: 'Total number of deadlocks on {#DBNAME}' name: 'Total number of deadlocks on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[deadlocks,{#DBNAME}]' key: 'pgmon_db[deadlocks,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of deadlocks detected in this database' description: 'Number of deadlocks detected in this database'
preprocessing: preprocessing:
@@ -235,7 +244,7 @@ zabbix_export:
parameters: parameters:
- $.deadlocks - $.deadlocks
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -245,6 +254,7 @@ zabbix_export:
name: 'Time spent in idle transactions on {#DBNAME}' name: 'Time spent in idle transactions on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[idle_in_transaction_time,{#DBNAME}]' key: 'pgmon_db[idle_in_transaction_time,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
value_type: FLOAT value_type: FLOAT
units: s units: s
@@ -257,7 +267,7 @@ zabbix_export:
parameters: parameters:
- '0.001' - '0.001'
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -267,6 +277,7 @@ zabbix_export:
name: 'Total number of sessions on {#DBNAME}' name: 'Total number of sessions on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[sessions,{#DBNAME}]' key: 'pgmon_db[sessions,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Total number of sessions established to this database' description: 'Total number of sessions established to this database'
preprocessing: preprocessing:
@@ -274,7 +285,7 @@ zabbix_export:
parameters: parameters:
- $.sessions - $.sessions
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -284,6 +295,7 @@ zabbix_export:
name: 'Total number of abandoned sessions on {#DBNAME}' name: 'Total number of abandoned sessions on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[sessions_abandoned,{#DBNAME}]' key: 'pgmon_db[sessions_abandoned,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of database sessions to this database that were terminated because connection to the client was lost' description: 'Number of database sessions to this database that were terminated because connection to the client was lost'
preprocessing: preprocessing:
@@ -291,7 +303,7 @@ zabbix_export:
parameters: parameters:
- $.sessions_abandoned - $.sessions_abandoned
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -301,6 +313,7 @@ zabbix_export:
name: 'Total number of fatal sessions on {#DBNAME}' name: 'Total number of fatal sessions on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[sessions_fatal,{#DBNAME}]' key: 'pgmon_db[sessions_fatal,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of database sessions to this database that were terminated by fatal errors' description: 'Number of database sessions to this database that were terminated by fatal errors'
preprocessing: preprocessing:
@@ -308,7 +321,7 @@ zabbix_export:
parameters: parameters:
- $.sessions_fatal - $.sessions_fatal
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -318,6 +331,7 @@ zabbix_export:
name: 'Total number of terminated sessions on {#DBNAME}' name: 'Total number of terminated sessions on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[sessions_killed,{#DBNAME}]' key: 'pgmon_db[sessions_killed,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of database sessions to this database that were terminated by operator intervention' description: 'Number of database sessions to this database that were terminated by operator intervention'
preprocessing: preprocessing:
@@ -325,7 +339,7 @@ zabbix_export:
parameters: parameters:
- $.sessions_killed - $.sessions_killed
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -335,6 +349,7 @@ zabbix_export:
name: 'Total temp file size on {#DBNAME}' name: 'Total temp file size on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[temp_bytes,{#DBNAME}]' key: 'pgmon_db[temp_bytes,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
units: b units: b
description: 'Total amount of data written to temporary files by queries in this database. All temporary files are counted, regardless of why the temporary file was created, and regardless of the log_temp_files setting.' description: 'Total amount of data written to temporary files by queries in this database. All temporary files are counted, regardless of why the temporary file was created, and regardless of the log_temp_files setting.'
@@ -343,7 +358,7 @@ zabbix_export:
parameters: parameters:
- $.temp_bytes - $.temp_bytes
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -353,6 +368,7 @@ zabbix_export:
name: 'Total number of temp files on {#DBNAME}' name: 'Total number of temp files on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[temp_files,{#DBNAME}]' key: 'pgmon_db[temp_files,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of temporary files created by queries in this database. All temporary files are counted, regardless of why the temporary file was created (e.g., sorting or hashing), and regardless of the log_temp_files setting.' description: 'Number of temporary files created by queries in this database. All temporary files are counted, regardless of why the temporary file was created (e.g., sorting or hashing), and regardless of the log_temp_files setting.'
preprocessing: preprocessing:
@@ -360,7 +376,7 @@ zabbix_export:
parameters: parameters:
- $.temp_files - $.temp_files
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -370,6 +386,7 @@ zabbix_export:
name: 'Tuples deleted on {#DBNAME}' name: 'Tuples deleted on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[tup_deleted,{#DBNAME}]' key: 'pgmon_db[tup_deleted,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of rows deleted by queries in this database' description: 'Number of rows deleted by queries in this database'
preprocessing: preprocessing:
@@ -377,7 +394,7 @@ zabbix_export:
parameters: parameters:
- $.tup_deleted - $.tup_deleted
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -387,6 +404,7 @@ zabbix_export:
name: 'Tuples fetched by index scans on {#DBNAME}' name: 'Tuples fetched by index scans on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[tup_fetched,{#DBNAME}]' key: 'pgmon_db[tup_fetched,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of live rows fetched by index scans in this database' description: 'Number of live rows fetched by index scans in this database'
preprocessing: preprocessing:
@@ -394,7 +412,7 @@ zabbix_export:
parameters: parameters:
- $.tup_fetched - $.tup_fetched
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -404,6 +422,7 @@ zabbix_export:
name: 'Tuples inserted on {#DBNAME}' name: 'Tuples inserted on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[tup_inserted,{#DBNAME}]' key: 'pgmon_db[tup_inserted,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of rows inserted by queries in this database' description: 'Number of rows inserted by queries in this database'
preprocessing: preprocessing:
@@ -411,7 +430,7 @@ zabbix_export:
parameters: parameters:
- $.tup_inserted - $.tup_inserted
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -421,6 +440,7 @@ zabbix_export:
name: 'Tuples returned by sequential scans on {#DBNAME}' name: 'Tuples returned by sequential scans on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[tup_returned,{#DBNAME}]' key: 'pgmon_db[tup_returned,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of live rows fetched by sequential scans and index entries returned by index scans in this database' description: 'Number of live rows fetched by sequential scans and index entries returned by index scans in this database'
preprocessing: preprocessing:
@@ -428,7 +448,7 @@ zabbix_export:
parameters: parameters:
- $.tup_returned - $.tup_returned
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -438,6 +458,7 @@ zabbix_export:
name: 'Tuples updated on {#DBNAME}' name: 'Tuples updated on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[tup_updated,{#DBNAME}]' key: 'pgmon_db[tup_updated,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of rows updated by queries in this database' description: 'Number of rows updated by queries in this database'
preprocessing: preprocessing:
@@ -445,7 +466,7 @@ zabbix_export:
parameters: parameters:
- $.tup_updated - $.tup_updated
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -455,6 +476,7 @@ zabbix_export:
name: 'Total number of commits on {#DBNAME}' name: 'Total number of commits on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[xact_commit,{#DBNAME}]' key: 'pgmon_db[xact_commit,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of transactions in this database that have been committed' description: 'Number of transactions in this database that have been committed'
preprocessing: preprocessing:
@@ -462,7 +484,7 @@ zabbix_export:
parameters: parameters:
- $.xact_commit - $.xact_commit
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -472,6 +494,7 @@ zabbix_export:
name: 'Total number of rollbacks on {#DBNAME}' name: 'Total number of rollbacks on {#DBNAME}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_db[xact_rollback,{#DBNAME}]' key: 'pgmon_db[xact_rollback,{#DBNAME}]'
delay: '0'
history: 90d history: 90d
description: 'Number of transactions in this database that have been rolled back' description: 'Number of transactions in this database that have been rolled back'
preprocessing: preprocessing:
@@ -479,7 +502,7 @@ zabbix_export:
parameters: parameters:
- $.xact_rollback - $.xact_rollback
master_item: master_item:
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' key: 'pgmon_db_stats[{#DBNAME}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -497,6 +520,8 @@ zabbix_export:
value: PostgreSQL value: PostgreSQL
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
- tag: Type
value: Calculated
- uuid: a81ba5a7c96d40bca9cc0861da574b49 - uuid: a81ba5a7c96d40bca9cc0861da574b49
name: 'Tuples fetched by index scans on {#DBNAME} - 1m delta' name: 'Tuples fetched by index scans on {#DBNAME} - 1m delta'
type: CALCULATED type: CALCULATED
@@ -508,6 +533,8 @@ zabbix_export:
value: PostgreSQL value: PostgreSQL
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
- tag: Type
value: Calculated
- uuid: 3bfa9b7db9394b6d9e0cac6255524f50 - uuid: 3bfa9b7db9394b6d9e0cac6255524f50
name: 'Tuples fetched by index scans on {#DBNAME} - 5m delta' name: 'Tuples fetched by index scans on {#DBNAME} - 5m delta'
type: CALCULATED type: CALCULATED
@@ -519,6 +546,8 @@ zabbix_export:
value: PostgreSQL value: PostgreSQL
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
- tag: Type
value: Calculated
- uuid: 261e83e1e87c42d587a9409a8a26f971 - uuid: 261e83e1e87c42d587a9409a8a26f971
name: 'Tuples returned by sequential scans on {#DBNAME} - 1h delta' name: 'Tuples returned by sequential scans on {#DBNAME} - 1h delta'
type: CALCULATED type: CALCULATED
@@ -531,6 +560,8 @@ zabbix_export:
value: PostgreSQL value: PostgreSQL
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
- tag: Type
value: Calculated
- uuid: e46c4ec2a21d48288337b90549fbf757 - uuid: e46c4ec2a21d48288337b90549fbf757
name: 'Tuples returned by sequential scans on {#DBNAME} - 1m delta' name: 'Tuples returned by sequential scans on {#DBNAME} - 1m delta'
type: CALCULATED type: CALCULATED
@@ -543,6 +574,8 @@ zabbix_export:
value: PostgreSQL value: PostgreSQL
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
- tag: Type
value: Calculated
- uuid: 26a42b55d7f949588f0739388ff52831 - uuid: 26a42b55d7f949588f0739388ff52831
name: 'Tuples returned by sequential scans on {#DBNAME} - 5m delta' name: 'Tuples returned by sequential scans on {#DBNAME} - 5m delta'
type: CALCULATED type: CALCULATED
@@ -555,24 +588,88 @@ zabbix_export:
value: PostgreSQL value: PostgreSQL
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
- tag: Type
value: Calculated
- uuid: 492b3cac15f348c2b85f97b69c114d1b - uuid: 492b3cac15f348c2b85f97b69c114d1b
name: 'Database Stats for {#DBNAME}' name: 'Database Stats for {#DBNAME}'
key: 'web.page.get[localhost,/db_stats?dbname={#DBNAME},{$AGENT_PORT}]' type: HTTP_AGENT
key: 'pgmon_db_stats[{#DBNAME}]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
preprocessing: trends: '0'
- type: REGEX url: 'http://localhost:{$AGENT_PORT}/db_stats'
parameters: query_fields:
- '\n\s?\n([\s\S]*)' - name: dbname
- \1 value: '{#DBNAME}'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
graph_prototypes: graph_prototypes:
- uuid: 1f7de43b77714f819e61c31273712b70
name: 'DML Totals for {#DBNAME}'
graph_items:
- color: 199C0D
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[tup_deleted,{#DBNAME}]'
- sortorder: '1'
color: F63100
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[tup_inserted,{#DBNAME}]'
- sortorder: '2'
color: 2774A4
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[tup_updated,{#DBNAME}]'
- uuid: aaec1de6f2314cbd875980fc1be3a2db
name: 'Sessions for {#DBNAME}'
graph_items:
- color: 199C0D
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[sessions_fatal,{#DBNAME}]'
- sortorder: '1'
color: F63100
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[xact_rollback,{#DBNAME}]'
- sortorder: '2'
color: 2774A4
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[sessions,{#DBNAME}]'
- sortorder: '3'
color: F7941D
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[sessions_killed,{#DBNAME}]'
- uuid: a08d60bd0ffb4f90b5411d30a057a85e
name: 'Temp Files for {#DBNAME}'
graph_items:
- color: 199C0D
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[temp_files,{#DBNAME}]'
- sortorder: '1'
color: F63100
yaxisside: RIGHT
calc_fnc: ALL
item:
host: 'PostgreSQL by pgmon'
key: 'pgmon_db[temp_bytes,{#DBNAME}]'
- uuid: 4949cdfbda614af796a2856fdfa9ac3f - uuid: 4949cdfbda614af796a2856fdfa9ac3f
name: 'Time breakdown for {#DBNAME} on {#CLUSTER}' name: 'Time breakdown for {#DBNAME}'
graph_items: graph_items:
- color: 199C0D - color: 199C0D
calc_fnc: ALL calc_fnc: ALL
@@ -597,17 +694,14 @@ zabbix_export:
item: item:
host: 'PostgreSQL by pgmon' host: 'PostgreSQL by pgmon'
key: 'pgmon_db[blk_write_time,{#DBNAME}]' key: 'pgmon_db[blk_write_time,{#DBNAME}]'
url: 'http://localhost:{$AGENT_PORT}/discover_dbs'
lld_macro_paths: lld_macro_paths:
- lld_macro: '{#DBNAME}' - lld_macro: '{#DBNAME}'
path: $.dbname path: $.dbname
preprocessing:
- type: REGEX
parameters:
- '\n\s?\n([\s\S]*)'
- \1
- uuid: 8ec029d577ae4872858e2e5cfd1cc40e - uuid: 8ec029d577ae4872858e2e5cfd1cc40e
name: 'Discover Replication' name: 'Discover Replication'
key: 'web.page.get[localhost,/discover_rep,{$AGENT_PORT}]' type: HTTP_AGENT
key: pgmon_discover_rep
delay: 10m delay: 10m
lifetime: 30d lifetime: 30d
enabled_lifetime_type: DISABLE_NEVER enabled_lifetime_type: DISABLE_NEVER
@@ -616,6 +710,7 @@ zabbix_export:
name: 'Flush lag for {#REPID}' name: 'Flush lag for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[flush_lag,repid={#REPID}]' key: 'pgmon_rep[flush_lag,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: FLOAT value_type: FLOAT
description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written and flushed it (but not yet applied it). This can be used to gauge the delay that synchronous_commit level on incurred while committing if this server was configured as a synchronous standby.' description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written and flushed it (but not yet applied it). This can be used to gauge the delay that synchronous_commit level on incurred while committing if this server was configured as a synchronous standby.'
@@ -624,7 +719,7 @@ zabbix_export:
parameters: parameters:
- $.flush_lag - $.flush_lag
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -636,15 +731,17 @@ zabbix_export:
name: 'Last flush LSN for {#REPID}' name: 'Last flush LSN for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[flush_lsn,repid={#REPID}]' key: 'pgmon_rep[flush_lsn,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: TEXT value_type: TEXT
trends: '0'
description: 'Last write-ahead log location flushed to disk by this standby server' description: 'Last write-ahead log location flushed to disk by this standby server'
preprocessing: preprocessing:
- type: JSONPATH - type: JSONPATH
parameters: parameters:
- $.flush_lsn - $.flush_lsn
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -656,6 +753,7 @@ zabbix_export:
name: 'Replay lag for {#REPID}' name: 'Replay lag for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[replay_lag,repid={#REPID}]' key: 'pgmon_rep[replay_lag,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: FLOAT value_type: FLOAT
description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written, flushed and applied it. This can be used to gauge the delay that synchronous_commit level remote_apply incurred while committing if this server was configured as a synchronous standby.' description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written, flushed and applied it. This can be used to gauge the delay that synchronous_commit level remote_apply incurred while committing if this server was configured as a synchronous standby.'
@@ -664,7 +762,7 @@ zabbix_export:
parameters: parameters:
- $.replay_lag - $.replay_lag
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -676,15 +774,17 @@ zabbix_export:
name: 'Last replay LSN for {#REPID}' name: 'Last replay LSN for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[replay_lsn,repid={#REPID}]' key: 'pgmon_rep[replay_lsn,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: TEXT value_type: TEXT
trends: '0'
description: 'Last write-ahead log location replayed into the database on this standby server' description: 'Last write-ahead log location replayed into the database on this standby server'
preprocessing: preprocessing:
- type: JSONPATH - type: JSONPATH
parameters: parameters:
- $.replay_lsn - $.replay_lsn
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -696,15 +796,17 @@ zabbix_export:
name: 'Last sent LSN for {#REPID}' name: 'Last sent LSN for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[sent_lsn,repid={#REPID}]' key: 'pgmon_rep[sent_lsn,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: TEXT value_type: TEXT
trends: '0'
description: 'Last write-ahead log location sent on this connection' description: 'Last write-ahead log location sent on this connection'
preprocessing: preprocessing:
- type: JSONPATH - type: JSONPATH
parameters: parameters:
- $.sent_lsn - $.sent_lsn
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -716,8 +818,10 @@ zabbix_export:
name: 'Replication state for {#REPID}' name: 'Replication state for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[state,repid={#REPID}]' key: 'pgmon_rep[state,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: TEXT value_type: TEXT
trends: '0'
description: | description: |
Current WAL sender state. Possible values are: Current WAL sender state. Possible values are:
* startup: This WAL sender is starting up. * startup: This WAL sender is starting up.
@@ -730,7 +834,7 @@ zabbix_export:
parameters: parameters:
- $.state - $.state
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -742,6 +846,7 @@ zabbix_export:
name: 'Write lag for {#REPID}' name: 'Write lag for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[write_lag,repid={#REPID}]' key: 'pgmon_rep[write_lag,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: FLOAT value_type: FLOAT
description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written it (but not yet flushed it or applied it). This can be used to gauge the delay that synchronous_commit level remote_write incurred while committing if this server was configured as a synchronous standby.' description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written it (but not yet flushed it or applied it). This can be used to gauge the delay that synchronous_commit level remote_write incurred while committing if this server was configured as a synchronous standby.'
@@ -750,7 +855,7 @@ zabbix_export:
parameters: parameters:
- $.write_lag - $.write_lag
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -762,15 +867,17 @@ zabbix_export:
name: 'Last write LSN for {#REPID}' name: 'Last write LSN for {#REPID}'
type: DEPENDENT type: DEPENDENT
key: 'pgmon_rep[write_lsn,repid={#REPID}]' key: 'pgmon_rep[write_lsn,repid={#REPID}]'
delay: '0'
history: 90d history: 90d
value_type: TEXT value_type: TEXT
trends: '0'
description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written it (but not yet flushed it or applied it). This can be used to gauge the delay that synchronous_commit level remote_write incurred while committing if this server was configured as a synchronous standby.' description: 'Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written it (but not yet flushed it or applied it). This can be used to gauge the delay that synchronous_commit level remote_write incurred while committing if this server was configured as a synchronous standby.'
preprocessing: preprocessing:
- type: JSONPATH - type: JSONPATH
parameters: parameters:
- $.write_lsn - $.write_lsn
master_item: master_item:
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' key: 'pgmon_rep_stats[{#REPID}]'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -780,14 +887,15 @@ zabbix_export:
value: '{#DBNAME}' value: '{#DBNAME}'
- uuid: efbe11f37c2f499488bdc5853c3d89e6 - uuid: efbe11f37c2f499488bdc5853c3d89e6
name: 'Replication Stats for {#REPID}' name: 'Replication Stats for {#REPID}'
key: 'web.page.get[localhost,/rep_stats?repid={#REPID},{$AGENT_PORT}]' type: HTTP_AGENT
key: 'pgmon_rep_stats[{#REPID}]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
preprocessing: trends: '0'
- type: REGEX url: 'http://localhost:{$AGENT_PORT}/rep_stats'
parameters: query_fields:
- '\n\s?\n([\s\S]*)' - name: repid
- \1 value: '{#REPID}'
tags: tags:
- tag: Application - tag: Application
value: PostgreSQL value: PostgreSQL
@@ -795,6 +903,7 @@ zabbix_export:
value: Replication value: Replication
- tag: Database - tag: Database
value: '{#DBNAME}' value: '{#DBNAME}'
url: 'http://localhost:{$AGENT_PORT}/discover_rep'
lld_macro_paths: lld_macro_paths:
- lld_macro: '{#CLIENT_ADDR}' - lld_macro: '{#CLIENT_ADDR}'
path: $.client_addr path: $.client_addr
@@ -802,12 +911,80 @@ zabbix_export:
path: $.repid path: $.repid
- lld_macro: '{#STATE}' - lld_macro: '{#STATE}'
path: $.state path: $.state
preprocessing:
- type: REGEX
parameters:
- '\n\s?\n([\s\S]*)'
- \1
macros: macros:
- macro: '{$AGENT_PORT}' - macro: '{$AGENT_PORT}'
value: '5400' value: '5400'
description: 'The port the agent listens on' description: 'The port the agent listens on'
dashboards:
- uuid: a818cfb97d654c75a3d70ae7f942bb89
name: 'PostgreSQL - Overview'
pages:
- widgets:
- type: graphprototype
name: 'Time Breakdown'
width: '39'
height: '5'
fields:
- type: INTEGER
name: columns
value: '1'
- type: GRAPH_PROTOTYPE
name: graphid.0
value:
host: 'PostgreSQL by pgmon'
name: 'Time breakdown for {#DBNAME}'
- type: STRING
name: reference
value: ZAWWD
- type: graphprototype
name: 'Session Breakdown'
'y': '5'
width: '39'
height: '6'
fields:
- type: INTEGER
name: columns
value: '1'
- type: GRAPH_PROTOTYPE
name: graphid.0
value:
host: 'PostgreSQL by pgmon'
name: 'Sessions for {#DBNAME}'
- type: STRING
name: reference
value: WBVFA
- type: graphprototype
name: 'Temp Files'
x: '39'
width: '33'
height: '5'
fields:
- type: INTEGER
name: columns
value: '1'
- type: GRAPH_PROTOTYPE
name: graphid.0
value:
host: 'PostgreSQL by pgmon'
name: 'Temp Files for {#DBNAME}'
- type: STRING
name: reference
value: CFQAN
- type: graphprototype
name: 'DML Totals'
x: '39'
'y': '5'
width: '33'
height: '6'
fields:
- type: INTEGER
name: columns
value: '1'
- type: GRAPH_PROTOTYPE
name: graphid.0
value:
host: 'PostgreSQL by pgmon'
name: 'DML Totals for {#DBNAME}'
- type: STRING
name: reference
value: XSCMZ