Skip to content

Commit

Permalink
Merge pull request #33 from schigh/master
Browse files Browse the repository at this point in the history
Add SQL Checker
  • Loading branch information
dselans authored Jan 22, 2018
2 parents 95b6ea1 + a0c8877 commit 93121e5
Show file tree
Hide file tree
Showing 8 changed files with 1,070 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*.sketch
/vendor/
.DS_Store
167 changes: 165 additions & 2 deletions checkers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The only **required** attribute is `HTTPConfig.URL` (`*url.URL`).
Refer to the source code for all available attributes on the struct.

### Redis
The Redis checker allows allows you to test that your server is either available (by ping), is able to set a value, is able to get a value or all of the above.
The Redis checker allows you to test that your server is either available (by ping), is able to set a value, is able to get a value or all of the above.

To make use of it, instantiate and fill out a `RedisConfig` struct and pass it to `checkers.NewRedis(...)`.

Expand All @@ -33,7 +33,170 @@ The `RedisConfig` must contain a valid `RedisAuthConfig` and at least _one_ chec
Refer to the godocs for additional info.

### SQL DB
Planned, but PR's welcome!

The SQL DB checker has implementations for the following interfaces:

- `SQLPinger`, which encloses `PingContext` in [`sql.DB`](https://golang.org/pkg/database/sql/#DB.PingContext) and [`sql.Conn`](https://golang.org/pkg/database/sql/#Conn.PingContext)
- `SQLQueryer`, which encloses `QueryContext` in [`sql.DB`](https://golang.org/pkg/database/sql/#DB.QueryContext), [`sql.Conn`](https://golang.org/pkg/database/sql/#Conn.QueryContext), [`sql.Stmt`](https://golang.org/pkg/database/sql/#Stmt.QueryContext), and [`sql.Tx`](https://golang.org/pkg/database/sql/#Tx.QueryContext)
- `SQLExecer`, which encloses `ExecContext` in [`sql.DB`](https://golang.org/pkg/database/sql/#DB.ExecContext), [`sql.Conn`](https://golang.org/pkg/database/sql/#Conn.ExecContext), [`sql.Stmt`](https://golang.org/pkg/database/sql/#Stmt.ExecContext), and [`sql.Tx`](https://golang.org/pkg/database/sql/#Tx.ExecContext)

#### SQLConfig
The `SQLConfig` struct is required when using the SQL DB health check. It **must** contain an inplementation of one of either `SQLPinger`, `SQLQueryer`, or `SQLExecer`.

If `SQLQueryer` or `SQLExecer` are implemented, then `Query` must be valid (len > 0).

Additionally, if `SQLQueryer` or `SQLExecer` are implemented, you have the option to also set either the `QueryerResultHandler` or `ExecerResultHandler` functions. These functions allow you to evaluate the result of a query or exec operation. If you choose not to implement these yourself, the default handlers are used.

The default `ExecerResultHandler` is successful if the passed exec operation affected one and only one row.

The default `QueryerResultHandler` is successful if the passed query operation returned one and only one row.

#### SQLPinger
Use the `SQLPinger` interface if your health check is only concerned with your application's database connectivity. All you need to do is set the `Pinger` value in your `SQLConfig`.

```golang
db, err := sql.Open("mysql", dsn)
if err != nil {
return err
}

sqlCheck, err := checkers.NewSQL(&checkers.SQLConfig{
Pinger: db
})
if err != nil {
return err
}

hc := health.New()
healthCheck.AddCheck(&health.Config{
Name: "sql-check",
Checker: sqlCheck,
Interval: time.Duration(3) * time.Second,
Fatal: true,
})
```

#### SQLQueryer
Use the `SQLQueryer` interface if your health check requires you to read rows from your database. You can optionally supply a query result handler function. If you don't supply one, the default function will be used. The function signature for the handler is:

```golang
type SQLQueryerResultHandler func(rows *sql.Rows) (bool, error)
```
The default query handler returns true if there was exactly one row in the resultset:

```golang
func DefaultQueryHandler(rows *sql.Rows) (bool, error) {
defer rows.Close()
numRows := 0
for rows.Next() {
numRows++
}
return numRows == 1, nil
}
```
**IMPORTANT**: Note that your query handler is responsible for closing the passed `*sql.Rows` value.

Sample `SQLQueryer` implementation:

```golang
// this is our custom query row handler
func myQueryHandler(rows *sql.Rows) (bool, error) {
defer rows.Close()
var healthValue string
for rows.Next() {
// this query will ever return at most one row
if err := rows.Scan(&healthValue); err != nil {
return false, err
}
}
return healthValue == "ok", nil
}

db, err := sql.Open("mysql", dsn)
if err != nil {
return err
}

// we pass the id we are looking for inside the params value
sqlCheck, err := checkers.NewSQL(&checkers.SQLConfig{
Queryerer: db,
Query: "SELECT healthValue FROM some_table WHERE id = ?",
Params: []interface{}{1},
QueryerResultHandler: myQueryHandler
})
if err != nil {
return err
}

hc := health.New()
healthCheck.AddCheck(&health.Config{
Name: "sql-check",
Checker: sqlCheck,
Interval: time.Duration(3) * time.Second,
Fatal: true,
})
```

#### SQLExecer
Use the `SQLExecer` interface if your health check requires you to update or insert to your database. You can optionally supply an exec result handler function. If you don't supply one, the default function will be used. The function signature for the handler is:

```golang
type SQLExecerResultHandler func(result sql.Result) (bool, error)
```
The default exec handler returns true if there was exactly one affected row:

```golang
func DefaultExecHandler(result sql.Result) (bool, error) {
affectedRows, err := result.RowsAffected()
if err != nil {
return false, err
}
return affectedRows == int64(1), nil
}
```

Sample `SQLExecer ` implementation:

```golang
// this is our custom exec result handler
func myExecHandler(result sql.Result) (bool, error) {
insertId, err := result.LastInsertId()
if err != nil {
return false, err
}
// for this example, a check isn't valid
// until after the 100th iteration
return insertId > int64(100), nil
}

db, err := sql.Open("mysql", dsn)
if err != nil {
return err
}

sqlCheck, err := checkers.NewSQL(&checkers.SQLConfig{
Execer: db,
Query: "INSERT INTO checks (checkTS) VALUES (NOW())",
ExecerResultHandler: myExecHandler
})
if err != nil {
return err
}

hc := health.New()
healthCheck.AddCheck(&health.Config{
Name: "sql-check",
Checker: sqlCheck,
Interval: time.Duration(3) * time.Second,
Fatal: true,
})
```

### Mongo
Planned, but PR's welcome!
212 changes: 212 additions & 0 deletions checkers/sql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package checkers

import (
"context"
"database/sql"
"fmt"
)

//go:generate counterfeiter -o ../fakes/isqlpinger.go . SQLPinger
//go:generate counterfeiter -o ../fakes/isqlqueryer.go . SQLQueryer
//go:generate counterfeiter -o ../fakes/isqlexecer.go . SQLExecer

// SQLPinger is an interface that allows direct pinging of the database
type SQLPinger interface {
PingContext(ctx context.Context) error
}

// SQLQueryer is an interface that allows querying of the database
type SQLQueryer interface {
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
}

// SQLExecer is an interface that allows executing of queries in the database
type SQLExecer interface {
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}

// SQLQueryerResultHandler is the BYO function to
// handle the result of an SQL SELECT query
type SQLQueryerResultHandler func(rows *sql.Rows) (bool, error)

// SQLExecerResultHandler is the BYO function
// to handle a database exec result
type SQLExecerResultHandler func(result sql.Result) (bool, error)

// SQLConfig is used for configuring a database check.
// One of the Pinger, Queryer, or Execer fields is required.
//
//
// If Execer is set, it will take precedence over Queryer and Pinger,
// Execer implements the SQLExecer interface in this package.
// The sql.DB and sql.TX structs both implement this interface.
//
// Note that if the Execer is set, then the ExecerResultHandler
// and Query values MUST also be set
//
//
// If Queryer is set, it will take precedence over Pinger.
// SQLQueryer implements the SQLQueryer interface in this package.
// The sql.DB and sql.TX structs both implement this interface.
//
// Note that if the Queryer is set, then the QueryerResultHandler
// and Query values MUST also be set
//
//
// Pinger implements the SQLPinger interface in this package.
// The sql.DB struct implements this interface.
type SQLConfig struct {
// Pinger is the value implementing SQLPinger
Pinger SQLPinger

// Queryer is the value implementing SQLQueryer
Queryer SQLQueryer

// Execer is the value implementing SQLExecer
Execer SQLExecer

// Query is the parameterized SQL query required
// with both Queryer and Execer
Query string

// Params are the SQL query parameters, if any
Params []interface{}

// QueryerResultHandler handles the result of
// the QueryContext function
QueryerResultHandler SQLQueryerResultHandler

// ExecerResultHandler handles the result of
// the ExecContext function
ExecerResultHandler SQLExecerResultHandler
}

// SQL implements the "ICheckable" interface
type SQL struct {
Config *SQLConfig
}

// NewSQL creates a new database checker that can be used for ".AddCheck(s)".
func NewSQL(cfg *SQLConfig) (*SQL, error) {
if err := validateSQLConfig(cfg); err != nil {
return nil, err
}

return &SQL{
Config: cfg,
}, nil
}

// DefaultQueryHandler is the default SQLQueryer result handler
// that assumes one row was returned from the passed query
func DefaultQueryHandler(rows *sql.Rows) (bool, error) {
defer rows.Close()

numRows := 0
for rows.Next() {
numRows++
}

return numRows == 1, nil
}

// DefaultExecHandler is the default SQLExecer result handler
// that assumes one row was affected in the passed query
func DefaultExecHandler(result sql.Result) (bool, error) {
affectedRows, err := result.RowsAffected()
if err != nil {
return false, err
}

return affectedRows == int64(1), nil
}

// this makes sure the sql check is properly configured
func validateSQLConfig(cfg *SQLConfig) error {
if cfg == nil {
return fmt.Errorf("config is required")
}

if cfg.Execer == nil && cfg.Queryer == nil && cfg.Pinger == nil {
return fmt.Errorf("one of Execer, Queryer, or Pinger is required in SQLConfig")
}

if (cfg.Execer != nil || cfg.Queryer != nil) && len(cfg.Query) == 0 {
return fmt.Errorf("SQLConfig.Query is required")
}

return nil
}

// Status is used for performing a database ping against a dependency; it satisfies
// the "ICheckable" interface.
func (s *SQL) Status() (interface{}, error) {
if err := validateSQLConfig(s.Config); err != nil {
return nil, err
}

switch {
// check for SQLExecer first
case s.Config.Execer != nil:
// if the result handler is nil, use the default
if s.Config.ExecerResultHandler == nil {
s.Config.ExecerResultHandler = DefaultExecHandler
}
// run the execer
return s.runExecer()
// check for SQLQueryer next
case s.Config.Queryer != nil:
// if the result handler is nil, use the default
if s.Config.QueryerResultHandler == nil {
s.Config.QueryerResultHandler = DefaultQueryHandler
}
// run the queryer
return s.runQueryer()
// finally, must be a pinger
default:
ctx := context.Background()
return nil, s.Config.Pinger.PingContext(ctx)
}
}

// This will run the execer from the Status func
func (s *SQL) runExecer() (interface{}, error) {
ctx := context.Background()
result, err := s.Config.Execer.ExecContext(ctx, s.Config.Query, s.Config.Params...)
if err != nil {
return nil, err
}

ok, err := s.Config.ExecerResultHandler(result)
if err != nil {
return nil, err
}

if !ok {
return nil, fmt.Errorf("userland exec result handler returned false")
}

return nil, nil
}

// This will run the queryer from the Status func
func (s *SQL) runQueryer() (interface{}, error) {
ctx := context.Background()
rows, err := s.Config.Queryer.QueryContext(ctx, s.Config.Query, s.Config.Params...)
if err != nil {
return nil, err
}

// the BYO result handler is responsible for closing the rows

ok, err := s.Config.QueryerResultHandler(rows)
if err != nil {
return nil, err
}

if !ok {
return nil, fmt.Errorf("userland query result handler returned false")
}

return nil, nil
}
Loading

0 comments on commit 93121e5

Please sign in to comment.