├── screenshot.png ├── .gitignore ├── release.sh ├── .goreleaser.yml ├── config.toml ├── configuration ├── definition.go ├── template.go └── io.go ├── LICENSE ├── go.mod ├── README.md ├── mysql ├── query.go └── connect.go ├── kill-mysql-query.go └── go.sum /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugli/go-kill-mysql-query/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | /__test.toml 3 | /go-kill-mysql-query 4 | /__local.toml 5 | /dist 6 | /kill-mysql-query 7 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tag manually first 4 | # git tag -a v0.1.0 -m "First release" 5 | # git push origin v0.1.0 6 | 7 | # For dry run: 8 | # goreleaser --snapshot --skip-publish --rm-dist 9 | 10 | goreleaser release --rm-dist 11 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | project_name: kill-mysql-query 4 | before: 5 | hooks: 6 | - go mod download 7 | 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - darwin 13 | - windows 14 | - linux 15 | ignore: 16 | - goos: darwin 17 | goarch: 386 18 | 19 | archives: 20 | - replacements: 21 | darwin: macOS 22 | linux: Linux 23 | windows: Windows 24 | 386: i386 25 | amd64: x86_64 26 | format_overrides: 27 | - goos: windows 28 | format: zip 29 | - goos: darwin 30 | format: zip 31 | checksum: 32 | name_template: 'checksums.txt' 33 | 34 | snapshot: 35 | name_template: '{{ .Tag }}-next' 36 | 37 | changelog: 38 | sort: asc 39 | filters: 40 | exclude: 41 | - '^docs:' 42 | - '^test:' 43 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | [MySQL] 4 | mysql_host = "" 5 | mysql_port = 3306 6 | mysql_username = "" 7 | mysql_password = "" 8 | 9 | # hosted_in_aws_rds: Optional. 10 | # 11 | # Uses "CALL mysql.rds_kill()" instead of "kill" command. 12 | # Useful in RDS databases or replica where 13 | # "mysql_username" may not have privilege to use "kill" 14 | hosted_in_aws_rds = false 15 | 16 | # db: Optional. 17 | # 18 | # If provided, filter out long running 19 | # queries from other databases 20 | db = "" 21 | 22 | [ssh_tunnel] 23 | use_ssh_tunnel = false 24 | ssh_host = "" 25 | ssh_port = 22 26 | ssh_username = "" 27 | ssh_password = "" 28 | 29 | # ssh_private_key takes priority over ssh_password 30 | # if both are provided 31 | ssh_private_key = "" 32 | 33 | # ssh_key_passphrase: Optional. 34 | ssh_key_passphrase = "" 35 | 36 | [long_running_query] 37 | # Default is 10 seconds. 38 | # kill-mysql-query will only list running queries 39 | # those are being executed for more than or equal to 40 | # this value. 41 | timeout_second = 10 42 | 43 | -------------------------------------------------------------------------------- /configuration/definition.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | type Config struct { 4 | MySQL mysql 5 | SSH sshTunnel `toml:"ssh_tunnel"` 6 | LongQuery longRunningQuery `toml:"long_running_query"` 7 | } 8 | 9 | type mysql struct { 10 | Host string `toml:"mysql_host" validate:"required"` 11 | Port int `toml:"mysql_port"` 12 | Username string `toml:"mysql_username" validate:"required"` 13 | Password string `toml:"mysql_password"` 14 | AwsRds bool `toml:"hosted_in_aws_rds"` 15 | DB string `toml:"db"` 16 | } 17 | 18 | type sshTunnel struct { 19 | UseTunnel bool `toml:"use_ssh_tunnel"` 20 | Host string `toml:"ssh_host" validate:"required_with=UseTunnel"` 21 | Port int `toml:"ssh_port"` 22 | Username string `toml:"ssh_username" validate:"required_with=UseTunnel"` 23 | Password string `toml:"ssh_password"` 24 | Key string `toml:"ssh_private_key"` 25 | KeyPassphrase string `toml:"ssh_key_passphrase"` 26 | } 27 | 28 | type longRunningQuery struct { 29 | TimeoutSecond int `toml:"timeout_second"` 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Mehdi Hasan Khan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /configuration/template.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | var baseConfig =` 4 | 5 | [MySQL] 6 | mysql_host = "" 7 | mysql_port = 3306 8 | mysql_username = "" 9 | mysql_password = "" 10 | 11 | # hosted_in_aws_rds: Optional. 12 | # 13 | # Uses "CALL mysql.rds_kill()" instead of "kill" command. 14 | # Useful in RDS databases or replica where 15 | # "mysql_username" may not have privilege to use "kill" 16 | hosted_in_aws_rds = false 17 | 18 | # db: Optional. 19 | # 20 | # If provided, filter out long running 21 | # queries from other databases 22 | db = "" 23 | 24 | [ssh_tunnel] 25 | use_ssh_tunnel = false 26 | ssh_host = "" 27 | ssh_port = 22 28 | ssh_username = "" 29 | ssh_password = "" 30 | 31 | # ssh_private_key takes priority over ssh_password 32 | # if both are provided 33 | ssh_private_key = "" 34 | 35 | # ssh_key_passphrase: Optional. 36 | ssh_key_passphrase = "" 37 | 38 | [long_running_query] 39 | # Default is 10 seconds. 40 | # kill-mysql-query will only list running queries 41 | # those are being executed for more than or equal to 42 | # this value. 43 | timeout_second = 10 44 | 45 | ` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mugli/go-kill-mysql-query 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/alecthomas/gometalinter v3.0.0+incompatible // indirect 8 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 // indirect 9 | github.com/go-playground/locales v0.12.1 // indirect 10 | github.com/go-playground/universal-translator v0.16.0 // indirect 11 | github.com/go-sql-driver/mysql v1.4.1 12 | github.com/golang/lint v0.0.0-20190409202823-5614ed5bae6fb75893070bdc0996a68765fdd275 // indirect 13 | github.com/gookit/color v1.1.7 14 | github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 // indirect 15 | github.com/jmoiron/sqlx v1.2.0 16 | github.com/leodido/go-urn v1.1.0 // indirect 17 | github.com/lib/pq v1.2.0 // indirect 18 | github.com/lunixbochs/vtclean v1.0.0 // indirect 19 | github.com/manifoldco/promptui v0.3.2 20 | github.com/mattn/go-colorable v0.1.2 // indirect 21 | github.com/mattn/go-sqlite3 v1.11.0 // indirect 22 | github.com/rhysd/abspath v0.0.0-20190409124310-ff0e3470a837 23 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 24 | golang.org/x/lint v0.0.0-20190511005446-959b441ac422 // indirect 25 | golang.org/x/sys v0.0.0-20190804062209-51ab0e2deafa // indirect 26 | golang.org/x/tools v0.0.0-20190809152137-6d4652c779c4 // indirect 27 | google.golang.org/appengine v1.6.1 // indirect 28 | gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect 29 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 30 | gopkg.in/go-playground/validator.v9 v9.29.1 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kill-mysql-query 2 | 3 | ``` 4 | _____ ____ 5 | / \ | o | 6 | | |/ ___\| 7 | |_________/ 8 | |_|_| |_|_| 9 | ``` 10 | 11 | `kill-mysql-query` interactively shows long running queries in MySQL database and provides option to kill them one by one. 12 | 13 | 👉 Great for firefighting situations 🔥🚨🚒 14 | 15 | It can connect to MySQL server as configured, can use SSH Tunnel if necessary, and let you decide which query to kill. By default queries running for more than 10 seconds will be marked as long running queries, but it can be configured. 16 | 17 | ![screenshot](https://raw.githubusercontent.com/mugli/go-kill-mysql-query/master/screenshot.png) 18 | 19 | --- 20 | 21 | ## Installation 22 | 23 | Download binary from [release tab](https://github.com/mugli/go-kill-mysql-query/releases). 24 | 25 | --- 26 | 27 | ## Usage 28 | 29 | ``` 30 | kill-mysql-query [config.toml]: 31 | Checks for long running queries in the configured server. 32 | If no file is given, it tries to read from config.toml 33 | in the current directory. 34 | 35 | Other commands: 36 | 37 | generate [config.toml]: 38 | Generates a new empty configuration file 39 | 40 | init: 41 | Alias for generate 42 | 43 | help, --help, -h: 44 | Shows this message 45 | 46 | ``` 47 | 48 | --- 49 | 50 | ## Configuration 51 | 52 | Run `kill-mysql-query init` to generate an empty configuration file. 53 | 54 | ``` 55 | [MySQL] 56 | mysql_host = "" 57 | mysql_port = 3306 58 | mysql_username = "" 59 | mysql_password = "" 60 | 61 | # hosted_in_aws_rds: Optional. 62 | # 63 | # Uses `CALL mysql.rds_kill()` instead of `kill` command. 64 | # Useful in RDS databases or replica where 65 | # `mysql_username` may not have privilege to use `kill` 66 | hosted_in_aws_rds = false 67 | 68 | # db: Optional. 69 | # 70 | # If provided, filter out long running 71 | # queries from other databases 72 | db = "" 73 | 74 | [ssh_tunnel] 75 | use_ssh_tunnel = false 76 | ssh_host = "" 77 | ssh_port = 22 78 | ssh_username = "" 79 | ssh_password = "" 80 | 81 | # ssh_private_key takes priority over ssh_password 82 | # if both are provided 83 | ssh_private_key = "" 84 | 85 | # ssh_key_passphrase: Optional. 86 | ssh_key_passphrase = "" 87 | 88 | [long_running_query] 89 | # Default is 10 seconds. 90 | # `kill-mysql-query` will only list running queries 91 | # those are being executed for more than or equal to 92 | # this value. 93 | timeout_second = 10 94 | 95 | ``` 96 | 97 | --- 98 | 99 | ## FAQ 100 | 101 | **How do I simulate a long running query to test `kill-mysql-query`?** 102 | 103 | This stackoverflow answer may come in handy: 104 | https://stackoverflow.com/a/3892443/761555 105 | 106 | ``` 107 | select benchmark(9999999999, md5('when will it end?')); 108 | ``` 109 | 110 | --- 111 | 112 | ## License 113 | 114 | MIT 115 | -------------------------------------------------------------------------------- /configuration/io.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/BurntSushi/toml" 12 | "github.com/rhysd/abspath" 13 | "gopkg.in/go-playground/validator.v9" 14 | ) 15 | 16 | func setDefault() Config { 17 | return Config{ 18 | LongQuery: longRunningQuery{ 19 | TimeoutSecond: 10, 20 | }, 21 | MySQL: mysql{ 22 | Port: 3306, 23 | }, 24 | SSH: sshTunnel{ 25 | Port: 22, 26 | }, 27 | } 28 | } 29 | 30 | func validate(config Config) error { 31 | validate := validator.New() 32 | 33 | validate.RegisterTagNameFunc(func(fld reflect.StructField) string { 34 | name := strings.SplitN(fld.Tag.Get("toml"), ",", 2)[0] 35 | if name == "-" { 36 | return "" 37 | } 38 | return name 39 | }) 40 | 41 | err := validate.Struct(config) 42 | 43 | if err != nil { 44 | errs := err.(validator.ValidationErrors) 45 | 46 | return errors.New(fmt.Sprintf("%s is required in configuration", errs[0].Field())) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func Read(file string) (Config, error) { 53 | fmt.Println("⚙️ Reading configuration") 54 | var filePath string 55 | config := setDefault() 56 | 57 | if file == "" { 58 | dir, err := os.Getwd() 59 | if err != nil { 60 | fmt.Println(err) 61 | os.Exit(1) 62 | } 63 | 64 | defaultConfig := filepath.Join(dir, "config.toml") 65 | 66 | if fExists(defaultConfig) { 67 | filePath = defaultConfig 68 | } else { 69 | return config, errors.New(defaultConfig + " does not exist. Cannot read from config. \n\nRun `kill-mysql-query help` for usage details.\n") 70 | } 71 | } else { 72 | abs, err := abspath.ExpandFrom(file) 73 | if err != nil { 74 | return config, err 75 | } 76 | 77 | filePath = abs.String() 78 | if !fExists(filePath) { 79 | return config, errors.New(filePath + " does not exist. Cannot read from config. \n\nRun `kill-mysql-query help` for usage details.\n") 80 | } 81 | } 82 | 83 | _, err := toml.DecodeFile(filePath, &config) 84 | if err != nil { 85 | return config, err 86 | } 87 | 88 | err = validate(config) 89 | if err != nil { 90 | return config, err 91 | } 92 | 93 | return config, err 94 | } 95 | 96 | func Generate(file string) error { 97 | var filePath string 98 | if file == "" { 99 | dir, err := os.Getwd() 100 | if err != nil { 101 | fmt.Println(err) 102 | os.Exit(1) 103 | } 104 | 105 | defaultConfig := filepath.Join(dir, "config.toml") 106 | 107 | if !fExists(defaultConfig) { 108 | filePath = defaultConfig 109 | } else { 110 | return errors.New(defaultConfig + " already exists") 111 | } 112 | } else { 113 | abs, err := abspath.ExpandFrom(file) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | filePath = abs.String() 119 | 120 | if !strings.HasSuffix(strings.ToLower(filePath), ".toml") { 121 | filePath = filePath + ".toml" 122 | } 123 | 124 | if fExists(filePath) { 125 | return errors.New(file + " already exists") 126 | } 127 | } 128 | 129 | f, err := os.Create(filePath) 130 | defer f.Close() 131 | if err != nil { 132 | return err 133 | } 134 | 135 | _, err = f.WriteString(baseConfig) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | fmt.Println("Saved empty config to file " + filePath) 141 | 142 | return nil 143 | } 144 | 145 | func fExists(name string) bool { 146 | _, err := os.Stat(name) 147 | return !os.IsNotExist(err) 148 | } 149 | -------------------------------------------------------------------------------- /mysql/query.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "fmt" 7 | "regexp" 8 | "text/template" 9 | 10 | "github.com/gookit/color" 11 | "github.com/mugli/go-kill-mysql-query/configuration" 12 | 13 | "github.com/jmoiron/sqlx" 14 | ) 15 | 16 | const ( 17 | killRegularPrefix = "kill " 18 | killRegularSuffix = ";" 19 | killRdsPrefix = "CALL mysql.rds_kill(" 20 | killRdsSuffix = ");" 21 | 22 | baseQuery = ` 23 | SELECT 24 | ID, 25 | CONCAT('{{.KillPrefix}}', ID, '{{.KillSuffix}}') AS KILL_COMMAND, 26 | DB, 27 | STATE, 28 | COMMAND, 29 | TIME, 30 | INFO 31 | FROM information_schema.PROCESSLIST 32 | WHERE TRUE 33 | AND COMMAND NOT IN ('Sleep', 'Killed') 34 | AND DB IS NOT NULL 35 | {{.DBFilter}} 36 | {{.TimeFilter}} 37 | AND INFO NOT LIKE '%PROCESSLIST%' 38 | ORDER BY TIME DESC 39 | LIMIT 10; 40 | ` 41 | ) 42 | 43 | type queryParams struct { 44 | KillPrefix string 45 | KillSuffix string 46 | DBFilter string 47 | TimeFilter string 48 | } 49 | 50 | type MysqlProcess struct { 51 | ID int `db:"ID"` 52 | KillCommand string `db:"KILL_COMMAND"` 53 | DB string `db:"DB"` 54 | State sql.NullString `db:"STATE"` 55 | Command string `db:"COMMAND"` 56 | Time int `db:"TIME"` 57 | Info sql.NullString `db:"INFO"` 58 | TruncatedQuery string 59 | } 60 | 61 | func generateQuery(config configuration.Config) (string, error) { 62 | tmpl := template.New("query") 63 | 64 | tmpl, err := tmpl.Parse(baseQuery) 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | params := queryParams{} 70 | if config.MySQL.AwsRds { 71 | params.KillPrefix = killRdsPrefix 72 | params.KillSuffix = killRdsSuffix 73 | } else { 74 | params.KillPrefix = killRegularPrefix 75 | params.KillSuffix = killRegularSuffix 76 | } 77 | 78 | params.TimeFilter = fmt.Sprintf("AND TIME >= %d", config.LongQuery.TimeoutSecond) 79 | 80 | if config.MySQL.DB != "" { 81 | params.DBFilter = fmt.Sprintf("AND DB = '%s'", config.MySQL.DB) 82 | } 83 | 84 | var queryBytes bytes.Buffer 85 | err = tmpl.Execute(&queryBytes, params) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | return queryBytes.String(), nil 91 | } 92 | 93 | func truncateString(str string, num int) string { 94 | // Remove newlines 95 | re := regexp.MustCompile(`\r?\n`) 96 | retval := re.ReplaceAllString(str, " ") 97 | 98 | if len(retval) > num { 99 | if num > 3 { 100 | num -= 3 101 | } 102 | retval = retval[0:num] + "..." 103 | } 104 | return retval 105 | } 106 | 107 | func GetLongRunningQueries(dbConn *sqlx.DB, config configuration.Config) ([]MysqlProcess, error) { 108 | fmt.Println("🕴 Looking for long running queries...") 109 | 110 | longQueries := make([]MysqlProcess, 0) 111 | query, err := generateQuery(config) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | if rows, err := dbConn.Queryx(query); err == nil { 117 | for rows.Next() { 118 | longQ := MysqlProcess{} 119 | rows.StructScan(&longQ) 120 | 121 | longQ.TruncatedQuery = truncateString(longQ.Info.String, 50) 122 | 123 | longQueries = append(longQueries, longQ) 124 | } 125 | rows.Close() 126 | } else { 127 | return nil, err 128 | } 129 | 130 | return longQueries, nil 131 | } 132 | 133 | func KillMySQLProcess(killCommand string, dbConn *sqlx.DB) error { 134 | fmt.Println() 135 | fmt.Println("☠️ Sending kill command...") 136 | 137 | if _, err := dbConn.Queryx(killCommand); err != nil { 138 | return err 139 | } 140 | 141 | green := color.FgGreen.Render 142 | fmt.Println("☠️ " + green("Killed it!")) 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /mysql/connect.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net" 7 | "os" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | "golang.org/x/crypto/ssh" 11 | "golang.org/x/crypto/ssh/agent" 12 | 13 | "github.com/mugli/go-kill-mysql-query/configuration" 14 | 15 | "github.com/jmoiron/sqlx" 16 | "github.com/rhysd/abspath" 17 | ) 18 | 19 | type SSHDialer struct { 20 | client *ssh.Client 21 | } 22 | 23 | func (self *SSHDialer) Dial(addr string) (net.Conn, error) { 24 | return self.client.Dial("tcp", addr) 25 | } 26 | 27 | var ( 28 | sshClientConn *ssh.Client 29 | dbConn *sqlx.DB 30 | ) 31 | 32 | func Disconnect() { 33 | if dbConn != nil { 34 | err := dbConn.Close() 35 | 36 | if err != nil { 37 | fmt.Printf("Failure: %s", err.Error()) 38 | } 39 | } 40 | 41 | if sshClientConn != nil { 42 | err := sshClientConn.Close() 43 | 44 | if err != nil { 45 | fmt.Printf("Failure: %s", err.Error()) 46 | } 47 | } 48 | } 49 | 50 | func sshAgent() (ssh.AuthMethod, error) { 51 | sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | authMethod := ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) 58 | return authMethod, nil 59 | } 60 | 61 | func Connect(config configuration.Config) (*sqlx.DB, error) { 62 | useSSHTunnel := config.SSH.UseTunnel 63 | sshHost := config.SSH.Host // SSH Server Hostname/IP 64 | sshPort := config.SSH.Port // SSH Port 65 | sshUser := config.SSH.Username // SSH Username 66 | sshPass := config.SSH.Password // Empty string for no password 67 | sshKey := config.SSH.Key 68 | sshKeyPassphrase := config.SSH.KeyPassphrase 69 | 70 | dbUser := config.MySQL.Username // DB username 71 | dbPass := config.MySQL.Password // DB Password 72 | dbHost := config.MySQL.Host // DB Hostname/IP 73 | dbPort := config.MySQL.Port // DB Port 74 | 75 | var network string 76 | 77 | fmt.Println("🔌 Connecting to database...") 78 | if useSSHTunnel { 79 | sshConfig := &ssh.ClientConfig{ 80 | User: sshUser, 81 | Auth: []ssh.AuthMethod{}, 82 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 83 | } 84 | 85 | if sshKey != "" { 86 | keyPath, err := abspath.ExpandFrom(sshKey) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | key, err := ioutil.ReadFile(keyPath.String()) 92 | 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | var signer ssh.Signer 98 | if sshKeyPassphrase != "" { 99 | signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(sshKeyPassphrase)) 100 | } else { 101 | signer, err = ssh.ParsePrivateKey(key) 102 | } 103 | 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signer)) 109 | } 110 | 111 | if sshPass != "" { 112 | sshConfig.Auth = append(sshConfig.Auth, ssh.Password(sshPass)) 113 | } 114 | 115 | if sshKey == "" && sshPass == "" { 116 | // Try using ssh-agent 117 | agent, err := sshAgent() 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | sshConfig.Auth = append(sshConfig.Auth, agent) 123 | } 124 | 125 | // Connect to the SSH Server 126 | sshClientConn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", sshHost, sshPort), sshConfig) 127 | 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | // Register the SSHDialer with the ssh connection as a parameter 133 | mysql.RegisterDial("mysql+tcp", (&SSHDialer{sshClientConn}).Dial) 134 | 135 | network = "mysql+tcp" 136 | } 137 | 138 | connString := fmt.Sprintf("%s:%s@%s(%s:%d)/", dbUser, dbPass, network, dbHost, dbPort) 139 | dbConn, err := sqlx.Connect("mysql", connString) 140 | 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | fmt.Println("⚡️ Successfully connected to the database") 146 | return dbConn, nil 147 | } 148 | -------------------------------------------------------------------------------- /kill-mysql-query.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/mugli/go-kill-mysql-query/configuration" 9 | "github.com/mugli/go-kill-mysql-query/mysql" 10 | 11 | "github.com/gookit/color" 12 | "github.com/jmoiron/sqlx" 13 | "github.com/manifoldco/promptui" 14 | ) 15 | 16 | func main() { 17 | var config configuration.Config 18 | 19 | if len(os.Args) >= 2 { 20 | switch os.Args[1] { 21 | case "generate", "init": 22 | generateConfig() 23 | case "help", "-h", "--help": 24 | showHelp() 25 | default: 26 | config = readConfig(os.Args[1]) 27 | } 28 | } else { 29 | config = readConfig("") 30 | } 31 | 32 | killQueries(config) 33 | } 34 | 35 | func readConfig(filePath string) configuration.Config { 36 | config, err := configuration.Read(filePath) 37 | 38 | if err != nil { 39 | fmt.Println(err) 40 | os.Exit(1) 41 | } 42 | 43 | return config 44 | } 45 | 46 | func killQueries(config configuration.Config) { 47 | dbConn, err := mysql.Connect(config) 48 | if err != nil { 49 | fmt.Println(err) 50 | os.Exit(1) 51 | } 52 | 53 | defer mysql.Disconnect() 54 | 55 | for { 56 | longQueries, err := mysql.GetLongRunningQueries(dbConn, config) 57 | if err != nil { 58 | fmt.Println(err) 59 | os.Exit(1) 60 | } 61 | 62 | showKillPrompt(longQueries, dbConn, config) 63 | fmt.Println("💫 Rechecking...") 64 | } 65 | } 66 | 67 | func generateConfig() { 68 | var file string 69 | if len(os.Args) > 2 { 70 | file = os.Args[2] 71 | } 72 | 73 | err := configuration.Generate(file) 74 | 75 | if err != nil { 76 | fmt.Println(err) 77 | os.Exit(1) 78 | } 79 | 80 | os.Exit(0) 81 | } 82 | 83 | func showHelp() { 84 | help := ` 85 | _____ ____ 86 | / \ | o | 87 | | |/ ___\| 88 | |_________/ 89 | |_|_| |_|_| 90 | 91 | kill-mysql-query interactively shows long running queries in MySQL database 92 | and provides option to kill them one by one. Great for firefighting. 🔥🚨🚒 93 | 94 | It can connect to MySQL server as configured, using SSH Tunnel if necessary 95 | and let you decide which query to kill. By default queries running for more 96 | than 10 seconds will be marked as long running queries, but it can be configured. 97 | 98 | ------ 99 | Usage: 100 | 101 | kill-mysql-query [config.toml]: 102 | Checks for long running queries in the configured server. 103 | If no file is given, it tries to read from config.toml 104 | in the current directory. 105 | 106 | Other commands: 107 | 108 | generate [config.toml]: 109 | Generates a new empty configuration file 110 | 111 | init: 112 | Alias for generate 113 | 114 | help, --help, -h: 115 | Shows this message 116 | ` 117 | fmt.Println(help) 118 | os.Exit(0) 119 | } 120 | 121 | func showKillPrompt(longQueries []mysql.MysqlProcess, dbConn *sqlx.DB, config configuration.Config) { 122 | if len(longQueries) == 0 { 123 | fmt.Printf("✨ No queries are running for more than %d second(s). Quitting! 👋\n", config.LongQuery.TimeoutSecond) 124 | os.Exit(0) 125 | } 126 | 127 | cyan := color.FgCyan.Render 128 | green := color.FgGreen.Render 129 | 130 | if len(longQueries) == 1 { 131 | fmt.Println() 132 | fmt.Println() 133 | fmt.Printf("❄️ Found %s long running query!\n", cyan("1")) 134 | query := longQueries[0] 135 | label := fmt.Sprintf("🐢 This query is running for %s second(s) in the `%s` database:\n\n%s\n\n", cyan(query.Time), cyan(query.DB), cyan(query.TruncatedQuery)) 136 | 137 | fmt.Println(label) 138 | prompt := promptui.Prompt{ 139 | Label: "🧨 Kill it?", 140 | IsConfirm: true, 141 | Default: "n", 142 | } 143 | 144 | result, _ := prompt.Run() 145 | 146 | if strings.TrimSpace(strings.ToLower(result)) == "y" { 147 | err := mysql.KillMySQLProcess(query.KillCommand, dbConn) 148 | 149 | if err != nil { 150 | fmt.Printf("😓 There was an error killing the query: %v\n", err) 151 | os.Exit(1) 152 | } 153 | } else { 154 | fmt.Println("Quitting! 👋") 155 | os.Exit(0) 156 | } 157 | } 158 | 159 | if len(longQueries) > 1 { 160 | fmt.Println() 161 | fmt.Println() 162 | fmt.Printf("❄️ Found %s long running queries!\n", cyan(len(longQueries))) 163 | fmt.Println() 164 | 165 | templates := &promptui.SelectTemplates{ 166 | Label: "{{ . }}?", 167 | Active: "👉 DB `{{ .DB | cyan }}`, Running Time: {{ .Time | cyan }}s, Query: {{ .TruncatedQuery | cyan }}", 168 | Inactive: " DB `{{ .DB }}`, Running Time: {{ .Time }}s, Query: {{ .TruncatedQuery }}", 169 | Selected: "💥 DB `{{ .DB | cyan }}`, Running Time: {{ .Time | cyan }}s, Query: {{ .TruncatedQuery | cyan }}", 170 | Details: ` 171 | --------- QUERY ---------- 172 | {{ "ID:" | faint }} {{ .ID }} 173 | {{ "DB:" | faint }} {{ .DB }} 174 | {{ "State:" | faint }} {{ .State.String }} 175 | {{ "Command:" | faint }} {{ .Command }} 176 | {{ "Running Time:" | faint }} {{ .Time }} second(s) 177 | {{ "Query:" | faint }} {{ .TruncatedQuery }}`, 178 | } 179 | 180 | label := fmt.Sprintf("Press %s to confirm. Which one to kill?", green("ENTER")) 181 | prompt := promptui.Select{ 182 | Label: label, 183 | Items: longQueries, 184 | Templates: templates, 185 | Size: 10, 186 | } 187 | 188 | i, _, err := prompt.Run() 189 | 190 | if err != nil { 191 | fmt.Printf("😓 There was an error: %v\n", err) 192 | os.Exit(1) 193 | } 194 | 195 | selectedQuery := longQueries[i] 196 | err = mysql.KillMySQLProcess(selectedQuery.KillCommand, dbConn) 197 | 198 | if err != nil { 199 | fmt.Printf("😓 There was an error killing the query: %v\n", err) 200 | os.Exit(1) 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/alecthomas/gometalinter v2.0.11+incompatible h1:ENdXMllZNSVDTJUUVIzBW9CSEpntTrQa76iRsEFLX/M= 4 | github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= 5 | github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU= 6 | github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= 7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= 8 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 10 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 11 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 12 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 13 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 14 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 15 | github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= 16 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 21 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 22 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 23 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 24 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 25 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 26 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 27 | github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3 h1:I4BOK3PBMjhWfQM2zPJKK7lOBGsrsvOB7kBELP33hiE= 28 | github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 29 | github.com/golang/lint v0.0.0-20190409202823-5614ed5bae6fb75893070bdc0996a68765fdd275 h1:BSaiyMr/kqkz4KbXi14W0Xhve79ysYRVu4zCkyh3GV0= 30 | github.com/golang/lint v0.0.0-20190409202823-5614ed5bae6fb75893070bdc0996a68765fdd275/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 31 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 32 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= 33 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= 34 | github.com/gookit/color v1.1.7 h1:WR5I/mhSHzemW2DzG54hTsUb7OzaREvkcmUG4/WST4Q= 35 | github.com/gookit/color v1.1.7/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= 36 | github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY= 37 | github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= 38 | github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 h1:ehVe1P3MbhHjeN/Rn66N2fGLrP85XXO1uxpLhv0jtX8= 39 | github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= 40 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 41 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 42 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 43 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 44 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 45 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 47 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 48 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 49 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 50 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 51 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 52 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 53 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 54 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 55 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 56 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 57 | github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= 58 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 59 | github.com/manifoldco/promptui v0.3.2 h1:rir7oByTERac6jhpHUPErHuopoRDvO3jxS+FdadEns8= 60 | github.com/manifoldco/promptui v0.3.2/go.mod h1:8JU+igZ+eeiiRku4T5BjtKh2ms8sziGpSYl1gN8Bazw= 61 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 62 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 63 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 64 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 65 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 66 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 67 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 68 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 69 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= 70 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 71 | github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= 72 | github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 73 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/rhysd/abspath v0.0.0-20190409124310-ff0e3470a837 h1:K1UeXw5M4HSTxNsqTXX9vgN8n4JjMooAFsr+ybKSij4= 76 | github.com/rhysd/abspath v0.0.0-20190409124310-ff0e3470a837/go.mod h1:6lJvAsQlC7HHuw+YVkjhw+X12knsftTV2IO8AyTvC7I= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 79 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 80 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 81 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 82 | github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c= 83 | github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= 84 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 85 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 86 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 87 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 88 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= 89 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 90 | golang.org/x/lint v0.0.0-20190511005446-959b441ac422 h1:3+pelC/J5RixTQ38KTnqOBzS/c79QHAs1AHTNhkUGco= 91 | golang.org/x/lint v0.0.0-20190511005446-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 92 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 93 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 94 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 95 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 96 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 99 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 101 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs= 103 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20190804062209-51ab0e2deafa h1:1m/9Gp2JBy4grWjxTBTM4+Wx8R7rT8txbjAxYc6zccE= 105 | golang.org/x/sys v0.0.0-20190804062209-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 107 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 108 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 109 | golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1 h1:bsEj/LXbv3BCtkp/rBj9Wi/0Nde4OMaraIZpndHAhdI= 110 | golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 112 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b h1:mSUCVIwDx4hfXJfWsOPfdzEHxzb2Xjl6BQ8YgPnazQA= 113 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 114 | golang.org/x/tools v0.0.0-20190809152137-6d4652c779c4 h1:Lsfy/gJCZ5VDaOHEgAXbjOl0GNOc3+VlziUHXQ6C/dU= 115 | golang.org/x/tools v0.0.0-20190809152137-6d4652c779c4/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 116 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= 118 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 119 | gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c h1:vTxShRUnK60yd8DZU+f95p1zSLj814+5CuEh7NjF2/Y= 120 | gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= 121 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 122 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 123 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 124 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 125 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= 126 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 127 | --------------------------------------------------------------------------------