Source code for ruxit_postgresql

"""
OneAgent PostgreSQL plugin
==========================

Gathers PostgreSQL stats.
The full description of stats is described at https://www.postgresql.org/docs/current/static/monitoring-stats.html

Plugin collects subset of:

* database level metrics available at *pg_stat_database* view. Only non-template databases are taken into account (those that have *datistemplate* set to *false*)

* aggregated table level metrics read from *pg_stat_all_tables*

This is a singleton type plugin - one instance will be created even if multiple PostgreSQL process
groups instances are detected. To enable plugin following configuration options need to be specified:

* user credentials - role with *md5* authentication method should be provided
* port - access to the PostgreSQL via localhost interface is required
* database name

"""

import collections
from ruxit.api.data import PluginMeasurement, PluginProperty
from ruxit.api.base_plugin import BasePlugin
from ruxit.api.exceptions import AuthException, ConfigException
from ruxit.api.selectors import ListenPortSelector
from contextlib import closing
from contextlib import suppress

import pg8000
import logging

from pg8000.core import NULL_BYTE, RESPONSE_CODE, InterfaceError, ProgrammingError

DatabasePerSecondStats = collections.namedtuple("DatabasePerSecondStats", ['tup_inserted', 'tup_updated', 'tup_deleted',
                                                                           'xact_commit', 'xact_rollback', 'blks_hit',
                                                                           'blks_read'])
"""
List of metrics that are collected on database level as per second stats:
"""

TablePerSecondStats = collections.namedtuple("TablePerSecondStats", ["idx_tup_fetch", "seq_tup_read", "idx_scan"])
"""
List of metrics that are collected on table level as per second stats:
"""

log = logging.getLogger(__name__)


[docs]def handle_ERROR_RESPONSE_patch(self, data, ps): """ Patch for handle_ERROR_RESPONSE function from pg8000 package ensuring that plugin will not crash while decoding error response in the situation when proper encoding is not set yet. """ responses = tuple( (s[0:1], s[1:].decode(self._client_encoding, "ignore")) for s in data.split(NULL_BYTE)) msg_dict = dict(responses) if msg_dict[RESPONSE_CODE] == "28000": self.error = InterfaceError("md5 password authentication failed") else: self.error = ProgrammingError(*tuple(v for k, v in responses))
pg8000.Connection.handle_ERROR_RESPONSE = handle_ERROR_RESPONSE_patch
[docs]class PostgresqlPlugin(BasePlugin): """ PostgreSQL plugin class. This plugin is stateful - initialize method is defined in order to maintain one connection to database engine for the lifetime of the plugin. """ postgres_auth_errors = ['28P01'] """ PostgreSQL authentication error code """ postgres_auth_msgs = ['md5 password authentication failed', 'server requesting password authentication, but no password was provided', 'server requesting MD5 password authentication, but no password was provided' ] """ List of messages returned by PostgreSQL that raises authorization exception if encountered while initializing database connection """
[docs] def initialize(self, **kwargs): """ Plugin initialization is limited to parsing configuration. """ config = kwargs['config'] self.process_types = kwargs["activation_context"].value.process_types if config['port'] == '': raise ConfigException('"Port" field cannot be empty') self.port = config['port'] if config['database'] == '': raise ConfigException('"Database name" field cannot be empty') self.database = config['database'] if 'auth_user' in config and 'auth_password' in config: self.user = config['auth_user'] self.password = config['auth_password'] else: raise ConfigException('"User" or "Password" field is not present in configuration') self.databases = {}
def _createpm(self, *args, **kwargs): return PluginMeasurement(*args, entity_selector=ListenPortSelector(self.port, self.process_types), **kwargs)
[docs] def get_databases(self, cursor): """ Collects and stores list of existing database. """ cursor.execute('SELECT datname FROM pg_database WHERE datistemplate = false') self.databases = {row[0] for row in cursor.fetchall()}
[docs] def get_database_level_statistics(self, cursor): """ Performs query that selects requested database metrics and sends gathered results. """ cursor.execute('SELECT datname,numbackends,tup_inserted,tup_updated,tup_deleted,xact_commit,xact_rollback,' 'blks_hit,blks_read FROM pg_stat_database') results = cursor.fetchall() for row in results: # numbackends++ for requested db datname = row[0] numbackends = row[1] stats = DatabasePerSecondStats(*row[2:]) if datname in self.databases: dim = {'database': datname} for field in stats._fields: self.results_builder.add_per_second_result(self._createpm( dimensions=dim, key=field, value=getattr(stats, field)) ) self.results_builder.add_absolute_result(self._createpm( dimensions=dim, key='numbackends', value=numbackends) ) with suppress(ZeroDivisionError): cache_hit_ratio = (100*stats.blks_hit/(stats.blks_read+stats.blks_hit)) # calculated cache hit ratio self.results_builder.add_absolute_result(self._createpm( dimensions=dim, key='cache_hit_ratio', value=cache_hit_ratio) )
[docs] def get_table_level_statistics(self): """ Method that tries connecting to every database and gets its stats. """ for database in self.databases: try: with closing(pg8000.connect( user=self.user, password=self.password, database=database, port=int(self.port))) as conn, \ closing(conn.cursor()) as cursor: cursor.execute('SELECT SUM(idx_tup_fetch),SUM(seq_tup_read),SUM(idx_scan) FROM pg_stat_all_tables') results = cursor.fetchall() stats = TablePerSecondStats(*results[0]) dim = {'database': database} for field in stats._fields: self.results_builder.add_per_second_result( self._createpm(dimensions=dim, key=field, value=getattr(stats, field)) ) except: log.info(self, exc_info=1)
[docs] def query(self, **kwargs): """ Main method that connects to PostgreSQL using configuration from global variable. Raises: ruxit.api.exceptions.AuthException: PostgreSQL responded with 28P01 ruxit.api.exceptions.ConfigException: connection and data parsing errors """ try: with closing(pg8000.connect( user=self.user, password=self.password, database=self.database, port=int(self.port))) as conn, \ closing(conn.cursor()) as cursor: self.get_databases(cursor) self.get_database_level_statistics(cursor) except pg8000.ProgrammingError as ex: args_len = len(ex.args) if args_len > 1 and ex.args[1] in self.postgres_auth_errors: raise AuthException from ex elif args_len > 2: raise ConfigException(ex.args[2]) from ex else: raise ConfigException(ex.args[0]) from ex except pg8000.InterfaceError as ex: if ex.args[0] in self.postgres_auth_msgs: raise AuthException from ex else: raise ConfigException(ex.args[0]) from ex self.get_table_level_statistics()