Skip to content
This repository has been archived by the owner on Oct 2, 2022. It is now read-only.

Commit

Permalink
Added health check client
Browse files Browse the repository at this point in the history
  • Loading branch information
Janos Pasztor committed Jun 9, 2021
1 parent ed77002 commit 49abd49
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 52 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Changelog

## 1.0.1
## 1.1.0: Added health check client

This release adds a dedicated client library for health checks. It also adds an "Enable" field to the configuration.

## 1.0.1: JSON and YAML tags

Add JSON and YAML tag as well as a sane default for the HTTP configuration.

## 1.0.0
## 1.0.0: Initial version

Initial version of a health check service using an external configuration.
Initial version of a health check service using an external configuration.
7 changes: 7 additions & 0 deletions CODES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Message / error codes

| Code | Explanation |
|------|-------------|
| `HEALTH_AVAILABLE` | The health check service is now online and ready for service. |
| `HEALTH_REQUEST_FAILED` | A request to the health check endpoint failed. |

24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ You can instantiate this service as described in the [service library](https://g
```go
svc, err := health.New(
health.Config{
Enable: true
ServerConfiguration: http.ServerConfiguration{
Listen: "0.0.0.0:23074",
},
Expand All @@ -33,4 +34,25 @@ You can change the `ok`/`not ok` status by calling `srv.ChangeStatus(bool)`, lik

```go
srv.ChangeStatus(true)
```
```

## Health check client

This library also provides a built-in client for running health checks. This can be used as follows:

```go
client, err := health.NewClient(
health.Config{
Enable: true
Client: http.ClientConfiguration{
URL: "http://0.0.0.0:23074",
},
},
logger)
)
if client.Run() {
// Success
} else {
// Failed
}
```
7 changes: 7 additions & 0 deletions codes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package health

// The health check service is now online and ready for service.
const MServiceAvailable = "HEALTH_AVAILABLE"

// A request to the health check endpoint failed.
const ERequestFailed = "HEALTH_REQUEST_FAILED"
3 changes: 3 additions & 0 deletions codes_doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package health

//go:generate containerssh-generate-codes
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/containerssh/health
go 1.16

require (
github.com/containerssh/http v1.1.0
github.com/containerssh/http v1.2.0
github.com/containerssh/log v1.1.6
github.com/containerssh/service v1.0.0
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
github.com/containerssh/http v1.1.0 h1:N3nYCra07//pYyVCW/ryZTDh/G4CHFzd5ht27pUtKu0=
github.com/containerssh/http v1.1.0/go.mod h1:0czFGNEJTPpEjkSp/YcPseNzJSOvwc8H5rMRpQQ+Kj4=
github.com/containerssh/http v1.2.0 h1:PH6LxDErDZyryLfGJduI4F15puVlOyUP6MApwS8yV4k=
github.com/containerssh/http v1.2.0/go.mod h1:aUBi5CsAZsvYBn6NYeT3oUwnvzZRkneL/k2o2pOMdU0=
github.com/containerssh/log v1.0.0/go.mod h1:7Gy+sx0H1UDtjYBySvK0CnXRRHPHZPXMsa9MYmLBI0I=
github.com/containerssh/log v1.1.3/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
github.com/containerssh/log v1.1.6 h1:5fBBZA2MZss5NMUP2FgJ/iERNwN4y+68uwRE8XjbEKA=
github.com/containerssh/log v1.1.6/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
github.com/containerssh/service v1.0.0 h1:+AcBsmeaR0iMDnemXJ6OgeTEYB3C0YJF3h03MNIo1Ak=
Expand Down
88 changes: 74 additions & 14 deletions health.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,92 @@
package health

import (
"fmt"

"github.com/containerssh/http"
"github.com/containerssh/log"
"github.com/containerssh/service"
)

// Config is the configuration for the HealthCheckService.
// Config is the configuration for the Service.
type Config struct {
http.ServerConfiguration `json:",inline" yaml:",inline" default:"{\"listen\":\"0.0.0.0:23074\"}"`
Enable bool `json:"enable" yaml:"enable"`
http.ServerConfiguration `json:",inline" yaml:",inline" default:"{\"listen\":\"0.0.0.0:7000\"}"`
Client http.ClientConfiguration `json:"client" yaml:"client" default:"{\"url\":\"http://127.0.0.1:7000/\"}"`
}

// New creates a new HTTP health service on port 23074
func New(config Config, logger log.Logger) (HealthCheckService, error) {
func (c Config) Validate() error {
if !c.Enable {
return nil
}
if err := c.ServerConfiguration.Validate(); err != nil {
return err
}
if err := c.Client.Validate(); err != nil {
return fmt.Errorf("invalid client configuration (%w)", err)
}
return nil
}

rh := &requestHandler{}
// New creates a new HTTP health service on port 23074
func New(config Config, logger log.Logger) (Service, error) {
if err := config.Validate(); err != nil {
return nil, err
}

handler := http.NewServerHandler(rh, logger)
handler := &requestHandler{}
svc, err := http.NewServer(
"health",
"Health check endpoint",
config.ServerConfiguration,
handler,
http.NewServerHandlerNegotiate(handler, logger),
logger,
func(url string) {},
func(url string) {
logger.Info(log.NewMessage(MServiceAvailable, "Health check endpoint available at %s", url))
},
)

if err != nil {
return nil, err
}

return &healthCheckService{
Service: svc,
requestHandler: rh,
requestHandler: handler,
}, nil
}

// NewClient creates a new health check client based on the supplied configuration. If the health check is not enabled
// no client is returned.
func NewClient(config Config, logger log.Logger) (Client, error) {
if !config.Enable {
return nil, nil
}
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid health check configuration (%w)", err)
}

httpClient, err := http.NewClient(config.Client, logger)
if err != nil {
return nil, fmt.Errorf("failed to create health check client (%w)", err)
}

return &healthCheckClient{
httpClient: httpClient,
logger: logger,
}, nil
}

// HealthCheckService is an HTTP service that lets you change the status via ChangeStatus().
type HealthCheckService interface {
// Service is an HTTP service that lets you change the status via ChangeStatus().
type Service interface {
service.Service
ChangeStatus(ok bool)
}

// Client is the client to run health checks.
type Client interface {
// Run runs a HTTP query against the health check service.
Run() bool
}

type healthCheckService struct {
service.Service
requestHandler *requestHandler
Expand All @@ -54,7 +100,7 @@ type requestHandler struct {
ok bool
}

func (r requestHandler) OnRequest(request http.ServerRequest, response http.ServerResponse) error {
func (r requestHandler) OnRequest(_ http.ServerRequest, response http.ServerResponse) error {
if r.ok {
response.SetBody("ok")
} else {
Expand All @@ -63,3 +109,17 @@ func (r requestHandler) OnRequest(request http.ServerRequest, response http.Serv
}
return nil
}

type healthCheckClient struct {
httpClient http.Client
logger log.Logger
}

func (h *healthCheckClient) Run() bool {
responseBody := ""
statusCode, err := h.httpClient.Get("", &responseBody)
if err != nil {
h.logger.Warning(log.Wrap(err, ERequestFailed, "Request to health check endpoint failed"))
}
return statusCode == 200
}
53 changes: 20 additions & 33 deletions health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,26 @@ import (

func TestOk(t *testing.T) {
logger := log.NewTestLogger(t)
srv, err := health.New(
health.Config{
ServerConfiguration: http.ServerConfiguration{
Listen: "127.0.0.1:23074",
},
config := health.Config{
Enable: true,
ServerConfiguration: http.ServerConfiguration{
Listen: "127.0.0.1:23074",
},
logger)
Client: http.ClientConfiguration{
URL: "http://127.0.0.1:23074",
AllowRedirects: false,
Timeout: 5 * time.Second,
},
}

srv, err := health.New(config, logger)
if err != nil {
t.Fatal(err)
}

l := service.NewLifecycle(srv)

running := make(chan struct{})

l.OnRunning(func(s service.Service, l service.Lifecycle) {
running <- struct{}{}
})
Expand All @@ -37,39 +41,22 @@ func TestOk(t *testing.T) {

<-running

client, err := http.NewClient(http.ClientConfiguration{
URL: "http://127.0.0.1:23074",
AllowRedirects: false,
Timeout: 5 * time.Second,
},
logger,
)

client, err := health.NewClient(config, logger)
if err != nil {
t.Fatal(err)
}

srv.ChangeStatus(true)
checkStatusResponse(t, client, 200, "ok")

srv.ChangeStatus(false)

checkStatusResponse(t, client, 503, "not ok")
}

func checkStatusResponse(t *testing.T, client http.Client, expectedStatusCode int, expectedResponse string) {
response := ""
status, err := client.Get("", &response)

if err != nil {
t.Fatal(err)
if client.Run() {
t.Fatal("Health check did not fail, even though status is false.")
}

if status != expectedStatusCode {
t.Fatalf("Unexpected status code: %d", status)
srv.ChangeStatus(true)
if !client.Run() {
t.Fatal("Health check failed, even though status is true.")
}

if response != expectedResponse {
t.Fatalf("Unexpected response: %s", response)
srv.ChangeStatus(false)
if client.Run() {
t.Fatal("Health check did not fail, even though status is false.")
}
}

0 comments on commit 49abd49

Please sign in to comment.