diff --git a/README.md b/README.md index 782a2ce..b8497ce 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ -Boundary PostgreSQL Plugin --------------------------- -**Awaiting Certification** +# Boundary PostgreSQL Plugin Extracts metrics from a PostgreSQL database instance. -### Prerequisites +## Prerequisites + +### Supported OS | OS | Linux | Windows | SmartOS | OS X | |:----------|:-----:|:-------:|:-------:|:----:| | Supported | v | v | v | v | +#### Boundary Meter Versions V4.0 or later + +- To install new meter go to Settings->Installation or [see instructons|https://help.boundary.com/hc/en-us/sections/200634331-Installation]. +- To upgrade the meter to the latest version - [see instructons|https://help.boundary.com/hc/en-us/articles/201573102-Upgrading-the-Boundary-Meter]. + +#### For Boundary Meter less than V4.0 | Runtime | node.js | Python | Java | |:---------|:-------:|:------:|:----:| @@ -20,6 +26,7 @@ Extracts metrics from a PostgreSQL database instance. ### Plugin Setup +#### For Boundary Meter Versions Less than V4.0 #### Installation of the PsycoPG2 Library Using `pip` 1. Install [pip](http://pip.readthedocs.org/en/latest/installing.html) if not already installed @@ -35,6 +42,9 @@ Extracts metrics from a PostgreSQL database instance. ``` ### Plugin Configuration Fields + +#### For All Versions + |Field Name|Description | |:-------|:-------------------------------------------------| |host |database host name or IP | @@ -46,6 +56,8 @@ Extracts metrics from a PostgreSQL database instance. ### Metrics Collected +#### For All Versions + |Metric Name |Description | |:----------------------------------------------|:----------------------------------------------| |PostgreSQL - Locks Exclusive |PostgreSQL - Locks Exclusive | diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..15a3f6c --- /dev/null +++ b/init.lua @@ -0,0 +1,85 @@ +local boundary = require('boundary') +local pginfo = require('pginfo') +local table = require('table') +local string = require('string') +local timer = require('timer') + +-- Default params +local connections = { +[0] = { + host = "localhost", + port = 5432, + user = "postgres", + password = "123qwe", + database = "postgres", + source = "", + pollInterval = 5000 +}} + +-- Fetching params +if (boundary.param ~= nil) then + connections = boundary.param or connections +end + +print("_bevent:Boundary LUA Postgres plugin up : version 1.0|t:info|tags:lua,plugin") + +local function getStats(conobj, pgcon, source) + conobj:get_bg_writer_stats(pgcon, function(writer_stats) + conobj:get_lock_stats_mode(pgcon, function(db_locks) + conobj:get_database_stats(pgcon, function(db_stats) + + --lock stats + p(string.format("POSTGRESQL_EXCLUSIVE_LOCKS %s %s", db_locks['all']['Exclusive'], source)) + p(string.format("POSTGRESQL_ROW_EXCLUSIVE_LOCKS %s %s", db_locks['all']['RowExclusive'], source)) + p(string.format("POSTGRESQL_SHARE_ROW_EXCLUSIVE_LOCKS %s %s", db_locks['all']['ShareRowExclusive'], source)) + p(string.format("POSTGRESQL_SHARE_UPDATE_EXCLUSIVE_LOCKS %s %s", db_locks['all']['ShareUpdateExclusive'], source)) + p(string.format("POSTGRESQL_SHARE_LOCKS %s %s", db_locks['all']['Share'], source)) + p(string.format("POSTGRESQL_ACCESS_SHARE_LOCKS %s %s", db_locks['all']['AccessShare'], source)) + + --checkpoint/bgwriter stats + p(string.format("POSTGRESQL_CHECKPOINT_WRITE_TIME %s %s", writer_stats['checkpoint_write_time'], source)) + p(string.format("POSTGRESQL_CHECKPOINTS_TIMED %s %s", writer_stats['checkpoints_timed'], source)) + p(string.format("POSTGRESQL_BUFFERS_ALLOCATED %s %s", writer_stats['buffers_alloc'], source)) + p(string.format("POSTGRESQL_BUFFERS_CLEAN %s %s", writer_stats['buffers_clean'], source)) + p(string.format("POSTGRESQL_BUFFERS_BACKEND_FSYNC %s %s", writer_stats['buffers_backend_fsync'], source)) + p(string.format("POSTGRESQL_CHECKPOINT_SYNCHRONIZATION_TIME %s %s", writer_stats['checkpoint_sync_time'], source)) + p(string.format("POSTGRESQL_CHECKPOINTS_REQUESTED %s %s", writer_stats['checkpoints_req'], source)) + p(string.format("POSTGRESQL_BUFFERS_BACKEND %s %s", writer_stats['buffers_backend'], source)) + p(string.format("POSTGRESQL_MAXIMUM_WRITTEN_CLEAN %s %s", writer_stats['maxwritten_clean'], source)) + p(string.format("POSTGRESQL_BUFFERS_CHECKPOINT %s %s", writer_stats['buffers_checkpoint'], source)) + + --Global DB Stats + p(string.format("POSTGRESQL_BLOCKS_READ %s %s", db_stats['totals']['blks_read'], source)) + p(string.format("POSTGRESQL_DISK_SIZE %s %s", db_stats['totals']['disk_size'], source)) + p(string.format("POSTGRESQL_TRANSACTIONS_COMMITTED %s %s", db_stats['totals']['xact_commit'], source)) + p(string.format("POSTGRESQL_TUPLES_DELETED %s %s", db_stats['totals']['tup_deleted'], source)) + p(string.format("POSTGRESQL_TRANSACTIONS_ROLLEDBACK %s %s", db_stats['totals']['xact_rollback'], source)) + p(string.format("POSTGRESQL_BLOCKS_HIT %s %s", db_stats['totals']['blks_hit'], source)) + p(string.format("POSTGRESQL_TUPLES_RETURNED %s %s", db_stats['totals']['tup_returned'], source)) + p(string.format("POSTGRESQL_TUPLES_FETCHED %s %s", db_stats['totals']['tup_fetched'], source)) + p(string.format("POSTGRESQL_TUPLES_UPDATED %s %s", db_stats['totals']['tup_updated'], source)) + p(string.format("POSTGRESQL_TUPLES_INSERTED %s %s", db_stats['totals']['tup_inserted'], source)) + p(string.format("POSTGRESQL_TUPLES_FETCHED %s %s", db_stats['totals']['tup_fetched'], source)) + + end) + end) + end) +end + +local function poll(connections) + if table.getn(connections) > 0 then + local query = connections[1] + local dbcon = pginfo:new(query.host, query.port, query.user, query.password, query.database, query.source) + table.remove(connections, 1) + + dbcon:establish(function(connection) + dbcon:get_databases(connection, function(dbs) + getStats(connection, query.source) + timer.setInterval(query.pollInterval, getStats, dbcon, connection, query.source) + poll(connections) + end) + end) + end +end + +poll(connections) diff --git a/modules/pginfo.lua b/modules/pginfo.lua new file mode 100644 index 0000000..5891e6f --- /dev/null +++ b/modules/pginfo.lua @@ -0,0 +1,220 @@ +-------------------------------------------------------------------------- +-- Module to extract Postgres Process Information for Boundary Lua Postgres Plugin +-- +-- Author: Yegor Dia +-- Email: yegordia at gmail.com +-- +-------------------------------------------------------------------------- + +local object = require('core').Object +local ffi = require("ffi") + +--[[ Check os for binding library path +]] +if ffi.os == "Windows" then + _G.POSTGRESQL_LIBRARY_PATH = ".\\windows-bindings\\lib\\x64\\pq" +end +postgresLuvit = require('postgresLuvit') + + +local function callIfNotNil(callback, ...) + if callback ~= nil then + callback(...) + end +end + +local function createDictWithKeys(keys, default_val) + local dict = {} + local default = nil + + if default_val ~= nil then + default = default_val + end + + for i, name in ipairs(keys) do + dict[name] = default + end + return dict +end + +local function createDictWithKeysAndValues(keys, values) + local dict = {} + + for i, name in ipairs(keys) do + dict[name] = values[i] + end + return dict +end + +local function deepcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[deepcopy(orig_key)] = deepcopy(orig_value) + end + setmetatable(copy, deepcopy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end + +local PgInfo = object:extend() + +--[[ Initialize PgInfo with connection parameters +]] +function PgInfo:initialize(host, port, user, pwd, database, source) + self.connection_string = string.format("host=%s port=%s dbname=%s user=%s password=%s", host, port, database, user, pwd) + --p(string.format("connection string: %s", self.connection_string)) + self.lock_modes = {'AccessExclusive', 'Exclusive', 'ShareRowExclusive', 'Share', 'ShareUpdateExclusive', 'RowExclusive', 'RowShare', 'AccessShare'} + self.connection = nil + return self +end + +--[[ Establishing method required to be used before every query +]] +function PgInfo:establish(queries_callback) + self.connection = postgresLuvit:new(self.connection_string, function(err) + assert(not err, err) + callIfNotNil(queries_callback, self.connection) + end) +end + +--[[ Test function +]] +function PgInfo:test(connection, callback) + connection:sendQuery("DROP TABLE IF EXISTS test",function(err, res) + assert(not err, err) + + connection:sendQuery("CREATE TABLE test (id bigserial primary key, content text, addedAt timestamp)",function(err, res) + assert(not err, err) + + connection:sendQuery("INSERT INTO test (content, addedAt) VALUES (" .. + connection:escape([["); DROP TABLE test; --]]) .. + ", now() )",function(err, res) + assert(not err, err) + + connection:sendQuery("SELECT * FROM test",function(err, res) + assert(not err, err) + callIfNotNil(callback) + end) + end) + end) + end) +end + +--[[ Get list of databases +]] +function PgInfo:get_databases(connection, callback) + connection:sendQuery("SELECT datname FROM pg_database;",function(err, res) + assert(not err, err) + local dbs = res + + callIfNotNil(callback, dbs) + end) +end + + +--[[ Returns the number of active lock discriminated by lock mode +]] +function PgInfo:get_lock_stats_mode(connection, callback) + connection:sendQuery("SELECT TRIM(mode, 'Lock'), granted, COUNT(*) FROM pg_locks " .. "GROUP BY TRIM(mode, 'Lock'), granted;", function(err, res) + assert(not err, err) + info_dict = { wait = createDictWithKeys(self.lock_modes, 0), all = createDictWithKeys(self.lock_modes, 0)} + + for i, value in ipairs(res) do + local mode = value[1] + local granted = value[2] == "t" + local count = value[3] + + info_dict['all'][mode] = info_dict['all'][mode] + count + if not granted then + info_dict['wait'][mode] = info_dict['wait'][mode] + count + end + end + + callIfNotNil(callback, info_dict) + end) +end + + +--[[ Global Background Writer and Checkpoint Activity stats +]] +function PgInfo:get_bg_writer_stats(connection, callback) + connection:sendQuery("SELECT * FROM pg_stat_bgwriter;", function(err, res) + assert(not err, err) + info_dict = {} + + for i=1, table.getn(res[0]) do + local name = res[0][i] + local result = res[1][i] + info_dict[name] = result + end + + callIfNotNil(callback, info_dict) + end) +end + +--[[ Returns database block read, transaction and tuple stats for each + database +]] +function PgInfo:get_database_stats(connection, callback) + local headers = {'datname', 'numbackends', 'xact_commit', 'xact_rollback', + 'blks_read', 'blks_hit', 'tup_returned', 'tup_fetched', + 'tup_inserted', 'tup_updated', 'tup_deleted', 'disk_size'} + local query_headers = deepcopy(headers) + table.remove(query_headers, table.getn(query_headers)) + + local query_string = string.format("SELECT %s, pg_database_size(datname) FROM pg_stat_database;", table.concat(query_headers, ", ")) + connection:sendQuery(query_string, function(err, res) + assert(not err, err) + info_dict = {} + info_dict['databases'] = self:create_stats_dict(headers, res) + info_dict['totals'] = self:create_totals_dict(headers, res) + + callIfNotNil(callback, info_dict) + end) +end + + +--[[ Utility method that returns database stats as a nested dictionary +]] +function PgInfo:create_stats_dict(headers, rows) + local db_stats = {} + table.remove(headers, 1) + for index, row in ipairs(rows) do + local key = row[1] + table.remove(row, 1) + local stats = createDictWithKeysAndValues(headers, row) + db_stats[key] = stats + end + return db_stats +end + + +--[[ Utility method that returns totals for database statistics +]] +function PgInfo:create_totals_dict(headers, rows) + local totals_dict = {} + local totals = {} + for key,row in ipairs(rows) do + local key = row[1] + + for i=1, table.getn(row) do + if totals[i] ~= nil then + totals[i] = totals[i] + row[i] + else + totals[i] = row[i] + end + end + end + + for index, header in ipairs(headers) do + totals_dict[header] = totals[index] + end + return totals_dict +end + +return PgInfo diff --git a/modules/postgresLuvit.lua b/modules/postgresLuvit.lua new file mode 100644 index 0000000..1de1b8b --- /dev/null +++ b/modules/postgresLuvit.lua @@ -0,0 +1,194 @@ +-------------------------------------------------------------------------- +-- This module is a luvit binding for the postgresql api. +-- +-- Copyright (C) 2012 Moritz Kühner, Germany. +-- Permission is hereby granted, free of charge, to any person obtaining +-- a copy of this software and associated documentation files (the +-- "Software"), to deal in the Software without restriction, including +-- without limitation the rights to use, copy, modify, merge, publish, +-- distribute, sublicense, and/or sell copies of the Software, and to +-- permit persons to whom the Software is furnished to do so, subject to +-- the following conditions: +-- +-- The above copyright notice and this permission notice shall be +-- included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------- + +local postgres = require("postgresffi") +local timer = require('timer') +local object = require('core').Object +local table = require("table") + + +--[[Loads the library at POSTGRESQL_LIBRARY_PATH if not nil + defaults to "/usr/lib/libpq.so.5". + + To use this set the global with a string pointing at the right + path befor requiring this library. +]] +postgres.init(POSTGRESQL_LIBRARY_PATH) + + +--Timeout for the poll timer +local POLLTIMER_INTERVALL = 5 + + +local LuvPostgres = object:extend() + +local function callIfNotNil(callback, ...) + if callback ~= nil then + callback(...) + end +end + +--[[Constructor the coninfo string is passed to PQconnectStart. +]] +function LuvPostgres:initialize(coninfo, callback) + self.con = postgres.newAsync(coninfo) + + --[[ This is an dirty hack to update the connection state. The correct + solution should watch the socket descriptor and update upon + network activity + ]] + self.watcher = timer.setInterval(POLLTIMER_INTERVALL, function() + local state = self.con:dialUpState() + if 0 == state then + timer.clearTimer(self.watcher) + self.established = true + callIfNotNil(callback) + elseif 1 == state then + timer.clearTimer(self.watcher) + callIfNotNil(callback, self.con:getError()) + end + end) +end + + +-- internal function to retrieve a fragment of the result +local function getFragment(connection) + local ok, ready = pcall(connection.readReady, connection) + + if not ok then + --error occured + return nil, ready + elseif not ready then + --no input ready + return {} + end + + local ok, result , status = pcall(connection.getAvailable, connection) + if not ok then + --error occured + return nil, ready + end + + local isOver = false + if status <= 7 then + --query is over + if connection:getAvailable() ~= nil then + --internal error occured + return nil, "Internal binding error. Query is not over!" + end + + if status == 5 or status == 7 then + --error occured + return nil, connection:getError() + end + isOver = true + end + + return result, isOver +end + +--[[Sends a query to the sql server + + Callback is called with error and the entire resultset. + Read postgresffi.getAvailable for a description of the format. +]] +function LuvPostgres:sendQuery(query, callback) + if not self.established then + callIfNotNil(callback, "Can't send query. Connection is not established!") + return + end + self.con:sendQuery(query) + + local resultAccu = {} + --[[ This is an dirty hack to update the connection state. The correct + solution should watch the socket descriptor and update upon + network activity + ]] + self.watcher = timer.setInterval(POLLTIMER_INTERVALL, function() + local result, over = getFragment(self.con) + + if not result then + timer.clearTimer(self.watcher) + callIfNotNil(callback, over) + else + local cntRow = #result + for i = 1, cntRow do + table.insert(resultAccu, result[i]) + end + + if over then + resultAccu[0] = result[0] + timer.clearTimer(self.watcher) + callIfNotNil(callback, nil, resultAccu) + end + end + end) +end + +--[[Sends a query to the sql server and returns intermediate results + + Callback is called with error and subset of the result and to mark the end a boolean true. + Read postgresffi.getAvailable for a description of the format. +]] +function LuvPostgres:sendQueryIntermediate(query, callback) + if not self.established then + callIfNotNil(callback, "Can't send query. Connection is not established!") + return + end + self.con:sendQuery(query) + + --[[ This is an dirty hack to update the connection state. The correct + solution should watch the socket descriptor and update upon + network activity + ]] + self.watcher = timer.setInterval(POLLTIMER_INTERVALL, function() + local result, over = getFragment(self.con) + if not result then + timer.clearTimer(self.watcher) + callIfNotNil(callback, over) + else + if over then + timer.clearTimer(self.watcher) + end + callIfNotNil(callback, nil, result, over) + end + end) +end + + +--[[Returns a escaped version of the string than can be savely + used in a query without danger of SQL injection or nil and + a message on error +]] +function LuvPostgres:escape(query) + local ok, value = pcall(self.con.escape, self.con, query) + if ok then + return value + end + return nil, value +end + + + +return LuvPostgres diff --git a/modules/postgresffi.lua b/modules/postgresffi.lua new file mode 100644 index 0000000..613a30d --- /dev/null +++ b/modules/postgresffi.lua @@ -0,0 +1,288 @@ +-------------------------------------------------------------------------- +-- This module is a luajit ffi binding for the postgresql api +-- with a spacial emphasis on the non blocking functions. +-- +-- Copyright (C) 2012 Moritz Kühner, Germany. +-- Permission is hereby granted, free of charge, to any person obtaining +-- a copy of this software and associated documentation files (the +-- "Software"), to deal in the Software without restriction, including +-- without limitation the rights to use, copy, modify, merge, publish, +-- distribute, sublicense, and/or sell copies of the Software, and to +-- permit persons to whom the Software is furnished to do so, subject to +-- the following conditions: +-- +-- The above copyright notice and this permission notice shall be +-- included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------- + +PSqlConnection = {} +local PSqlConnection_mt = { __index = PSqlConnection } + +local ffi = require("ffi") +local libpq = nil + +--definitions of the c api +--se http://www.postgresql.org/docs/9.2/static/libpq-async.html +ffi.cdef[[ +typedef struct PGconn_s PGconn; +typedef struct PGresult_s PGresult; + +typedef enum +{ + PGRES_POLLING_FAILED = 0, + PGRES_POLLING_READING, /* These two indicate that one may */ + PGRES_POLLING_WRITING, /* use select before polling again. */ + PGRES_POLLING_OK +} PostgresPollingStatusType; + +typedef enum +{ + PGRES_EMPTY_QUERY = 0, /* empty query string was executed */ + PGRES_COMMAND_OK, /* a query command that doesn't return + * anything was executed properly by the + * backend */ + PGRES_TUPLES_OK, /* a query command that returns tuples was + * executed properly by the backend, PGresult + * contains the result tuples */ + PGRES_COPY_OUT, /* Copy Out data transfer in progress */ + PGRES_COPY_IN, /* Copy In data transfer in progress */ + PGRES_BAD_RESPONSE, /* an unexpected response was recv'd from the + * backend */ + PGRES_NONFATAL_ERROR, /* notice or warning message */ + PGRES_FATAL_ERROR, /* query failed */ + PGRES_COPY_BOTH, /* Copy In/Out data transfer in progress */ + PGRES_SINGLE_TUPLE /* single tuple from larger resultset */ +} ExecStatusType; + +typedef enum +{ + CONNECTION_OK, + CONNECTION_BAD, + /* Non-blocking mode only below here */ + + /* + * The existence of these should never be relied upon - they should only + * be used for user feedback or similar purposes. + */ + CONNECTION_STARTED, /* Waiting for connection to be made. */ + CONNECTION_MADE, /* Connection OK; waiting to send. */ + CONNECTION_AWAITING_RESPONSE, /* Waiting for a response from the + * postmaster. */ + CONNECTION_AUTH_OK, /* Received authentication; waiting for + * backend startup. */ + CONNECTION_SETENV, /* Negotiating environment. */ + CONNECTION_SSL_STARTUP, /* Negotiating SSL. */ + CONNECTION_NEEDED /* Internal state: connect() needed */ +} ConnStatusType; + +PGconn *PQconnectStart(const char *conninfo); + +//Returns the status of the connection. +ConnStatusType PQstatus(const PGconn *conn); + +PostgresPollingStatusType PQconnectPoll(PGconn *conn); + +int PQsetnonblocking(PGconn *conn, int arg); + +int PQsendQuery(PGconn *conn, const char *command); + +char *PQerrorMessage(const PGconn *conn); + +int PQconsumeInput(PGconn *conn); + +int PQisBusy(PGconn *conn); + +PGresult *PQgetResult(PGconn *conn); + +ExecStatusType PQresultStatus(const PGresult *res); + +//Returns the number of rows (tuples) in the query result. +int PQntuples(const PGresult *res); + +//Returns the number of columns (fields) in each row of the query result. +int PQnfields(const PGresult *res); + +//Returns the column name associated with the given column number. Column numbers start at 0. +//The caller should not free the result directly. +//It will be freed when the associated PGresult handle is passed to PQclear. +char *PQfname(const PGresult *res, int column_number); + +//Returns a single field value of one row of a PGresult. +//Row and column numbers start at 0. The caller should not +//free the result directly. It will be freed when the associated +//PGresult handle is passed to PQclear. +char *PQgetvalue(const PGresult *res, int row_number, int column_number); + +//Tests a field for a null value. Row and column numbers start at 0. +int PQgetisnull(const PGresult *res, int row_number, int column_number); + +//Returns the actual length of a field value in bytes. Row and column numbers start at 0. +int PQgetlength(const PGresult *res, int row_number, int column_number); + +//Returns the actual length of a field value in bytes. Row and column numbers start at 0. +int PQgetlength(const PGresult *res, int row_number, int column_number); + +//PQescapeLiteral escapes a string for use within an SQL command. +//This memory should be freed using PQfreemem() when the result is no longer needed. +char *PQescapeLiteral(PGconn *conn, const char *str, size_t length); + +void PQfreemem(void *ptr); + +//Frees the storage associated with a PGresult. +void PQclear(PGresult *res); + +//Closes the connection to the server. Also frees memory used by the PGconn object. +void PQfinish(PGconn *conn); + +//returns the filedescriptor for this connection +int PQsocket(PGconn *conn); +]] + + +--[[Loads the shared library of postgres + libpath is the path to the library (ie. the .so or .dll) of postgres + defaults to "/usr/lib/libpq.so.5" if omitted +]] + + +function PSqlConnection.init(libpath) + libpq = ffi.load(libpath or "/usr/lib/libpq.so.5", true) +end + + +--[[Creates a new connection to a Postgres SQL in + async mode. The string conninfo is passed to + PQconnectStart. +]] +function PSqlConnection.newAsync(conninfo) + local new_inst = {} + setmetatable( new_inst, PSqlConnection_mt ) + assert(conninfo) + new_inst.PGconn = ffi.gc(libpq.PQconnectStart(conninfo), libpq.PQfinish) + return new_inst +end + + +--[[Returns the dialUpState +]] +function PSqlConnection:dialUpState() + assert(libpq.PQconnectPoll(self.PGconn) ~= "PGRES_POLLING_FAILED", self:getError()) + return libpq.PQstatus(self.PGconn) +end + + +--[[Returns the last errormessage +]] +function PSqlConnection:getError() + return ffi.string(libpq.PQerrorMessage(self.PGconn)) +end + + +--[[Sends a query to the server + All old data has to be collecet befor an new + query can be send (getAvailable returns nil) + + Node: Multiple querys in one will be treated as one transaction. + read PostgreSQL doc 31.3.1 +]] +function PSqlConnection:sendQuery(query) + assert(not self.queryInProcess, "Error: Old query isn't finished") + local ret = libpq.PQsendQuery(self.PGconn, query) + if ret == 0 then + error(self:getError()) + end + self.queryInProcess = true +end + + +--[[Returns true if getAvailable can be called without + blocking +]] +function PSqlConnection:readReady() + local ret = libpq.PQconsumeInput(self.PGconn) + if ret == 0 then + error(self:getError()) + end + return libpq.PQisBusy(self.PGconn) == 0 +end + + +--[[Returns a tables of all avilable data or nil if no more data is availible + The table is structured with the column names in row 0 and the data afterwards + ie. [1][1] is the first row in the first column and [0][1] is the name of that column + | 1 | 2 | ... +------------------------------------------- + 0 | column name 1 | column name 2 | ... +-------------------------------------------- + 1 | data column 1 | data ...... +]] +function PSqlConnection:getAvailable() + local result = libpq.PQgetResult(self.PGconn) + if result ~= nil then + local status = libpq.PQresultStatus(result) + local rows = libpq.PQntuples(result) + local columns = libpq.PQnfields(result) + + local tab = {} + tab[0] = {} + + for j = 1, columns do + tab[0][j] = ffi.string(libpq.PQfname(result, j-1)) + end + + for i = 1, rows do + tab[i] = {} + + for j = 1, columns do + local value = nil + if 0 == libpq.PQgetisnull(result, i-1, j-1) then + value = ffi.string( + libpq.PQgetvalue(result, i-1, j-1), + libpq.PQgetlength(result, i-1, j-1) + ) + end + + tab[i][j] = value + end + end + + libpq.PQclear(result) + + return tab, status + else + self.queryInProcess = nil + return nil + end +end + +--[[Returns the socketdescriptor +]] +function PSqlConnection:getSocket() + return libpq.PQsocket(self.PGconn) +end + + +--[[Returns a escaped version of the string than can be savely + used in a query without danger of SQL injection +]] +function PSqlConnection:escape(data) + local strData = tostring(data) + local result = libpq.PQescapeLiteral(self.PGconn, strData, #strData) + if result ~= nil then + local strResult = ffi.string(result) + libpq.PQfreemem(result) + return strResult + else + error(self:getError()) + end +end + +return PSqlConnection \ No newline at end of file diff --git a/param.json b/param.json new file mode 100644 index 0000000..1cab3fc --- /dev/null +++ b/param.json @@ -0,0 +1,8 @@ +{ + "host":"localhost", + "port":5432, + "user":"postgres", + "pwd":"123qwe", + "database":"postgres", + "source":"" +} diff --git a/plugin.json b/plugin.json index 61a72c3..d36d46c 100644 --- a/plugin.json +++ b/plugin.json @@ -1,8 +1,12 @@ { - "description" : "Awaiting Certification - Collects statistics from PostgreSQL Database", - "command" : "python postgres_plugin.py $(host) $(port) $(database) $(username) $(password) $(source)", - "icon" : "icon.png", - "metrics" : [ + "description" : "Awaiting Certification - Collects statistics from PostgreSQL Database", + "icon" : "icon.png", + "command" : "python postgres_plugin.py", + "command_lua" : "boundary-meter init.lua", + "postExtract_lua" : "", + "ignore" : "node_modules", + + "metrics" : [ "POSTGRESQL_EXCLUSIVE_LOCKS", "POSTGRESQL_ROW_EXCLUSIVE_LOCKS", "POSTGRESQL_SHARE_ROW_EXCLUSIVE_LOCKS", @@ -30,12 +34,15 @@ "POSTGRESQL_TUPLES_RETURNED", "POSTGRESQL_TUPLES_UPDATED" ], + "dashboards": [ { "name":"PostgreSQL Lock Stats", "layout": "d-w=3&d-h=2&d-pad=5&d-bg=none&d-g-POSTGRESQL_EXCLUSIVE_LOCKS=0-0-1-1&d-g-POSTGRESQL_ROW_EXCLUSIVE_LOCKS=0-1-1-1&d-g-POSTGRESQL_SHARE_ROW_EXCLUSIVE=1-1-1-1&d-g-POSTGRESQL_SHARE_UPDATE_EXCLUSIVE=2-1-1-1&d-g-POSTGRESQL_SHARE_LOCKS=2-0-1-1&d-g-POSTGRESQL_ACCESS_SHARE_LOCKS=1-0-1-1" }, { "name":"PostgreSQL Reader Stats", "layout": "d-w=3&d-h=3&d-pad=5&d-bg=none&d-g-POSTGRESQL_CHECKPOINT_WRITE_TIME=0-0-1-1&d-g-POSTGRESQL_CHECKPOINTS_TIMED=0-1-1-1&d-g-POSTGRESQL_BUFFERS_ALLOCATED=0-2-1-1&d-g-POSTGRESQL_BUFFERS_CLEAN=1-0-1-1&d-g-POSTGRESQL_BUFFERS_BACKEND_FSYNC=1-1-1-1&d-g-POSTGRESQL_BUFFERS_BACKEND=1-2-1-1&d-g-POSTGRESQL_CHECKPOINTS_REQUESTED=2-2-1-1&d-g-POSTGRESQL_CHECKPOINT_SYNC_TIME=2-1-1-1&d-g-POSTGRESQL_BUFFERS_CHECKPOINT=2-0-1-1" }, { "name":"PostgreSQL DB Stats", "layout": "d-w=3&d-h=4&d-pad=5&d-bg=none&d-g-POSTGRESQL_BUFFERS_CHECKPOINT=0-0-1-1&d-g-POSTGRESQL_BLOCKS_READ=1-0-1-1&d-g-POSTGRESQL_DISK_SIZE=2-0-1-1&d-g-POSTGRESQL_TRANSACTIONS_COMMITTED=0-1-1-1&d-g-POSTGRESQL_TUPLES_DELETED=1-1-1-1&d-g-POSTGRESQL_TRANSACTIONS_ROLLEDBACK=2-1-1-1&d-g-POSTGRESQL_BLOCKS_HIT=0-2-1-1&d-g-POSTGRESQL_TUPLES_RETURNED=1-2-1-1&d-g-POSTGRESQL_TUPLES_FETCHED=2-2-1-1&d-g-POSTGRESQL_TUPLES_UPDATED=0-3-1-1&d-g-POSTGRESQL_TUPLES_INSERTED=2-3-1-1" } ], + + "paramArray" : { "itemTitle" : ["source"], "schemaTitle" : "Database"}, "paramSchema" : [ { "title" : "host", diff --git a/postgres_plugin.py b/postgres_plugin.py index a5a3493..6823db5 100755 --- a/postgres_plugin.py +++ b/postgres_plugin.py @@ -3,18 +3,24 @@ import json from postgresql import PgInfo from os import path +import time -if (len(sys.argv) != 7): - sys.stderr.write("usage: {0} host port database user password source\n".format(path.basename(sys.argv[0]))) - sys.exit(1) +#get params from file +paramFile="param.json" -#grab connection info's. This is expected in the Format host/port/db/user/pwd -_host = sys.argv[1] -_port = sys.argv[2] -_database = sys.argv[3] -_user = sys.argv[4] -_password = sys.argv[5] -_source = sys.argv[6] +if path.isfile(paramFile): + with open(paramFile) as data_file: + params = json.load(data_file) + _host = params["host"] + _port = params["port"] + _database = params["database"] + _user = params["username"] + _password = params["password"] + _source = params["source"] + +else: + sys.stderr.write("Either params file is missing or is not readable") + sys.exit(1) def poll(): _dbconn = PgInfo(_host, _port, _database, _user, _password) @@ -61,9 +67,9 @@ def poll(): print("POSTGRESQL_TUPLES_FETCHED {0} {1}".format(dbStats['totals']['tup_fetched'], _source)) print("POSTGRESQL_TUPLES_UPDATED {0} {1}".format(dbStats['totals']['tup_updated'], _source)) print("POSTGRESQL_TUPLES_INSERTED {0} {1}".format(dbStats['totals']['tup_inserted'], _source)) - print("POSTGRESQL_TUPLES_FETCHED {0} {1}".format(dbStats['totals']['tup_fetched'], _source)) -poll() - - + sys.stdout.flush() +while True: + poll() + time.sleep(1) diff --git a/windows-bindings/lib/x64/pq.dll b/windows-bindings/lib/x64/pq.dll new file mode 100644 index 0000000..311c7c7 Binary files /dev/null and b/windows-bindings/lib/x64/pq.dll differ diff --git a/windows-bindings/lib/x86/pq.dll b/windows-bindings/lib/x86/pq.dll new file mode 100644 index 0000000..7d665e7 Binary files /dev/null and b/windows-bindings/lib/x86/pq.dll differ