├── _config.yml ├── Makefile ├── Dockerfile ├── go.mod ├── main.go ├── .gitignore ├── pkg └── pg │ ├── config.go │ ├── sql_result.go │ └── db.go ├── go.sum ├── README.md └── cmd └── root.go /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | ifndef PGPING_VERSION 4 | PGPING_VERSION := $(shell git describe --tags)-dirty 5 | endif 6 | 7 | compile: 8 | go build -o pg-ping -ldflags "-s -w -X main.version=$(PGPING_VERSION)" main.go 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as build 2 | RUN apk add git make 3 | RUN mkdir pg-ping 4 | WORKDIR /opt/pg-ping 5 | ADD . . 6 | RUN make compile 7 | 8 | FROM alpine 9 | COPY --from=build /opt/pg-ping/pg-ping /bin/pg-ping 10 | ENTRYPOINT [ "pg-ping" ] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thecasualcoder/pg-ping 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/fatih/color v1.7.0 // indirect 7 | github.com/hokaccha/go-prettyjson v0.0.0-20180920040306-f579f869bbfe 8 | github.com/lib/pq v1.0.0 9 | github.com/mattn/go-colorable v0.1.1 // indirect 10 | github.com/mattn/go-isatty v0.0.6 // indirect 11 | github.com/urfave/cli v1.20.0 12 | ) 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/thecasualcoder/pg-ping/cmd" 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | var version string 12 | 13 | const defaultVersion = "dev" 14 | 15 | func main() { 16 | var app = cli.NewApp() 17 | if version == "" { 18 | version = defaultVersion 19 | } 20 | app.Version = version 21 | 22 | cli.HelpFlag = cli.BoolFlag{Name: "help"} 23 | 24 | if err := cmd.Execute(app); err != nil { 25 | fmt.Fprintf(os.Stderr, "pg-ping failed: %v\n", err) 26 | os.Exit(1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=go,visualstudiocode 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | ### Go Patch ### 20 | /vendor/ 21 | /Godeps/ 22 | 23 | ### VisualStudioCode ### 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | ### VisualStudioCode Patch ### 31 | # Ignore all local history of files 32 | .history 33 | 34 | # End of https://www.gitignore.io/api/go,visualstudiocode 35 | .env 36 | pg-ping 37 | -------------------------------------------------------------------------------- /pkg/pg/config.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Config contains configurations to connect to a PG database 9 | type Config struct { 10 | Username string 11 | Password string 12 | Host string 13 | DBName string 14 | Query string 15 | FrequencyInMS int32 16 | Debug bool 17 | } 18 | 19 | // ConnStr returns a connection string to connect to postgres 20 | func (c *Config) ConnStr() string { 21 | return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", c.Username, c.Password, c.Host, c.DBName) 22 | } 23 | 24 | // GetQuery returns the query to use to ping 25 | func (c *Config) GetQuery() string { 26 | if c.Query == "" { 27 | return "select 1" 28 | } 29 | 30 | return c.Query 31 | } 32 | 33 | // GetFrequency returns the frequence in MS in which the query should be run 34 | func (c *Config) GetFrequency() time.Duration { 35 | if c.FrequencyInMS == 0 { 36 | return time.Second 37 | } 38 | 39 | return time.Duration(c.FrequencyInMS) * time.Millisecond 40 | } 41 | -------------------------------------------------------------------------------- /pkg/pg/sql_result.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // SQLResult tracks sql response and time taken 9 | type SQLResult struct { 10 | Timestamp QueryStart `json:"timestamp"` 11 | Message string `json:"message"` 12 | TimeTaken QueryTime `json:"time_taken"` 13 | Status QueryStatus `json:"status"` 14 | } 15 | 16 | // QueryStatus represents if the query run was a success or failure 17 | type QueryStatus string 18 | 19 | // QueryStart is the timestamp the query started 20 | type QueryStart time.Time 21 | 22 | // MarshalJSON to format the QueryStart timestamp 23 | func (t QueryStart) MarshalJSON() ([]byte, error) { 24 | stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format("15:04:05")) 25 | return []byte(stamp), nil 26 | } 27 | 28 | // QueryTime represents the amount of time the query took 29 | type QueryTime float64 30 | 31 | // MarshalJSON to format the QueryTime 32 | func (t QueryTime) MarshalJSON() ([]byte, error) { 33 | stamp := fmt.Sprintf("\"%.3fms\"", t) 34 | return []byte(stamp), nil 35 | } 36 | 37 | const ( 38 | success QueryStatus = "success" 39 | failure QueryStatus = "failed" 40 | ) 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 2 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 3 | github.com/hokaccha/go-prettyjson v0.0.0-20180920040306-f579f869bbfe h1:MCgzztuoH5LZNr9AkIaicIDvCfACu11KUCCZQnRHDC0= 4 | github.com/hokaccha/go-prettyjson v0.0.0-20180920040306-f579f869bbfe/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= 5 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 6 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 7 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 8 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 9 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 10 | github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= 11 | github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 12 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= 13 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 14 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 15 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | -------------------------------------------------------------------------------- /pkg/pg/db.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "time" 7 | 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | // DB represents a pingable DB 12 | type DB struct { 13 | db *sql.DB 14 | conf Config 15 | } 16 | 17 | // NewDB creates a new DB connection 18 | func NewDB(conf Config) (*DB, error) { 19 | db, err := sql.Open("postgres", conf.ConnStr()) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &DB{db: db, conf: conf}, nil 24 | } 25 | 26 | // Close a pingable DB 27 | func (db *DB) Close() error { 28 | return db.db.Close() 29 | } 30 | 31 | // PingOnce will execute query only once 32 | func (db *DB) PingOnce() <-chan SQLResult { 33 | result := make(chan SQLResult, 1) 34 | go func() { 35 | defer close(result) 36 | result <- executeQuery(db.db, db.conf.GetQuery()) 37 | }() 38 | return result 39 | } 40 | 41 | // Ping will execute query indefinitely 42 | func (db *DB) Ping() <-chan SQLResult { 43 | result := make(chan SQLResult, 10) 44 | go func() { 45 | defer close(result) 46 | 47 | ticker := time.NewTicker(db.conf.GetFrequency()) 48 | for range ticker.C { 49 | go func() { 50 | result <- executeQuery(db.db, db.conf.GetQuery()) 51 | }() 52 | } 53 | }() 54 | return result 55 | } 56 | 57 | func executeQuery(db *sql.DB, query string) SQLResult { 58 | res := SQLResult{} 59 | 60 | ctx, cancelFunc := context.WithCancel(context.Background()) 61 | tenSecondTimer := time.NewTimer(10 * time.Second) 62 | go func() { 63 | <-tenSecondTimer.C 64 | cancelFunc() 65 | }() 66 | 67 | start := time.Now() 68 | res.Timestamp = QueryStart(start) 69 | rows, err := db.QueryContext(ctx, query) 70 | res.TimeTaken = QueryTime(time.Since(start).Seconds() * 1000) 71 | 72 | if err != nil { 73 | res.Status = failure 74 | res.Message = err.Error() 75 | return res 76 | } 77 | defer rows.Close() 78 | 79 | for rows.Next() { 80 | var message string 81 | if err := rows.Scan(&message); err != nil { 82 | res.Status = failure 83 | res.Message = err.Error() 84 | return res 85 | } 86 | 87 | res.Message = message 88 | res.Status = success 89 | 90 | } 91 | 92 | return res 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg-ping 2 | 3 | `pg-ping` is a command line utility to continously ping your postgres. This is useful to check if there is downtime when doing changes to your postgres instance. 4 | 5 | ## Installation 6 | 7 | Using Homebrew 8 | 9 | ```bash 10 | brew tap thecasualcoder/stable 11 | brew install pg-ping 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```bash 17 | NAME: 18 | pg-ping - Ping your postgres continously 19 | 20 | USAGE: 21 | pg-ping [global options] command [command options] [arguments...] 22 | 23 | VERSION: 24 | v0.1.0 25 | 26 | COMMANDS: 27 | help, h Shows a list of commands or help for one command 28 | 29 | GLOBAL OPTIONS: 30 | --once Ping only once and quit [$PGPING_ONCE] 31 | --debug Print debug logs [$PGPING_DEBUG] 32 | --username value, -U value Username to connect to postgres [$PGPING_USERNAME] 33 | --password value, -p value Password to connect to postgres [$PGPING_PASSWORD] 34 | --host value, -h value Host of postgres server [$PGPING_HOST] 35 | --dbname value, -d value DBName to connect to [$PGPING_DBNAME] 36 | --frequency value, -f value Frequency to ping (default: 0) [$PGPING_FREQUENCY_IN_MS] 37 | --query value, -c value Query to run (default: "SELECT 1") [$PGPING_QUERY] 38 | --help 39 | --version, -v print the version 40 | ``` 41 | 42 | ## Example 43 | 44 | ```bash 45 | $ pg-ping -U myuser -h myhost -d mydb -c 'SELECT 1' 46 | {"Value":"1","TimeTakenInMS":0.408563,"Failed":false,"FailureMessage":""} 47 | {"Value":"1","TimeTakenInMS":0.46392500000000003,"Failed":false,"FailureMessage":""} 48 | {"Value":"","TimeTakenInMS":0.634099,"Failed":true,"FailureMessage":"dial tcp 10.134.125.111:5432: connect: connection refused"} # Downtime 49 | {"Value":"","TimeTakenInMS":0.402107,"Failed":true,"FailureMessage":"dial tcp 10.134.125.111:5432: connect: connection refused"} 50 | {"Value":"1","TimeTakenInMS":10.726904000000001,"Failed":false,"FailureMessage":""} 51 | {"Value":"1","TimeTakenInMS":0.372709,"Failed":false,"FailureMessage":""} 52 | {"Value":"1","TimeTakenInMS":0.429533,"Failed":false,"FailureMessage":""} 53 | ``` 54 | 55 | ## Build locally 56 | 57 | ``` 58 | git clone https://github.com/thecasualcoder/pg-ping.git 59 | make compile 60 | ``` -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | prettyjson "github.com/hokaccha/go-prettyjson" 9 | "github.com/thecasualcoder/pg-ping/pkg/pg" 10 | "github.com/urfave/cli" 11 | ) 12 | 13 | // Execute the app 14 | func Execute(app *cli.App) error { 15 | app.Name = "pg-ping" 16 | app.Usage = "Ping your postgres continously" 17 | app.Action = run 18 | app.Flags = []cli.Flag{ 19 | cli.BoolFlag{ 20 | Name: "once", 21 | Usage: "Ping only once and quit", 22 | EnvVar: "PGPING_ONCE", 23 | }, 24 | cli.BoolFlag{ 25 | Name: "debug", 26 | Usage: "Print debug logs", 27 | EnvVar: "PGPING_DEBUG", 28 | }, 29 | cli.StringFlag{ 30 | Name: "username, U", 31 | Usage: "Username to connect to postgres", 32 | EnvVar: "PGPING_USERNAME", 33 | }, 34 | cli.StringFlag{ 35 | Name: "password, p", 36 | Usage: "Password to connect to postgres", 37 | EnvVar: "PGPING_PASSWORD", 38 | }, 39 | cli.StringFlag{ 40 | Name: "host, h", 41 | Usage: "Host of postgres server", 42 | EnvVar: "PGPING_HOST", 43 | }, 44 | cli.StringFlag{ 45 | Name: "dbname, d", 46 | Usage: "DBName to connect to", 47 | EnvVar: "PGPING_DBNAME", 48 | }, 49 | cli.IntFlag{ 50 | Name: "frequency, f", 51 | Usage: "Frequency to ping", 52 | EnvVar: "PGPING_FREQUENCY_IN_MS", 53 | }, 54 | cli.StringFlag{ 55 | Name: "query, c", 56 | Usage: "Query to run", 57 | EnvVar: "PGPING_QUERY", 58 | Value: "SELECT 1", 59 | }, 60 | } 61 | return app.Run(os.Args) 62 | } 63 | 64 | func run(c *cli.Context) error { 65 | if len(c.Args()) > 0 { 66 | cli.ShowAppHelp(c) 67 | return fmt.Errorf("args are not allowed") 68 | } 69 | conf := pg.Config{ 70 | Username: c.String("username"), 71 | Password: c.String("password"), 72 | Host: c.String("host"), 73 | DBName: c.String("dbname"), 74 | FrequencyInMS: int32(c.Int("frequency")), 75 | Query: c.String("query"), 76 | } 77 | 78 | encoder := json.NewEncoder(os.Stdout) 79 | 80 | if c.BoolT("debug") { 81 | encoder.Encode(conf) 82 | } 83 | 84 | db, err := pg.NewDB(conf) 85 | if err != nil { 86 | return err 87 | } 88 | defer db.Close() 89 | 90 | var resultChan <-chan pg.SQLResult 91 | 92 | if c.BoolT("once") { 93 | resultChan = db.PingOnce() 94 | } else { 95 | resultChan = db.Ping() 96 | } 97 | 98 | formatter := prettyjson.NewFormatter() 99 | 100 | formatter.Newline = "" 101 | formatter.Indent = 0 102 | 103 | for r := range resultChan { 104 | data, _ := formatter.Marshal(r) 105 | fmt.Println(string(data)) 106 | } 107 | 108 | return nil 109 | } 110 | --------------------------------------------------------------------------------