diff --git a/.gitignore b/.gitignore index f1c181e..2316981 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out + +# IDE +.idea/ +pxc-checker +config/test.conf diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..4abdd19 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,24 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +builds: + - + env: + - CGO_ENABLED=0 + binary: pxc-checker +archive: + replacements: + darwin: MacOS + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..80eb524 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: go +go: +- 1.9.x +- tip + +os: +- linux +dist: trusty +sudo: false + +matrix: + allow_failures: + - go: tip + +gobuild_args: -v + +script: + - cd "${TRAVIS_BUILD_DIR}" + - go get + - go build -o pxc-checker ./... \ No newline at end of file diff --git a/checker.go b/checker.go new file mode 100644 index 0000000..39fb2c9 --- /dev/null +++ b/checker.go @@ -0,0 +1,104 @@ +package main + +import ( + "encoding/json" + "fmt" + _ "github.com/go-sql-driver/mysql" + "github.com/valyala/fasthttp" + "strconv" + "time" +) + +type ReasonCode int + +const ( + reasonInternalError ReasonCode = -1 + reasonOk ReasonCode = 0 + reasonForceEnabled ReasonCode = 1 + reasonNodeNotAvailable ReasonCode = 2 + reasonWSRepFailed ReasonCode = 3 + reasonCheckTimeout ReasonCode = 4 + reasonRWDisabled ReasonCode = 5 +) + +type Response struct { + *NodeStatus + ReasonText string + ReasonCode ReasonCode +} + +func checkerHandler(ctx *fasthttp.RequestCtx) { + + response := Response{NodeStatus: status} + ctx.SetContentType("application/json") + + if config.CheckForceEnable { + ctx.SetStatusCode(fasthttp.StatusOK) + response.ReasonText = "Force enabled" + response.ReasonCode = reasonForceEnabled + } else if !status.NodeAvailable { + ctx.SetStatusCode(fasthttp.StatusServiceUnavailable) + response.ReasonText = "Node is not available" + response.ReasonCode = reasonNodeNotAvailable + } else if (status.WSRepStatus != 4) && (status.WSRepStatus != 2) { + ctx.SetStatusCode(fasthttp.StatusServiceUnavailable) + response.ReasonText = "WSRep failed" + response.ReasonCode = reasonWSRepFailed + } else if float64(status.Timestamp)+float64(config.CheckFailTimeout)/1000 < float64(time.Now().Unix()) { + ctx.SetStatusCode(fasthttp.StatusServiceUnavailable) + response.ReasonText = "Check timeout" + response.ReasonCode = reasonCheckTimeout + } else if !config.CheckROEnabled && !status.RWEnabled { + ctx.SetStatusCode(fasthttp.StatusServiceUnavailable) + response.ReasonText = "Node is read only" + response.ReasonCode = reasonRWDisabled + } else { + ctx.SetStatusCode(fasthttp.StatusOK) + response.ReasonText = "OK" + response.ReasonCode = reasonOk + } + + if respJson, err := json.Marshal(response); err != nil { + errStr := fmt.Sprintf(`{"ReasonText":"Internal checker error","ReasonCode":%d,"err":"%s"}`, reasonInternalError, err) + ctx.SetBody([]byte(errStr)) + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + } else { + ctx.SetBody(respJson) + } + + return +} + +func checker(status *NodeStatus) { + for { + time.Sleep(time.Duration(config.CheckInterval) * time.Millisecond) + curStatus := &NodeStatus{} + curStatus.Timestamp = int(time.Now().Unix()) + + rows, err := dbConn.Query("SHOW GLOBAL VARIABLES;") + if err != nil { + *status = *curStatus + continue + } + curStatus.NodeAvailable = true + + for rows.Next() { + var key, value string + err := rows.Scan(&key, &value) + if err != nil { + *status = *curStatus + continue + } + switch key { + case "read_only": + if value == "OFF" { + curStatus.RWEnabled = true + } + case "wsrep_local_state": + curStatus.WSRepStatus, _ = strconv.Atoi(value) + } + } + + *status = *curStatus + } +} diff --git a/config/example.conf b/config/example.conf new file mode 100644 index 0000000..549cda1 --- /dev/null +++ b/config/example.conf @@ -0,0 +1,11 @@ +WEB_LISTEN=":9200" + +MYSQL_HOST="127.0.0.1" +MYSQL_PORT=3306 +MYSQL_USER="monitor" +MYSQL_PASS="monitorUserPassword" + +CHECK_RO_ENABLED=false +CHECK_FORCE_ENABLE=false +CHECK_INTERVAL=500 # ms +CHECK_FAIL_TIMEOUT=3000 # ms diff --git a/main.go b/main.go new file mode 100644 index 0000000..1a4f9ee --- /dev/null +++ b/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "database/sql" + "fmt" + "github.com/buaazp/fasthttprouter" + "github.com/labstack/gommon/log" + "github.com/namsral/flag" + "github.com/valyala/fasthttp" + "time" +) + +type NodeStatus struct { + WSRepStatus int + RWEnabled bool + NodeAvailable bool + Timestamp int +} + +type Config struct { + WebListen string + WebReadTimeout int + WebWriteTimeout int + CheckROEnabled bool + CheckInterval int + CheckFailTimeout int + CheckForceEnable bool + MysqlHost string + MysqlPort int + MysqlUser string + MysqlPass string + MysqlTimeout int +} + +var ( + status = &NodeStatus{} + config *Config + dbConn *sql.DB +) + +func main() { + var err error + config, err = parseFlags() + if err != nil { + log.Fatalf("Options parsing failed with err: %s", err) + } + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/", config.MysqlUser, config.MysqlPass, config.MysqlHost, config.MysqlPort) + log.Printf("Connecting to mysql with dsn: %s", dsn) + dbConn, _ = sql.Open("mysql", dsn) + + go checker(status) + + router := getRouter() + server := &fasthttp.Server{ + Handler: router.Handler, + DisableKeepalive: true, + GetOnly: true, + Concurrency: 2048, + ReadTimeout: time.Duration(config.WebReadTimeout) * time.Millisecond, + WriteTimeout: time.Duration(config.WebWriteTimeout) * time.Millisecond, + } + + log.Printf("Server starting on %s", config.WebListen) + if err := server.ListenAndServe(config.WebListen); err != nil { + log.Fatalf("Error in ListenAndServe: %s", err) + } +} + +func getRouter() *fasthttprouter.Router { + router := fasthttprouter.New() + router.GET("/", checkerHandler) + return router +} + +func parseFlags() (*Config, error) { + config := Config{} + flag.StringVar(&config.WebListen, "WEB_LISTEN", ":9200", "Listen interface and port") + flag.IntVar(&config.WebReadTimeout, "WEB_READ_TIMEOUT", 30000, "Request read timeout, ms") + flag.IntVar(&config.WebWriteTimeout, "WEB_WRITE_TIMEOUT", 30000, "Request write timeout, ms") + flag.BoolVar(&config.CheckROEnabled, "CHECK_RO_ENABLED", false, "Make 'read_only' nodes availible") + flag.BoolVar(&config.CheckForceEnable, "CHECK_FORCE_ENABLE", false, "Ignoring checks status and force enable node") + flag.IntVar(&config.CheckInterval, "CHECK_INTERVAL", 500, "Mysql checks interval, ms") + flag.IntVar(&config.CheckFailTimeout, "CHECK_FAIL_TIMEOUT", 3000, "To count a node inaccessible if for the specified time there were no successful checks, ms") + flag.StringVar(&config.MysqlHost, "MYSQL_HOST", "127.0.0.1", "MySQL host addr") + flag.IntVar(&config.MysqlPort, "MYSQL_PORT", 3306, "MySQL port addr") + flag.StringVar(&config.MysqlUser, "MYSQL_USER", "monitor", "MySQL username") + flag.StringVar(&config.MysqlPass, "MYSQL_PASS", "", "MySQL password") + + flag.Parse() + return &config, nil +} diff --git a/systemd/pxc-check@.service b/systemd/pxc-check@.service new file mode 100644 index 0000000..fd63c4f --- /dev/null +++ b/systemd/pxc-check@.service @@ -0,0 +1,13 @@ +[Unit] +Description=PXC Checker +After=network.target + +[Service] +EnvironmentFile=/etc/pxc/checker/%i.conf +ExecStart=/usr/bin/pxc-checker +KillMode=process +User=mysql +Group=mysql + +[Install] +WantedBy=multi-user.target \ No newline at end of file