├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── actions.js ├── clientFactory.js ├── stats.js └── stats │ ├── initMysqlVars.js │ ├── ioMetrics.js │ ├── refreshGlobalStatusMetrics.js │ └── refreshProcessListMetrics.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.rdb 4 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Keymetrics Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | PM2 module to monitor a MySQL server with Keymetrics 4 | 5 | ## Install 6 | 7 | `pm2 install pm2-mysql` 8 | 9 | ## Requirements 10 | 11 | This module requires a MySQL install (tested against v5.6). Optionally, to monitor the DB disk Read/Write you will need `iostat` located in the `sysstat` package. 12 | 13 | ### Slow Queries 14 | 15 | To be able to display Slow Queries you first need to activate slow_query_log in MySQL via : 16 | Edit /etc/mysql/my.cnf and add : 17 | slow_query_log = '1' 18 | 19 | and you need to set the log file path to a file your pm2 user has read access to : 20 | slow_query_log_file = '/var/log/mysql/slow-queries.log' 21 | 22 | ### General Query Log 23 | 24 | To be able to display the last queries, you need to enable the General Query Log via : 25 | general_log = '1' 26 | 27 | and you need to set the log file path to a file your pm2 user has read access to : 28 | general_log_file = '/var/log/mysql/general.log' 29 | 30 | 31 | ## Configure 32 | 33 | - `host` (Defaults to `localhost`) : Set the hostname/ip of your mysql server 34 | - `port` (Defaults to `3306`): Set the port of your mysql server 35 | - `user` (Defaults to `root`): Set the user of your mysql server 36 | - `password` (Defaults to `none`): Set the password of your mysql server 37 | - `refreshRate` (Defaults to 1000): Set the stats refresh rate (in milliseconds) 38 | - `dbDiskName` (Defaults to "sda"): Sets the name of the disk used by the database (used to fetch io stats) 39 | - `slowQueriesLog` (Defaults to "/var/log/mysql/slow-queries.log"): log file to fetch slow queries from 40 | - `generalLog` (Defaults to "/var/log/mysql/general.log"): log file to fetch last queries from 41 | - `lastQueriesSize` (Defaults to 100): number of lines to fetch from general log for last queries display 42 | - `errorLog` (Defaults to "/var/log/mysql/error.log"): error log file 43 | - `lastErrorsSize` (Defaults to 100): number of lines to fetch from error log for last errors display 44 | 45 | #### How to set these values ? 46 | 47 | After having installed the module you have to type : 48 | `pm2 set pm2-mysql: ` 49 | 50 | e.g: 51 | - `pm2 set pm2-mysql:port 3307` (set the mysql port to 3307) 52 | - `pm2 set pm2-mysql:password keppo` (use `keppo` as password for your mysql server) 53 | 54 | ## Uninstall 55 | 56 | `pm2 uninstall pm2-mysql` 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keymetrics Team. All rights reserved. 3 | * Use of this source code is governed by a license that 4 | * can be found in the LICENSE file. 5 | */ 6 | 7 | var pmx = require('pmx'); 8 | var mysqlClientFactory = require('./lib/clientFactory.js'); 9 | var mysqlStats = require('./lib/stats.js'); 10 | var mysqlActions = require('./lib/actions.js'); 11 | 12 | // Initialize the module 13 | pmx.initModule({ 14 | 15 | pid : pmx.resolvePidPaths(['/var/run/mysqld/mysqld.pid', 16 | '/var/run/mysql/mysql.pid', 17 | '/var/run/mysql.pid', 18 | '/var/run/mysqld.pid']), 19 | 20 | widget : { 21 | 22 | // Module display type. Currently only 'generic' is available 23 | type : 'generic', 24 | 25 | // Logo to be displayed on the top left block 26 | // Must be https 27 | logo : 'https://upload.wikimedia.org/wikipedia/en/thumb/6/62/MySQL.svg/1280px-MySQL.svg.png', 28 | 29 | // 0 = main element 30 | // 1 = secondary 31 | // 2 = main border 32 | // 3 = secondary border 33 | // 4 = text color (not implemented yet) 34 | theme : ['#262E35', '#015A84', '#F79618', '#F79618'], 35 | 36 | // Toggle horizontal blocks above main widget 37 | el : { 38 | probes : true, 39 | actions: true 40 | }, 41 | 42 | block : { 43 | // Display remote action block 44 | actions : false, 45 | 46 | // Display CPU / Memory 47 | cpu : true, 48 | mem : true, 49 | 50 | // Issues count display 51 | issues : true, 52 | 53 | // Display meta block 54 | meta : true, 55 | 56 | // Display metadata about the probe (restart nb, interpreter...) 57 | meta_block : false, 58 | 59 | // Name of custom metrics to be displayed as a "major metrics" 60 | main_probes : ['questions/s', 'Open Tables', 'Total Processes', 'IO Read kb/s', 'Row Lock Waits', '% Max Used Connections'] 61 | }, 62 | }, 63 | 64 | // Status (in the future, not implemented yet) 65 | status_check : ['latency', 'event loop', 'query/s'] 66 | //= Status Green / Yellow / Red (maybe for probes?) 67 | }, function(err, conf) { 68 | var mysqlClient = mysqlClientFactory.build(conf); 69 | 70 | // Init metrics refresh loop 71 | mysqlStats.init(mysqlClient); 72 | 73 | // Init actions 74 | mysqlActions.init(mysqlClient); 75 | }); 76 | -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | var pmx = require('pmx'); 2 | var shelljs = require('shelljs'); 3 | var shellescape = require('shell-escape'); 4 | 5 | function init(mysqlClient) { 6 | var conf = pmx.getConf(); 7 | 8 | // Show Slow Queries 9 | pmx.action('Slow Queries', function (reply) { 10 | var args = ['mysqldumpslow', conf.slowQueriesLog]; 11 | shelljs.exec(shellescape(args), {async: true, silent: true}, function (err, out) { 12 | if (err) { 13 | return reply("Couldn't get slow queries: " + err); 14 | } 15 | 16 | reply(out); 17 | }); 18 | }); 19 | 20 | // Last Queries 21 | pmx.action('Last Queries', function (reply) { 22 | var args = ['tail', '-n', conf.lastQueriesSize, conf.generalLog]; 23 | shelljs.exec(shellescape(args), {async: true, silent: true}, function (err, out) { 24 | if (err) { 25 | return reply("Couldn't get last queries: " + err); 26 | } 27 | 28 | reply(out); 29 | }); 30 | }); 31 | 32 | // Last Errors 33 | pmx.action('Last Errors', function (reply) { 34 | var args = ['tail', '-n', conf.lastErrorsSize, conf.errorLog]; 35 | shelljs.exec(shellescape(args), {async: true, silent: true}, function (err, out) { 36 | if (err) { 37 | return reply("Couldn't get last errors: " + err); 38 | } 39 | 40 | reply(out); 41 | }); 42 | }); 43 | 44 | // List Databases 45 | pmx.action('List DBs', function (reply) { 46 | mysqlClient.query("SHOW DATABASES;", function (err, rows) { 47 | if (err) { 48 | return reply("Couldn't list databases: " + err); 49 | } 50 | 51 | var output = ""; 52 | rows.forEach(function (row) { 53 | output += row.Database + "\r\n"; 54 | }); 55 | reply(output); 56 | }); 57 | }); 58 | 59 | } 60 | 61 | module.exports.init = init; -------------------------------------------------------------------------------- /lib/clientFactory.js: -------------------------------------------------------------------------------- 1 | var mysql = require('mysql'); 2 | var pmx = require('pmx'); 3 | 4 | function build(conf) { 5 | var mysqlClient = {}; 6 | 7 | var pool = mysql.createPool({ 8 | connectionLimit: 10, 9 | host: conf.host, 10 | port: conf.port, 11 | user: conf.user, 12 | password: conf.password.toString() 13 | }); 14 | 15 | mysqlClient.query = function (queryString, cb) { 16 | pool.getConnection(function (err, connection) { 17 | if (err) { 18 | return cb(err); 19 | } 20 | 21 | connection.query(queryString, function (err, rows, fields) { 22 | connection.release(); // And done with the connection. 23 | if (err) { 24 | return cb(err); 25 | } 26 | 27 | cb(null, rows, fields); 28 | }); 29 | }); 30 | }; 31 | 32 | return mysqlClient; 33 | } 34 | 35 | module.exports.build = build; 36 | -------------------------------------------------------------------------------- /lib/stats.js: -------------------------------------------------------------------------------- 1 | var pmx = require('pmx'); 2 | 3 | var refreshProcessListMetrics = require('./stats/refreshProcessListMetrics'); 4 | var refreshGlobalStatusMetrics = require('./stats/refreshGlobalStatusMetrics'); 5 | 6 | var initMysqlVars = require('./stats/initMysqlVars'); 7 | var ioMetrics = require('./stats/ioMetrics'); 8 | 9 | var metrics = {}; 10 | var probe = pmx.probe(); 11 | 12 | // Init metrics with default values 13 | function initMetrics() { 14 | metrics.con = new probe.metric({ 15 | name: 'connections/s', 16 | value: 0 17 | }); 18 | metrics.lastcon = 'N/A'; 19 | 20 | metrics.bsent = new probe.metric({ 21 | name: 'Bytes Sent/s', 22 | value: 0 23 | }); 24 | metrics.lastbsent = 'N/A'; 25 | 26 | metrics.rread = new probe.metric({ 27 | name: 'Rows Read/s', 28 | value: 0 29 | }); 30 | metrics.lastrread = 'N/A'; 31 | 32 | metrics.totalProcesses = new probe.metric({ 33 | name: 'Total Processes', 34 | value: 'N/A' 35 | }); 36 | 37 | metrics.quest = new probe.metric({ 38 | name: 'questions/s', 39 | value: 0 40 | }); 41 | metrics.lastquest = 'N/A'; 42 | 43 | metrics.threadsRunning = new probe.metric({ 44 | name: 'Threads Running', 45 | value: 'N/A' 46 | }); 47 | 48 | metrics.preads = new probe.metric({ 49 | name: 'Pending Reads', 50 | value: 'N/A' 51 | }); 52 | 53 | metrics.openFiles = new probe.metric({ 54 | name: 'Open Files', 55 | value: 'N/A' 56 | }); 57 | 58 | metrics.poolRead = new probe.metric({ 59 | name: 'Buffer Read', 60 | value: 'N/A' 61 | }); 62 | 63 | metrics.uptime = new probe.metric({ 64 | name: 'Uptime', 65 | value: 'N/A', 66 | alert: { 67 | mode: 'threshold-avg', 68 | value: 180, 69 | msg: 'Too many Server Restarts', 70 | cmp: "<", 71 | interval: 600 72 | } 73 | }); 74 | 75 | metrics.threadsConnected = new probe.metric({ 76 | name: 'Threads Connected', 77 | value: 'N/A', 78 | alert: { 79 | mode: 'threshold-avg', 80 | value: 0, 81 | msg: 'No Threads Connected', 82 | cmp: "=", 83 | interval: 60 84 | } 85 | }); 86 | 87 | metrics.version = new probe.metric({ 88 | name: 'MySQL Version', 89 | value: 'N/A' 90 | }); 91 | 92 | metrics.maxUsedConnections = new probe.metric({ 93 | name: 'Max Used Connections', 94 | value: 'N/A' 95 | }); 96 | metrics.maxConnections = new probe.metric({ 97 | name: 'Max Connections', 98 | value: 'N/A' 99 | }); 100 | metrics.maxUsedConnectionsPerc = new probe.metric({ 101 | name: '% Max Used Connections', 102 | value: 'N/A', 103 | alert: { 104 | mode: 'threshold-avg', 105 | value: 85, 106 | msg: 'Connections could run out soon', 107 | cmp: ">", 108 | interval: 60 109 | } 110 | }); 111 | 112 | metrics.abortedConnects = new probe.metric({ 113 | name: 'Aborted_connects', 114 | value: 'N/A', 115 | alert: { 116 | mode: 'threshold-avg', 117 | value: 50, 118 | msg: 'Aborted connects too high', 119 | cmp: ">", 120 | interval: 60 121 | } 122 | }); 123 | 124 | metrics.rowLockWaits = new probe.metric({ 125 | name: 'Row Lock Waits', 126 | value: 'N/A' 127 | }); 128 | metrics.bufferPoolWaitFree = new probe.metric({ 129 | name: 'Row Lock Waits', 130 | value: 'N/A' 131 | }); 132 | metrics.openTables = new probe.metric({ 133 | name: 'Open Tables', 134 | value: 'N/A' 135 | }); 136 | 137 | metrics.ioWait = new probe.metric({ 138 | name: '% IO WAIT', 139 | value: 'N/A' 140 | }); 141 | metrics.ioReadRate = new probe.metric({ 142 | name: 'IO Read kb/s', 143 | value: 'N/A' 144 | }); 145 | metrics.ioWriteRate = new probe.metric({ 146 | name: 'IO Write kb/s', 147 | value: 'N/A' 148 | }); 149 | } 150 | 151 | // Refresh metrics 152 | function refreshMetrics(mysqlClient) { 153 | refreshProcessListMetrics(metrics, mysqlClient); 154 | refreshGlobalStatusMetrics(metrics, mysqlClient); 155 | } 156 | 157 | function init(mysqlClient) { 158 | initMetrics(); 159 | ioMetrics.init(metrics); 160 | setInterval(refreshMetrics.bind(this, mysqlClient), pmx.getConf().refreshRate); 161 | initMysqlVars(metrics, mysqlClient); 162 | } 163 | 164 | module.exports.init = init; -------------------------------------------------------------------------------- /lib/stats/initMysqlVars.js: -------------------------------------------------------------------------------- 1 | var pmx = require('pmx'); 2 | 3 | function init(metrics, mysqlClient) { 4 | var queryString = "SHOW VARIABLES ;"; 5 | 6 | mysqlClient.query(queryString, function (err, rows) { 7 | if (err) { 8 | return pmx.notify("SHOW VARIABLES Query Error: " + err); 9 | } 10 | 11 | rows.forEach(function (row) { 12 | if (row.Variable_name === "version") { 13 | metrics.version.set(row.Value); 14 | } 15 | if (row.Variable_name === "max_connections") { 16 | metrics.maxConnections.set(row.Value); 17 | } 18 | }); 19 | }); 20 | } 21 | 22 | module.exports = init; -------------------------------------------------------------------------------- /lib/stats/ioMetrics.js: -------------------------------------------------------------------------------- 1 | var pmx = require('pmx'); 2 | var shelljs = require('shelljs'); 3 | 4 | function init(metrics) { 5 | shelljs.exec('iostat', {async: true, silent: true}, function (err, out) { 6 | if (err) { 7 | return pmx.notify("iostat is not available. IO monitoring disabled"); 8 | } 9 | 10 | startMonitoring(metrics); 11 | }); 12 | } 13 | 14 | function startMonitoring(metrics) { 15 | var Hwmon = require('hwmon'); 16 | var hwmon = new Hwmon({interval: 1000}); 17 | 18 | hwmon.on('iostat', function (data) { 19 | if (data['avg-cpu:'] && data['avg-cpu:']['%iowait']) { 20 | metrics.ioWait.set(data['avg-cpu:']['%iowait']); 21 | } 22 | 23 | var deviceName = pmx.getConf().dbDiskName || 'sda'; 24 | if (data['devices'] && data['devices'][deviceName]) { 25 | if (data['devices'][deviceName]['kB_read/s']) { 26 | metrics.ioReadRate.set(data['devices'][deviceName]['kB_read/s']); 27 | } 28 | if (data['devices'][deviceName]['kB_wrtn/s']) { 29 | metrics.ioWriteRate.set(data['devices'][deviceName]['kB_wrtn/s']); 30 | } 31 | } 32 | }); 33 | 34 | hwmon.start(); 35 | } 36 | 37 | module.exports = { 38 | init: init 39 | }; -------------------------------------------------------------------------------- /lib/stats/refreshGlobalStatusMetrics.js: -------------------------------------------------------------------------------- 1 | var pmx = require('pmx'); 2 | 3 | module.exports = function refreshGlobalStatusMetrics(metrics, mysqlClient) { 4 | var queryString = "SHOW GLOBAL STATUS;"; 5 | 6 | var varNames = { 7 | 'Innodb_data_pending_reads': 'preads', 8 | 'Open_files': 'openFiles', 9 | 'Innodb_buffer_pool_read_requests': 'poolRead', 10 | 'Innodb_rows_read': 'rread', 11 | 'Bytes_sent': 'bsent', 12 | 'Connections': 'con', 13 | 'Questions': 'quest', 14 | 'Uptime': 'uptime', 15 | 'Threads_running': 'threadsRunning', 16 | 'Threads_connected': 'threadsConnected', 17 | 'Max_used_connections': 'maxUsedConnections', 18 | 'Innodb_row_lock_waits': 'rowLockWaits', 19 | 'Innodb_buffer_pool_wait_free': 'bufferPoolWaitFree', 20 | 'Open_tables': 'openTables', 21 | 'Aborted_connects': 'abortedConnects', 22 | }; 23 | 24 | mysqlClient.query(queryString, function (err, rows) { 25 | if (err) { 26 | return pmx.notify("GLOBAL STATUS Query Error: " + err); 27 | } 28 | 29 | rows.forEach(function (row) { 30 | var metricName = varNames[row.Variable_name]; 31 | if (metricName) { 32 | var lastMetric = metrics['last' + metricName]; 33 | if (lastMetric !== undefined) { 34 | if (lastMetric === 'N/A') { 35 | metrics[metricName].set(0); 36 | } else { 37 | metrics[metricName].set((row.Value - lastMetric) * (1000 / pmx.getConf().refreshRate)); 38 | } 39 | metrics['last' + metricName] = row.Value; 40 | } 41 | else { 42 | metrics[metricName].set(row.Value); 43 | } 44 | } 45 | }); 46 | 47 | if(metrics.maxConnections.val()){ 48 | metrics.maxUsedConnectionsPerc.set(parseInt(metrics.maxUsedConnections.val()/metrics.maxConnections.val() *100)); 49 | } 50 | 51 | }); 52 | }; -------------------------------------------------------------------------------- /lib/stats/refreshProcessListMetrics.js: -------------------------------------------------------------------------------- 1 | var pmx = require('pmx'); 2 | 3 | module.exports = function refreshBackendMetrics(metrics, mysqlClient) { 4 | var queryString = "SHOW FULL PROCESSLIST;"; 5 | 6 | mysqlClient.query(queryString, function (err, rows) { 7 | if (err) { 8 | return pmx.notify("PROCESSLIST Query Error: " + err); 9 | } 10 | 11 | // Total Processes count 12 | metrics.totalProcesses.set(rows.length); 13 | }); 14 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pm2-mysql", 3 | "version": "1.0.0", 4 | "description": "Mysql module", 5 | "dependencies": { 6 | "hwmon": "https://github.com/didil/node-hwmon#274445c1d7cbd8158f3549ef711d1bd6f565e828", 7 | "mysql": "^2.12.0", 8 | "pmx": "*", 9 | "shell-escape": "^0.2.0", 10 | "shelljs": "^0.7.5" 11 | }, 12 | "config": { 13 | "host": "localhost", 14 | "port": 3306, 15 | "user": "root", 16 | "password": "", 17 | "refreshRate": 1000, 18 | "dbDiskName": "sda", 19 | "slowQueriesLog": "/var/log/mysql/slow-queries.log", 20 | "generalLog": "/var/log/mysql/general.log", 21 | "lastQueriesSize": 100, 22 | "errorLog": "/var/log/mysql/error.log", 23 | "lastErrorsSize": 100 24 | }, 25 | "apps": [ 26 | { 27 | "merge_logs": true, 28 | "script": "index.js" 29 | } 30 | ], 31 | "author": "Keymetrics Inc.", 32 | "license": "MIT" 33 | } 34 | --------------------------------------------------------------------------------