Skip to content

Commit

Permalink
Merge pull request #7 from hellofresh/feature/checkers
Browse files Browse the repository at this point in the history
Added rabbit, redis and pg checkers implementations
  • Loading branch information
vgarvardt authored Sep 22, 2017
2 parents 9f26f20 + c98df3f commit 207d4e0
Show file tree
Hide file tree
Showing 8 changed files with 419 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vendor/
9 changes: 4 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
all: lint test
.PHONY: all

lint:
go vet .
.PHONY: lint
@go vet ./...

test:
go test -v -cover .
.PHONY: test
@go test -v -cover ./...

.PHONY: all test lint
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# health-go

* Exposes an HTTP handler that retrieves health status of the application.
* Exposes an HTTP handler that retrieves health status of the application
* Implements some generic checkers for the following services:
* RabbitMQ
* PostgreSQL
* Redis
* HTTP

## Usage

### Usage
The library exports `Handler` and `HandlerFunc` functions which are fully compatible with `net/http`.

#### Handler
### Handler

```go
package main

Expand Down Expand Up @@ -38,7 +45,7 @@ func main() {
}
```

#### HandlerFunc
### HandlerFunc
```go
package main

Expand Down Expand Up @@ -73,8 +80,10 @@ func main() {
}
```

### API Documentation
#### `GET /status`
## API Documentation

### `GET /status`

Get the health of the application.
- Method: `GET`
- Endpoint: `/status`
Expand Down
65 changes: 65 additions & 0 deletions checks/http/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package http

import (
"context"
"errors"
"net/http"
"time"
)

// Config is the HTTP checker configuration settings container.
type Config struct {
// URL is the remote service health check URL.
URL string
// RequestTimeout is the duration that health check will try to consume published test message.
// If not set - 5 seconds
RequestTimeout time.Duration
// LogFunc is the callback function for errors logging during check.
// If not set logging is skipped.
LogFunc func(err error, details string, extra ...interface{})
}

// New creates new HTTP service health check that verifies the following:
// - connection establishing
// - getting response status from defined URL
// - verifying that status code is less than 500
func New(config Config) func() error {
return func() error {
if config.LogFunc == nil {
config.LogFunc = func(err error, details string, extra ...interface{}) {}
}

if config.RequestTimeout == 0 {
config.RequestTimeout = time.Second * 5
}

req, err := http.NewRequest(http.MethodGet, config.URL, nil)
if err != nil {
config.LogFunc(err, "Creating the request for the health check failed")
return err
}

ctx, cancel := context.WithCancel(context.TODO())

// Inform remote service to close the connection after the transaction is complete
req.Header.Set("Connection", "close")
req = req.WithContext(ctx)

time.AfterFunc(config.RequestTimeout, func() {
cancel()
})

res, err := http.DefaultClient.Do(req)
if err != nil {
config.LogFunc(err, "Making the request for the health check failed")
return err
}
defer res.Body.Close()

if res.StatusCode >= http.StatusInternalServerError {
return errors.New("remote service is not available at the moment")
}

return nil
}
}
123 changes: 123 additions & 0 deletions checks/postgres/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package postgres

import (
"database/sql"
"errors"
"fmt"
"strings"

_ "github.com/lib/pq"
)

// Config is the PostgreSQL checker configuration settings container.
type Config struct {
// DSN is the PostgreSQL instance connection DSN. Required.
DSN string
// Table is the table name used for testing, must already exist in the DB and has insert/select/delete
// privileges for the connection user. Required.
Table string
// IDColumn is the primary column for the table used for testing. Required.
IDColumn string
// InsertColumnsFunc is the callback function that returns map[<column>]<value> for testing insert operation.
// Required.
InsertColumnsFunc func() map[string]interface{}
// LogFunc is the callback function for errors logging during check.
// If not set logging is skipped.
LogFunc func(err error, details string, extra ...interface{})
}

// New creates new PostgreSQL health check that verifies the following:
// - connection establishing
// - inserting a row into defined table
// - selecting inserted row
// - deleting inserted row
func New(config Config) func() error {
return func() error {
if config.LogFunc == nil {
config.LogFunc = func(err error, details string, extra ...interface{}) {}
}

db, err := sql.Open("postgres", config.DSN)
if err != nil {
config.LogFunc(err, "PostgreSQL health check failed during connect")
return err
}

defer func() {
if err = db.Close(); err != nil {
config.LogFunc(err, "PostgreSQL health check failed during connection closing")
}
}()

columns := config.InsertColumnsFunc()
columnNames := []string{}
columnPlaceholders := []string{}
columnValues := []interface{}{}
i := 1
for column, value := range columns {
columnNames = append(columnNames, column)
columnPlaceholders = append(columnPlaceholders, fmt.Sprintf("$%d", i))
columnValues = append(columnValues, value)

i++
}

insertQuery := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s) RETURNING %s",
config.Table,
strings.Join(columnNames, ", "),
strings.Join(columnPlaceholders, ", "),
config.IDColumn,
)

var idValue interface{}
err = db.QueryRow(insertQuery, columnValues...).Scan(&idValue)
if err != nil {
config.LogFunc(err, "PostgreSQL health check failed during insert and scan")
return err
}

selectQuery := fmt.Sprintf(
"SELECT %s FROM %s WHERE %s = $1",
strings.Join(columnNames, ", "),
config.Table,
config.IDColumn,
)
selectRows, err := db.Query(selectQuery, idValue)
if err != nil {
config.LogFunc(err, "PostgreSQL health check failed during select")
return err
}
if !selectRows.Next() {
config.LogFunc(err, "PostgreSQL health check failed during checking select result rows")
return errors.New("looks like select result has 0 rows")
}
err = selectRows.Close()
if err != nil {
config.LogFunc(err, "PostgreSQL health check failed during closing select result")
return err
}

deleteQuery := fmt.Sprintf(
"DELETE FROM %s WHERE %s = $1",
config.Table,
config.IDColumn,
)
deleteResult, err := db.Exec(deleteQuery, idValue)
if err != nil {
config.LogFunc(err, "PostgreSQL health check failed during delete")
return err
}
deleted, err := deleteResult.RowsAffected()
if err != nil {
config.LogFunc(err, "PostgreSQL health check failed during extracting delete result")
return err
}
if deleted < 1 {
config.LogFunc(err, "PostgreSQL health check failed during checking delete result")
return errors.New("looks like delete removed 0 rows")
}

return nil
}
}
33 changes: 33 additions & 0 deletions checks/postgres/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package postgres

import (
"os"
"testing"
"time"
)

const pgDSNEnv = "HEALTH_GO_PG_DSN"

func TestNew(t *testing.T) {
if os.Getenv(pgDSNEnv) == "" {
t.SkipNow()
}

check := New(Config{
DSN: os.Getenv(pgDSNEnv),
Table: "client",
IDColumn: "id",
InsertColumnsFunc: func() map[string]interface{} {
return map[string]interface{}{
"id": time.Now().Format(time.RFC3339Nano),
"secret": time.Now().Format(time.RFC3339Nano),
"extra": time.Now().Format(time.RFC3339Nano),
"redirect_uri": "http://localhost",
}
},
})

if err := check(); err != nil {
t.Fatalf("PostgreSQL check failed: %s", err.Error())
}
}
Loading

0 comments on commit 207d4e0

Please sign in to comment.