├── .gitignore ├── script ├── test └── bootstrap ├── index.coffee ├── package.json ├── Gruntfile.js ├── LICENSE ├── src ├── mysql-profile.coffee └── mysql-explain.coffee └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # bootstrap environment 4 | source script/bootstrap 5 | 6 | mocha --compilers coffee:coffee-script -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure everything is development forever 4 | export NODE_ENV=development 5 | 6 | # Load environment specific environment variables 7 | if [ -f .env ]; then 8 | source .env 9 | fi 10 | 11 | if [ -f .env.${NODE_ENV} ]; then 12 | source .env.${NODE_ENV} 13 | fi 14 | 15 | npm install 16 | 17 | # Make sure coffee and mocha are on the path 18 | export PATH="node_modules/.bin:$PATH" -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = (robot, scripts) -> 5 | scriptsPath = path.resolve(__dirname, 'src') 6 | fs.exists scriptsPath, (exists) -> 7 | if exists 8 | for script in fs.readdirSync(scriptsPath) 9 | if scripts? and '*' not in scripts 10 | robot.loadFile(scriptsPath, script) if script in scripts 11 | else 12 | robot.loadFile(scriptsPath, script) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-mysql-chatops", 3 | "description": "Hubot ChatOps scripts for MySQL", 4 | "version": "1.0.0", 5 | "author": "samlambert ", 6 | "license": "MIT", 7 | 8 | "keywords": ["hubot","MySQL","ChatOps","queries","profile","explain"], 9 | 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/samlambert/hubot-mysql-chatops.git" 13 | }, 14 | 15 | "bugs": { 16 | "url": "https://github.com/samlambert/hubot-mysql-chatops/issues" 17 | }, 18 | 19 | "dependencies": { 20 | "coffee-script": "~1.6.3", 21 | "cli-table": "~0.2.0", 22 | "mysql": "~0.9.5", 23 | "validator": "3.1.0" 24 | }, 25 | 26 | "devDependencies": { 27 | "mocha": "*", 28 | "chai": "*", 29 | "sinon-chai": "*", 30 | "sinon": "*", 31 | "grunt-mocha-test": "~0.7.0", 32 | "grunt-release": "~0.6.0", 33 | "matchdep": "~0.1.2", 34 | "grunt-contrib-watch": "~0.5.3" 35 | }, 36 | 37 | "main": "index.coffee", 38 | 39 | "scripts": { 40 | "test": "grunt test" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | grunt.loadNpmTasks('grunt-mocha-test'); 6 | grunt.loadNpmTasks('grunt-release'); 7 | 8 | grunt.initConfig({ 9 | mochaTest: { 10 | test: { 11 | options: { 12 | reporter: 'spec', 13 | require: 'coffee-script' 14 | }, 15 | src: ['test/**/*.coffee'] 16 | } 17 | }, 18 | release: { 19 | options: { 20 | tagName: 'v<%= version %>', 21 | commitMessage: 'Prepared to release <%= version %>.' 22 | } 23 | }, 24 | watch: { 25 | files: ['Gruntfile.js', 'test/**/*.coffee'], 26 | tasks: ['test'] 27 | } 28 | }); 29 | 30 | grunt.event.on('watch', function(action, filepath, target) { 31 | grunt.log.writeln(target + ': ' + filepath + ' has ' + action); 32 | }); 33 | 34 | // load all grunt tasks 35 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 36 | 37 | grunt.registerTask('test', ['mochaTest']); 38 | grunt.registerTask('test:watch', ['watch']); 39 | grunt.registerTask('default', ['test']); 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sam Lambert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/mysql-profile.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Profile a MySQL Query 3 | # 4 | # Notes: 5 | # This script requires a MySQL user with SELECT priviliges. 6 | # You can create a user like so: GRANT SELECT ON some_db.* TO 'hubot_mysql'@'hubot_host' IDENTIFIED BY 'some_pass'; 7 | # !! Warning. In order to collect a profile the query is executed. It is very strongly recommended that you use a read only user on a MySQL slave !! 8 | # 9 | # Dependencies: 10 | # "cli-table" : "https://github.com/LearnBoost/cli-table" 11 | # "mysql" : "https://github.com/felixge/node-mysql" 12 | # "validator" : "https://github.com/chriso/validator.js" 13 | # 14 | # Configuration: 15 | # HUBOT_MYSQL_CHATOPS_HOST 16 | # HUBOT_MYSQL_CHATOPS_DATABASE 17 | # HUBOT_MYSQL_CHATOPS_USER 18 | # HUBOT_MYSQL_CHATOPS_PASS 19 | # 20 | # Commands: 21 | # hubot mysql profile - Run MySQL profile on 22 | 23 | mysql = require 'mysql' 24 | table = require 'cli-table' 25 | validator = require 'validator' 26 | 27 | module.exports = (robot) -> 28 | 29 | robot.respond /mysql profile (.*)/i, (msg) -> 30 | msg.reply "This will run and profile a query against MySQL. To run this fo realz use mysql profile!. Be careful ;)" 31 | return 32 | 33 | robot.respond /mysql profile! (.*)/i, (msg) -> 34 | query = validator.blacklist(msg.match[1], [';']) 35 | 36 | unless process.env.HUBOT_MYSQL_CHATOPS_HOST? 37 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_HOST" 38 | return 39 | 40 | unless process.env.HUBOT_MYSQL_CHATOPS_DATABASE? 41 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_DATABASE" 42 | return 43 | 44 | unless process.env.HUBOT_MYSQL_CHATOPS_USER? 45 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_USER" 46 | return 47 | 48 | unless process.env.HUBOT_MYSQL_CHATOPS_PASS? 49 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_PASS" 50 | return 51 | 52 | @client = mysql.createClient 53 | host: "#{process.env.HUBOT_MYSQL_CHATOPS_HOST}" 54 | database: "#{process.env.HUBOT_MYSQL_CHATOPS_DATABASE}" 55 | user: "#{process.env.HUBOT_MYSQL_CHATOPS_USER}" 56 | password: "#{process.env.HUBOT_MYSQL_CHATOPS_PASS}" 57 | @client.on 'error', (err) -> 58 | robot.emit 'error', err, msg 59 | 60 | @client.query "SET PROFILING = 1", (err, results) => 61 | if err 62 | msg.reply err 63 | return 64 | @client.query "#{query}", (err, results) => 65 | if err 66 | msg.reply err 67 | return 68 | @client.query "SHOW PROFILE FOR QUERY 1", (err, results) => 69 | if err 70 | msg.reply err 71 | return 72 | 73 | status_max = 0 74 | duration_max = 0 75 | 76 | rows = [] 77 | 78 | for row in results 79 | profile = ["#{row.Status}", "#{row.Duration}"] 80 | padding = 8 81 | if profile[0].length + padding > status_max 82 | status_max = profile[0].length + padding 83 | if profile[1].length + padding > duration_max 84 | duration_max = profile[1].length + padding 85 | rows.push profile 86 | 87 | @grid = new table 88 | head: ['Status', 'Duration (secs)'] 89 | style: { head: false } 90 | colWidths: [status_max, duration_max] 91 | 92 | for row in rows 93 | @grid.push row 94 | 95 | msg.reply "\n#{@grid.toString()}" 96 | @client.destroy() 97 | -------------------------------------------------------------------------------- /src/mysql-explain.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Explain a MySQL Query 3 | # 4 | # Notes: 5 | # This script requires a MySQL user with SELECT priviliges. 6 | # You can create a user like so: GRANT SELECT ON some_db.* TO 'hubot_mysql'@'hubot_host' IDENTIFIED BY 'some_pass'; 7 | # !! It is very strongly recommended that you use a read only user on a MySQL slave !! 8 | # 9 | # Dependencies: 10 | # "cli-table" : "https://github.com/LearnBoost/cli-table" 11 | # "mysql" : "https://github.com/felixge/node-mysql" 12 | # "validator" : "https://github.com/chriso/validator.js" 13 | # 14 | # Configuration: 15 | # HUBOT_MYSQL_CHATOPS_HOST 16 | # HUBOT_MYSQL_CHATOPS_DATABASE 17 | # HUBOT_MYSQL_CHATOPS_USER 18 | # HUBOT_MYSQL_CHATOPS_PASS 19 | # 20 | # Commands: 21 | # hubot mysql explain - Run MySQL EXPLAIN on 22 | 23 | mysql = require 'mysql' 24 | table = require 'cli-table' 25 | validator = require 'validator' 26 | 27 | module.exports = (robot) -> 28 | robot.respond /mysql explain (.*)/i, (msg) -> 29 | query = validator.blacklist(msg.match[1], [';']) 30 | 31 | unless process.env.HUBOT_MYSQL_CHATOPS_HOST? 32 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_HOST" 33 | return 34 | 35 | unless process.env.HUBOT_MYSQL_CHATOPS_DATABASE? 36 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_DATABASE" 37 | return 38 | 39 | unless process.env.HUBOT_MYSQL_CHATOPS_USER? 40 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_USER" 41 | return 42 | 43 | unless process.env.HUBOT_MYSQL_CHATOPS_PASS? 44 | msg.reply "Would love to, but kind of missing HUBOT_MYSQL_CHATOPS_PASS" 45 | return 46 | 47 | @client = mysql.createClient 48 | host: "#{process.env.HUBOT_MYSQL_CHATOPS_HOST}" 49 | database: "#{process.env.HUBOT_MYSQL_CHATOPS_DATABASE}" 50 | user: "#{process.env.HUBOT_MYSQL_CHATOPS_USER}" 51 | password: "#{process.env.HUBOT_MYSQL_CHATOPS_PASS}" 52 | @client.on 'error', (err) -> 53 | robot.emit 'error', err, msg 54 | 55 | @client.query "EXPLAIN #{query}", (err, results) => 56 | if err 57 | msg.reply err 58 | return 59 | 60 | table_max = 0 61 | poss_max = 0 62 | key_max = 0 63 | ref_max = 0 64 | extra_max = 0 65 | 66 | rows = [] 67 | 68 | for row in results 69 | row.Extra ?= '' 70 | explain = ["#{row.select_type}", "#{row.table}", "#{row.type}", "#{row.possible_keys}", "#{row.key}", "#{row.key_len}", "#{row.ref}", "#{row.rows}", "#{row.Extra}"] 71 | padding = 4 72 | if explain[1].length + padding > table_max 73 | table_max = explain[1].length + padding 74 | if explain[3].length + padding > poss_max 75 | poss_max = explain[3].length + padding 76 | if explain[4].length + padding > key_max 77 | key_max = explain[4].length + padding 78 | if explain[6].length + padding > ref_max 79 | ref_max = explain[6].length + padding 80 | if explain[8].length + padding > extra_max 81 | extra_max = explain[8].length + padding 82 | rows.push explain 83 | 84 | @grid = new table 85 | head: ['Select Type', 'Table', 'Type', 'Possible Keys', 'Key', 'Key Len', 'Ref', 'Rows', 'Extra'] 86 | style: { head: false } 87 | colWidths: [15, table_max, 10, poss_max, key_max, 10, ref_max, 10, extra_max] 88 | 89 | for row in rows 90 | @grid.push row 91 | 92 | msg.reply "\n#{@grid.toString()}" 93 | @client.destroy() 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hubot: hubot-mysql-chatops 2 | 3 | A small collection of MySQL ChatOps scripts. 4 | 5 | See each script in `src/` for full documentation. 6 | 7 | ## Installation 8 | 9 | Add **hubot-mysql-chatops** to your `package.json` file: 10 | 11 | ```json 12 | "dependencies": { 13 | "hubot-mysql-chatops": ">= 1.0.0", 14 | } 15 | ``` 16 | 17 | Add **hubot-mysql-chatops** to your `external-scripts.json`: 18 | 19 | ```json 20 | ["hubot-mysql-chatops"] 21 | ``` 22 | 23 | Run `npm install` 24 | 25 | ## Warnings 26 | 27 | Some of these scripts execute queries. It is very strongly recommended that a read only user is used and queries are executed on a MySQL slave. 28 | 29 | I can't be responsible for you deleting all your data ;) 30 | 31 | An example GRANT would be: `GRANT SELECT ON some_db.* TO 'hubot_mysql'@'hubot_host' IDENTIFIED BY 'some_pass';` 32 | 33 | ## Sample Interaction 34 | 35 | ``` 36 | user1>> mysql explain SELECT * FROM users 37 | hubot>> user1: 38 | ┌───────────────┬─────────┬──────────┬────────┬────────┬──────────┬────────┬──────────┬────┐ 39 | │Select Type │Table │Type │Possibl…│Key │Key Len │Ref │Rows │Ext…│ 40 | ├───────────────┼─────────┼──────────┼────────┼────────┼──────────┼────────┼──────────┼────┤ 41 | │SIMPLE │users │ALL │null │null │null │null │0 │ │ 42 | └───────────────┴─────────┴──────────┴────────┴────────┴──────────┴────────┴──────────┴────┘ 43 | ``` 44 | 45 | ``` 46 | user1>> mysql profile SELECT * FROM users 47 | hubot>> user1: 48 | ┌──────────────────────────────────────┬────────────────┐ 49 | │Status │Duration (secs) │ 50 | ├──────────────────────────────────────┼────────────────┤ 51 | │starting │0.000036 │ 52 | ├──────────────────────────────────────┼────────────────┤ 53 | │Waiting for query cache lock │0.000004 │ 54 | ├──────────────────────────────────────┼────────────────┤ 55 | │checking query cache for query │0.000042 │ 56 | ├──────────────────────────────────────┼────────────────┤ 57 | │checking permissions │0.000009 │ 58 | ├──────────────────────────────────────┼────────────────┤ 59 | │Opening tables │0.000031 │ 60 | ├──────────────────────────────────────┼────────────────┤ 61 | │System lock │0.000011 │ 62 | ├──────────────────────────────────────┼────────────────┤ 63 | │Waiting for query cache lock │0.000027 │ 64 | ├──────────────────────────────────────┼────────────────┤ 65 | │init │0.000029 │ 66 | ├──────────────────────────────────────┼────────────────┤ 67 | │optimizing │0.000006 │ 68 | ├──────────────────────────────────────┼────────────────┤ 69 | │statistics │0.000013 │ 70 | ├──────────────────────────────────────┼────────────────┤ 71 | │preparing │0.000010 │ 72 | ├──────────────────────────────────────┼────────────────┤ 73 | │executing │0.000003 │ 74 | ├──────────────────────────────────────┼────────────────┤ 75 | │Sending data │0.000089 │ 76 | ├──────────────────────────────────────┼────────────────┤ 77 | │end │0.000006 │ 78 | ├──────────────────────────────────────┼────────────────┤ 79 | │query end │0.000006 │ 80 | ├──────────────────────────────────────┼────────────────┤ 81 | │closing tables │0.000008 │ 82 | ├──────────────────────────────────────┼────────────────┤ 83 | │freeing items │0.000007 │ 84 | ├──────────────────────────────────────┼────────────────┤ 85 | │Waiting for query cache lock │0.000003 │ 86 | ├──────────────────────────────────────┼────────────────┤ 87 | │freeing items │0.000064 │ 88 | ├──────────────────────────────────────┼────────────────┤ 89 | │Waiting for query cache lock │0.000008 │ 90 | ├──────────────────────────────────────┼────────────────┤ 91 | │freeing items │0.000003 │ 92 | ├──────────────────────────────────────┼────────────────┤ 93 | │storing result in query cache │0.000004 │ 94 | ├──────────────────────────────────────┼────────────────┤ 95 | │logging slow query │0.000002 │ 96 | ├──────────────────────────────────────┼────────────────┤ 97 | │cleaning up │0.000003 │ 98 | └──────────────────────────────────────┴────────────────┘ 99 | 100 | ``` 101 | 102 | ## Thanks 103 | 104 | Thanks to everyone who has contributed to Hubot and this packages dependencies. 105 | 106 | A special thank you to @technicalpickles for being awesome. --------------------------------------------------------------------------------