├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo.gif ├── logo.png ├── package.json ├── src ├── createDatabaseConnectionConfiguration.js ├── formatSql.js └── index.js └── test ├── .eslintrc └── seeql └── formatSql.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "plugins": [ 5 | "transform-object-rest-spread", 6 | "transform-flow-strip-types" 7 | ] 8 | }, 9 | "production": { 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | "transform-flow-comments" 13 | ] 14 | }, 15 | "test": { 16 | "plugins": [ 17 | "transform-object-rest-spread", 18 | "transform-flow-strip-types", 19 | "istanbul" 20 | ] 21 | } 22 | }, 23 | "presets": [ 24 | [ 25 | "env", 26 | { 27 | "targets": { 28 | "node": 7.7 29 | }, 30 | "useBuiltIns": true 31 | } 32 | ] 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/seeql/dc2c33a6968b11d6e3ec08a6a58b563e440fdcd6/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical", 4 | "canonical/flowtype" 5 | ], 6 | "root": true, 7 | "rules": { 8 | "import/no-commonjs": 0, 9 | "import/order": 0, 10 | "global-require": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*/test/.* 3 | .*/node_modules/babel-plugin-flow-runtime/.* 4 | .*/node_modules/config-chain/.* 5 | .*/node_modules/conventional-changelog-core/.* 6 | .*/node_modules/flow-runtime/.* 7 | .*/node_modules/npmconf/.* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintignore 9 | !.eslintrc 10 | !.flowconfig 11 | !.gitignore 12 | !.npmignore 13 | !.travis.yml 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | coverage 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | script: 5 | - npm run build 6 | - npm run test 7 | - npm run lint 8 | # - nyc --silent npm run test 9 | # - nyc report --reporter=text-lcov | coveralls 10 | # - nyc check-coverage --lines 80 11 | after_success: 12 | - export NODE_ENV=production 13 | - rm -fr ./dist 14 | - npm run build 15 | - semantic-release pre && npm publish && semantic-release post 16 | notifications: 17 | email: false 18 | sudo: false 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seeql 2 | 3 | logo 4 | 5 | [![Travis build status](http://img.shields.io/travis/gajus/seeql/master.svg?style=flat-square)](https://travis-ci.org/gajus/seeql) 6 | [![Coveralls](https://img.shields.io/coveralls/gajus/seeql.svg?style=flat-square)](https://coveralls.io/github/gajus/seeql) 7 | [![NPM version](http://img.shields.io/npm/v/seeql.svg?style=flat-square)](https://www.npmjs.org/package/seeql) 8 | [![Canonical Code Style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 9 | [![Twitter Follow](https://img.shields.io/twitter/follow/kuizinas.svg?style=social&label=Follow)](https://twitter.com/kuizinas) 10 | 11 | Real-time SQL profiler. 12 | 13 | > Hello all! 14 | > 15 | > I've created SeeQL for to enable real-time debugging of applications. SeeQL is extremely handy for identifying slow queries as you navigate the application. It acts as a transparent proxy, therefore 0 changes need to be done to the code base to use it. It basically allows you to see what queries are being executed, their response time, row count, etc. 16 | > 17 | > **I am looking for contributors who are equally passionate about using MySQL in Node.js to help further develop this project.** I've opened a few simple issues to pick up if anyone wants to give it a shot! 18 | 19 | ![Demo](https://rawgit.com/gajus/seeql/master/demo.gif) 20 | 21 | ## How to use it? 22 | 23 | 1. Start `seeql` and configure it to talk with the application database. 24 | 2. Configure application to connect to `seeql` service. 25 | 26 | ## CLI 27 | 28 | ```bash 29 | npm install seeql -g 30 | 31 | seeql --help 32 | 33 | Options: 34 | --help Show help [boolean] 35 | --database-host Target database host. Seeql will connect to this database 36 | and proxy all incoming queries. [string] [required] 37 | --database-database [string] [required] 38 | --database-password [string] [required] 39 | --database-user [string] [required] 40 | --service-port [number] [default: 3306] 41 | ``` 42 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/seeql/dc2c33a6968b11d6e3ec08a6a58b563e440fdcd6/demo.gif -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/seeql/dc2c33a6968b11d6e3ec08a6a58b563e440fdcd6/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "ava": { 8 | "babel": "inherit", 9 | "require": [ 10 | "babel-register" 11 | ] 12 | }, 13 | "bin": "./dist/index.js", 14 | "dependencies": { 15 | "ajv": "^4.11.5", 16 | "blessed": "^0.1.81", 17 | "blessed-contrib": "^4.7.5", 18 | "debug": "^2.6.3", 19 | "es6-error": "^4.0.2", 20 | "lodash": "^4.17.4", 21 | "mysql2": "^1.2.0", 22 | "pretty-hrtime": "^1.0.3", 23 | "yargs": "^7.0.2" 24 | }, 25 | "description": "", 26 | "devDependencies": { 27 | "ava": "^0.18.2", 28 | "babel-cli": "^6.24.0", 29 | "babel-plugin-istanbul": "^4.0.0", 30 | "babel-plugin-transform-flow-comments": "^6.22.0", 31 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 32 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 33 | "babel-preset-env": "^1.2.2", 34 | "babel-register": "^6.24.0", 35 | "coveralls": "^2.12.0", 36 | "eslint": "^3.17.1", 37 | "eslint-config-canonical": "^7.1.0", 38 | "flow-bin": "^0.41.0", 39 | "husky": "^0.13.2", 40 | "nyc": "^10.1.2", 41 | "semantic-release": "^6.3.6", 42 | "sinon": "^2.0.0" 43 | }, 44 | "engines": { 45 | "node": ">=7.7" 46 | }, 47 | "keywords": [ 48 | "mysql", 49 | "promise", 50 | "types" 51 | ], 52 | "license": "BSD-3-Clause", 53 | "main": "./dist/index.js", 54 | "name": "seeql", 55 | "nyc": { 56 | "include": [ 57 | "src/**/*.js" 58 | ], 59 | "instrument": false, 60 | "reporter": [ 61 | "text-lcov" 62 | ], 63 | "require": [ 64 | "babel-register" 65 | ], 66 | "sourceMap": false 67 | }, 68 | "repository": { 69 | "type": "git", 70 | "url": "https://github.com/gajus/seeql" 71 | }, 72 | "scripts": { 73 | "build": "babel ./src --out-dir ./dist --copy-files --source-maps", 74 | "lint": "eslint ./src ./test && flow", 75 | "test": "ava --verbose" 76 | }, 77 | "version": "1.0.1" 78 | } 79 | -------------------------------------------------------------------------------- /src/createDatabaseConnectionConfiguration.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type DatabaseConfigurationType = { 4 | +databaseDatabase: string, 5 | +databaseHost: string, 6 | +databasePassword: string, 7 | +databaseUser: string 8 | }; 9 | 10 | export default (configuration: DatabaseConfigurationType) => { 11 | return { 12 | database: configuration.databaseDatabase, 13 | dateStrings: true, 14 | host: configuration.databaseHost, 15 | password: configuration.databasePassword, 16 | user: configuration.databaseUser 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/formatSql.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | trim 5 | } from 'lodash'; 6 | 7 | export default (sql: string): string => { 8 | let formattedSql: string; 9 | 10 | formattedSql = sql 11 | .replace(/\n/g, ' ') 12 | .replace(/\t/g, ' ') 13 | .replace(/ +/g, ' '); 14 | 15 | formattedSql = trim(formattedSql); 16 | 17 | return formattedSql; 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import yargs from 'yargs'; 4 | import mysql from 'mysql2'; 5 | import createDebug from 'debug'; 6 | import prettyHrtime from 'pretty-hrtime'; 7 | import createDatabaseConnectionConfiguration from './createDatabaseConnectionConfiguration'; 8 | import formatSql from './formatSql'; 9 | 10 | const debug = createDebug('seeql'); 11 | 12 | const argv = yargs 13 | .env('SEEQL') 14 | .help() 15 | .strict() 16 | .options({ 17 | 'database-database': { 18 | demand: true, 19 | type: 'string' 20 | }, 21 | 'database-host': { 22 | demand: true, 23 | description: 'Target database host. Seeql will connect to this database and proxy all incoming queries.', 24 | type: 'string' 25 | }, 26 | 'database-password': { 27 | demand: true, 28 | type: 'string' 29 | }, 30 | 'database-user': { 31 | demand: true, 32 | type: 'string' 33 | }, 34 | 'service-port': { 35 | default: 3306, 36 | type: 'number' 37 | }, 38 | 'use-screen': { 39 | default: true, 40 | type: 'boolean' 41 | } 42 | }) 43 | .argv; 44 | 45 | let screen; 46 | let table; 47 | 48 | if (argv.useScreen) { 49 | const blessed = require('blessed'); 50 | const contrib = require('blessed-contrib'); 51 | 52 | screen = blessed.screen({ 53 | smartCSR: true 54 | }); 55 | 56 | screen.key(['escape', 'q', 'C-c'], () => { 57 | // eslint-disable-next-line no-process-exit 58 | return process.exit(0); 59 | }); 60 | 61 | table = contrib.table({ 62 | columnSpacing: 5, 63 | columnWidth: [ 64 | 15, 65 | 15, 66 | 50, 67 | 10, 68 | 30 69 | ], 70 | fg: 'white', 71 | height: '80%', 72 | interactive: true, 73 | keys: true, 74 | label: null, 75 | selectedBg: 'blue', 76 | selectedFg: 'white', 77 | width: '80%' 78 | }); 79 | 80 | table.focus(); 81 | 82 | screen.append(table); 83 | 84 | screen.render(); 85 | } 86 | 87 | const server = mysql.createServer(); 88 | 89 | const drawTable = (drawQueries) => { 90 | table.setData({ 91 | data: drawQueries.map((query) => { 92 | return [ 93 | query.connectionId, 94 | query.queryId, 95 | formatSql(query.sql).slice(0, 50), 96 | query.rows.length, 97 | prettyHrtime(query.executionTime) 98 | ]; 99 | }), 100 | headers: [ 101 | 'Connection ID', 102 | 'Query ID', 103 | 'SQL', 104 | 'Row count', 105 | 'Execution time' 106 | ] 107 | }); 108 | 109 | screen.render(); 110 | }; 111 | 112 | let connectionId = 0; 113 | let queryId = 0; 114 | 115 | const queries = []; 116 | 117 | const ClientFlags = require('mysql2/lib/constants/client.js'); 118 | 119 | server.on('connection', (connection) => { 120 | debug('received client connection request'); 121 | 122 | connection.serverHandshake({ 123 | capabilityFlags: 0xffffff ^ ClientFlags.COMPRESS, 124 | characterSet: 8, 125 | connectionId: connectionId++, 126 | protocolVersion: 10, 127 | serverVersion: '5.6.10', 128 | statusFlags: 2 129 | }); 130 | 131 | const remote = mysql.createConnection(createDatabaseConnectionConfiguration(argv)); 132 | 133 | connection.on('error', (error) => { 134 | debug('connection error', error.message); 135 | }); 136 | 137 | connection.on('field_list', (targetTable, fields) => { 138 | debug('field_list', targetTable, fields); 139 | 140 | connection.writeEof(); 141 | }); 142 | 143 | connection.on('query', (sql) => { 144 | queryId++; 145 | 146 | const start = process.hrtime(); 147 | 148 | debug('received query', sql); 149 | 150 | remote.query({ 151 | sql, 152 | typeCast: (field) => { 153 | return field.string(); 154 | } 155 | }, (queryError, rows, fields) => { 156 | if (queryError) { 157 | throw new Error('Unexpected error.'); 158 | } 159 | 160 | const end = process.hrtime(start); 161 | 162 | debug('received response from the remote database in %s', prettyHrtime(end), rows, fields); 163 | 164 | queries.push({ 165 | connectionId, 166 | executionTime: end, 167 | fields, 168 | queryId, 169 | rows, 170 | sql 171 | }); 172 | 173 | if (argv.useScreen) { 174 | drawTable(queries); 175 | } 176 | 177 | if (Array.isArray(rows)) { 178 | connection.writeTextResult(rows, fields); 179 | } else { 180 | connection.writeOk(rows); 181 | } 182 | }); 183 | }); 184 | 185 | connection.on('end', () => { 186 | remote.end(); 187 | }); 188 | }); 189 | 190 | server.listen(argv.servicePort, () => { 191 | debug('server listening on port %d', argv.servicePort); 192 | }); 193 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical/ava", 3 | "rules": { 4 | "filenames/match-regex": 0, 5 | "id-length": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/seeql/formatSql.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import test from 'ava'; 4 | import formatSql from '../../src/formatSql'; 5 | 6 | test('formats multiline SQL into a single string', (t) => { 7 | t.true(formatSql('foo\nbar\nbaz') === 'foo bar baz'); 8 | }); 9 | 10 | test('replaces tabs with spaces', (t) => { 11 | t.true(formatSql('foo\tbar\tbaz') === 'foo bar baz'); 12 | }); 13 | 14 | test('replaces multiple consequent spaces with a single space', (t) => { 15 | t.true(formatSql('foo bar baz') === 'foo bar baz'); 16 | }); 17 | 18 | test('trims the SQL', (t) => { 19 | t.true(formatSql(' foo ') === 'foo'); 20 | }); 21 | --------------------------------------------------------------------------------