Skip to content

Commit

Permalink
Merge pull request #2 from dotse/rewrite
Browse files Browse the repository at this point in the history
✍ Major rewrite
  • Loading branch information
biffen authored Nov 28, 2019
2 parents 79745d1 + e272fd2 commit 91413ed
Show file tree
Hide file tree
Showing 18 changed files with 615 additions and 302 deletions.
26 changes: 0 additions & 26 deletions .github/workflows/build.yml

This file was deleted.

9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

[![License](https://img.shields.io/github/license/dotse/go-health)](https://opensource.org/licenses/MIT)
[![GoDoc](https://img.shields.io/badge/-Documentation-green?logo=go)](https://godoc.org/github.com/dotse/go-health)
[![Actions](https://github.com/dotse/go-health/workflows/Build/badge.svg?branch=master)](https://github.com/dotse/go-health/actions)
[![Releases](https://img.shields.io/github/v/release/dotse/go-health?sort=semver)](https://github.com/dotse/go-health/releases)
[![Issues](https://img.shields.io/github/issues/dotse/go-health)](https://github.com/dotse/go-health/issues)

`go-health` is a Go library for easily setting up monitoring of anything within
an application. Anything that can have a health status can be registered, and
then, as if by magic 🧙, an HTTP server is running and serving a combined health
status.
then an HTTP server can be started to serve a combined health status.

It follows the proposed standard [_Health Check Response Format for HTTP APIs_]
(but you don’t even have to know that, `go-health` takes care of that for you).
Expand Down Expand Up @@ -51,6 +49,7 @@ Then whenever you create your application register it as a health check:
```go
app := NewMyApplication()
health.Register(true, "my-application", app)
server.Start()
```

Either like the above, e.g. in `main()`, or the application could even register
Expand All @@ -67,7 +66,7 @@ To then check the health, GET the response and look at its `status` field.
`go-health` has a function for this too:

```go
resp, err := health.CheckHealth(c.config)
resp, err := client.CheckHealth(c.config)
if err == nil {
fmt.Printf("Status: %s\n", resp.Status)
} else {
Expand All @@ -84,7 +83,7 @@ invoked with the first argument `healthcheck`:
```go
func main() {
if os.Args[1] == "healthcheck" {
health.CheckHealthCommand()
client.CheckHealthCommand()
}

// Your other code...
Expand Down
13 changes: 9 additions & 4 deletions check.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package health

import (
"net/url"
"time"
)

Expand All @@ -17,10 +16,16 @@ type Check struct {
ObservedValue interface{} `json:"observedValue,omitempty"`
ObservedUnit string `json:"observedUnit,omitempty"`
Status Status `json:"status"`
AffectedEndpoints []url.URL `json:"affectedEndpoints,omitempty"`
Time time.Time `json:"time,omitempty"`
AffectedEndpoints []string `json:"affectedEndpoints,omitempty"`
Time *time.Time `json:"time,omitempty"`
Output string `json:"output,omitempty"`
Links []url.URL `json:"links,omitempty"`
Links []string `json:"links,omitempty"`
}

// Good returns true if the Check is good, i.e. its status is ‘pass’ or
// ‘warn’.
func (check *Check) Good() bool {
return check.Status == StatusPass || check.Status == StatusWarn
}

// SetObservedTime sets the observedValue field to a time duration (and the
Expand Down
43 changes: 43 additions & 0 deletions check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright © 2019 The Swedish Internet Foundation
//
// Distributed under the MIT License. (See accompanying LICENSE file or copy at
// <https://opensource.org/licenses/MIT>.)

package health

import (
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestCheck(t *testing.T) {
var check Check

assert.True(t, check.Good())
check.Status = StatusWarn
assert.True(t, check.Good())
check.Status = StatusFail
assert.False(t, check.Good())

check.SetObservedTime(123*time.Microsecond + 456*time.Nanosecond)

check.AffectedEndpoints = []string{"https://example.test/1", "https://example.test/2"}
check.Output = "test output"
check.Links = []string{"https://example.test/about"}

j, err := json.Marshal(check)
assert.NoError(t, err)
assert.JSONEq(t, `
{
"affectedEndpoints": [ "https://example.test/1", "https://example.test/2" ],
"links": [ "https://example.test/about" ],
"observedUnit": "ns",
"observedValue": 123456,
"output": "test output",
"status": "fail"
}
`, string(j))
}
100 changes: 100 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright © 2019 The Swedish Internet Foundation
//
// Distributed under the MIT License. (See accompanying LICENSE file or copy at
// <https://opensource.org/licenses/MIT>.)

package client

import (
"fmt"
"net"
"net/http"
"os"
"strconv"
"time"

"github.com/go-http-utils/headers"

"github.com/dotse/go-health"
"github.com/dotse/go-health/server"
)

const (
// ErrExit is the exit code on failure.
ErrExit = 1

timeout = 30 * time.Second
)

// Config contains configuration for the CheckHealth() function.
type Config struct {
// The hostname. Defaults to 127.0.0.1.
Host string

// The port number. Defaults to 9999.
Port int

// HTTP timeout. Defaults to 30 seconds.
Timeout time.Duration
}

// CheckHealth gets a Response from an HTTP server.
func CheckHealth(config Config) (*health.Response, error) {
if config.Host == "" {
config.Host = "127.0.0.1"
}

if config.Port == 0 {
config.Port = server.Port
}

if config.Timeout == 0 {
config.Timeout = timeout
}

var (
addr = fmt.Sprintf("http://%s/", net.JoinHostPort(config.Host, strconv.Itoa(config.Port)))
client = http.Client{
Timeout: config.Timeout,
}
)

req, err := http.NewRequest(http.MethodGet, addr, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}

req.Header.Add(headers.Accept, server.ContentType)

httpResp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send HTTP request: %w", err)
}

defer httpResp.Body.Close() // nolint: errcheck

resp, err := health.ReadResponse(httpResp.Body)
if err != nil {
return nil, err
}

return resp, nil
}

// CheckHealthCommand is a utility for services that exits the current process
// with 0 or 1 for a healthy or unhealthy state, respectively.
func CheckHealthCommand() {
resp, err := CheckHealth(Config{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
os.Exit(ErrExit)
}

_, _ = resp.Write(os.Stdout)

if resp.Good() {
os.Exit(0)
}

os.Exit(ErrExit)
}
63 changes: 63 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright © 2019 The Swedish Internet Foundation
//
// Distributed under the MIT License. (See accompanying LICENSE file or copy at
// <https://opensource.org/licenses/MIT>.)

package client

import (
"fmt"
"time"

"github.com/dotse/go-health"
"github.com/dotse/go-health/server"
)

func ExampleCheckHealth() {
// The server can be started before registering…
if err := server.Start(); err != nil {
panic(err)
}

// Set up a checker so that there’s something to report.
health.RegisterFunc("example", func() []health.Check {
return []health.Check{{
Status: health.StatusPass,
Output: "all good",
}}
})

// …or after. (Subsequent Start()s do nothing.)
if err := server.Start(); err != nil {
panic(err)
}

// Get the current health status of a server running at localhost. More
// configuration is possible.
resp, err := CheckHealth(Config{
Timeout: time.Minute,
})

if resp == nil || err != nil {
panic(err)
}

fmt.Printf(
`
resp.Status: %q
resp.Checks["example"][0].Status: %q
resp.Checks["example"][0].Output: %q
err: %v
`,
resp.Status,
resp.Checks["example"][0].Status,
resp.Checks["example"][0].Output,
err,
)

// Output:
// resp.Status: "pass"
// resp.Checks["example"][0].Status: "pass"
// resp.Checks["example"][0].Output: "all good"
// err: <nil>
}
30 changes: 17 additions & 13 deletions cmd/healthcheck/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@ import (
"strings"
"time"

"github.com/docker/docker/client"
docker "github.com/docker/docker/client"
"github.com/logrusorgru/aurora"
"github.com/tidwall/pretty"
"golang.org/x/crypto/ssh/terminal"

"github.com/dotse/go-health"
"github.com/dotse/go-health/client"
)

const (
errorKey = "error"
interval = 2 * time.Second
)

//nolint: maligned
type cmd struct {
config health.CheckHealthConfig
config client.Config
continuous bool
interval time.Duration
isatty bool
Expand Down Expand Up @@ -59,7 +63,7 @@ func newCmd() cmd {
// Setting interval implies continuous
c.continuous = c.continuous || c.interval != 0
if c.continuous && c.interval == 0 {
c.interval = 2 * time.Second
c.interval = interval
}

var host string
Expand All @@ -73,7 +77,7 @@ func newCmd() cmd {
host = flag.Arg(0)
}

c.config = health.CheckHealthConfig{
c.config = client.Config{
Port: port,
Host: host,
Timeout: timeout,
Expand All @@ -90,12 +94,12 @@ func (c *cmd) exit() {
var str []string

for status, count := range c.stats {
str = append(str, fmt.Sprintf("\033[%dm%d %s\033[0m", map[string]int{
health.StatusPass.String(): 32,
health.StatusWarn.String(): 33,
health.StatusFail.String(): 31,
errorKey: 91,
}[status], count, status))
str = append(str, map[string]func(interface{}) aurora.Value{
health.StatusPass.String(): aurora.Green,
health.StatusWarn.String(): aurora.Yellow,
health.StatusFail.String(): aurora.Red,
errorKey: aurora.BrightRed,
}[status](fmt.Sprintf("%d %s", count, status)).String())
}

fmt.Printf("\n---\n%s\n", strings.Join(str, ", "))
Expand All @@ -107,7 +111,7 @@ func (c *cmd) exit() {
}

if count > 0 {
os.Exit(1)
os.Exit(client.ErrExit)
}
}

Expand Down Expand Up @@ -146,7 +150,7 @@ func (c *cmd) run() {
go c.wait()

for !c.stop {
resp, err := health.CheckHealth(c.config)
resp, err := client.CheckHealth(c.config)
if err == nil {
c.stats[resp.Status.String()]++
c.print(resp)
Expand Down Expand Up @@ -176,7 +180,7 @@ func (c *cmd) wait() {
}

func getContainerAddress(container string) (string, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
cli, err := docker.NewClientWithOpts(docker.FromEnv)
if err != nil {
return "", err
}
Expand Down
Loading

0 comments on commit 91413ed

Please sign in to comment.