Adding upstream version 1.6.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
ea7b92a91b
commit
9412e3a223
13 changed files with 568 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
3
.github/codecov.yml
vendored
Normal file
3
.github/codecov.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
coverage:
|
||||
status:
|
||||
patch: off
|
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
labels: ["dependencies"]
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "sunday"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 1
|
||||
labels: ["dependencies"]
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "sunday"
|
30
.github/workflows/test.yml
vendored
Normal file
30
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: test
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
- uses: actions/checkout@v3
|
||||
- name: Benchmark
|
||||
run: go test -bench . -race
|
||||
- name: Test (race)
|
||||
run: go test -mod vendor ./... -race
|
||||
- name: Test (coverage)
|
||||
run: go test -mod vendor ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v3.1.1
|
||||
with:
|
||||
files: ./coverage.txt
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
*.iml
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 TwiN
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
5
Makefile
Normal file
5
Makefile
Normal file
|
@ -0,0 +1,5 @@
|
|||
bench:
|
||||
go test -bench . -race
|
||||
|
||||
test:
|
||||
go test . -race
|
92
README.md
Normal file
92
README.md
Normal file
|
@ -0,0 +1,92 @@
|
|||
# health
|
||||

|
||||
[](https://goreportcard.com/report/github.com/TwiN/health)
|
||||
[](https://codecov.io/gh/TwiN/health)
|
||||
[](https://github.com/TwiN/health)
|
||||
[](https://pkg.go.dev/github.com/TwiN/health)
|
||||
|
||||
Health is a library used for creating a very simple health endpoint.
|
||||
|
||||
While implementing a health endpoint is very simple, I've grown tired of implementing
|
||||
it over and over again.
|
||||
|
||||
|
||||
## Installation
|
||||
```console
|
||||
go get -u github.com/TwiN/health
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
To retrieve the handler, you must use `health.Handler()` and are expected to pass it to the router like so:
|
||||
```go
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/health", health.Handler())
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: router,
|
||||
}
|
||||
```
|
||||
|
||||
By default, the handler will return `UP` when the status is up, and `DOWN` when the status is down.
|
||||
If you prefer using JSON, however, you may initialize the health handler like so:
|
||||
```go
|
||||
router.Handle("/health", health.Handler().WithJSON(true))
|
||||
```
|
||||
The above will cause the response body to become `{"status":"UP"}` and `{"status":"DOWN"}` for both status respectively,
|
||||
unless there is a reason, in which case a reason set to `because` would return `{"status":"UP", "reason":"because"}`
|
||||
and `{"status":"DOWN", "reason":"because"}` respectively.
|
||||
|
||||
To set the health status to `DOWN` with a reason:
|
||||
```go
|
||||
health.SetUnhealthy("<enter reason here>")
|
||||
```
|
||||
The string passed will be automatically set as the reason.
|
||||
|
||||
In a similar fashion, to set the health status to `UP` and clear the reason:
|
||||
```go
|
||||
health.SetHealthy()
|
||||
```
|
||||
|
||||
|
||||
Alternatively, to set the status and the reason individually you can use `health.SetStatus(<status>)` where `<status>` is `health.Up`
|
||||
or `health.Down`:
|
||||
```go
|
||||
health.SetStatus(health.Up)
|
||||
health.SetStatus(health.Down)
|
||||
```
|
||||
As for the reason:
|
||||
```go
|
||||
health.SetReason("database is unreachable")
|
||||
```
|
||||
|
||||
Generally speaking, you'd only want to include a reason if the status is `Down`, but you can do as you desire.
|
||||
|
||||
For the sake of convenience, you can also use `health.SetStatusAndReason(<status>, <reason>)` instead of doing
|
||||
`health.SetStatus(<status>)` and `health.SetReason(<reason>)` separately.
|
||||
|
||||
|
||||
### Complete example
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/health"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/health", health.Handler())
|
||||
server := &http.Server{
|
||||
Addr: "0.0.0.0:8080",
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
server.ListenAndServe()
|
||||
}
|
||||
```
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module github.com/TwiN/health
|
||||
|
||||
go 1.19
|
142
health.go
Normal file
142
health.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
handler = &healthHandler{
|
||||
useJSON: false,
|
||||
status: Up,
|
||||
}
|
||||
)
|
||||
|
||||
// responseBody is the body of the response returned by the health handler.
|
||||
type responseBody struct {
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// healthHandler is the HTTP handler for serving the health endpoint
|
||||
type healthHandler struct {
|
||||
useJSON bool
|
||||
|
||||
status Status
|
||||
reason string
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// WithJSON configures whether the handler should output a response in JSON or in raw text
|
||||
//
|
||||
// Defaults to false
|
||||
func (h *healthHandler) WithJSON(v bool) *healthHandler {
|
||||
h.useJSON = v
|
||||
return h
|
||||
}
|
||||
|
||||
// ServeHTTP serves the HTTP request for the health handler
|
||||
func (h *healthHandler) ServeHTTP(writer http.ResponseWriter, _ *http.Request) {
|
||||
statusCode, body, useJSON := h.getResponseStatusCodeAndBodyAndWhetherBodyUsesJSON()
|
||||
if useJSON {
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
writer.WriteHeader(statusCode)
|
||||
_, _ = writer.Write(body)
|
||||
}
|
||||
|
||||
func (h *healthHandler) GetResponseStatusCodeAndBody() (statusCode int, body []byte) {
|
||||
statusCode, body, _ = h.getResponseStatusCodeAndBodyAndWhetherBodyUsesJSON()
|
||||
return statusCode, body
|
||||
}
|
||||
|
||||
func (h *healthHandler) getResponseStatusCodeAndBodyAndWhetherBodyUsesJSON() (statusCode int, body []byte, useJSON bool) {
|
||||
var status Status
|
||||
var reason string
|
||||
h.mutex.RLock()
|
||||
status, reason, useJSON = h.status, h.reason, h.useJSON
|
||||
h.mutex.RUnlock()
|
||||
if status == Up {
|
||||
statusCode = http.StatusOK
|
||||
} else {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
if useJSON {
|
||||
// We can safely ignore the error here because we know that both values are strings, therefore are supported encoders.
|
||||
body, _ = json.Marshal(responseBody{Status: string(status), Reason: reason})
|
||||
} else {
|
||||
if len(reason) == 0 {
|
||||
body = []byte(status)
|
||||
} else {
|
||||
body = []byte(string(status) + ": " + reason)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handler retrieves the health handler
|
||||
func Handler() *healthHandler {
|
||||
return handler
|
||||
}
|
||||
|
||||
// GetStatus retrieves the current status returned by the health handler
|
||||
func GetStatus() Status {
|
||||
handler.mutex.RLock()
|
||||
defer handler.mutex.RUnlock()
|
||||
return handler.status
|
||||
}
|
||||
|
||||
// SetStatus sets the status to be returned by the health handler
|
||||
func SetStatus(status Status) {
|
||||
handler.mutex.Lock()
|
||||
handler.status = status
|
||||
handler.mutex.Unlock()
|
||||
}
|
||||
|
||||
// GetReason retrieves the current status returned by the health handler
|
||||
func GetReason() string {
|
||||
handler.mutex.RLock()
|
||||
defer handler.mutex.RUnlock()
|
||||
return handler.reason
|
||||
}
|
||||
|
||||
// SetReason sets a reason for the current status to be returned by the health handler
|
||||
func SetReason(reason string) {
|
||||
handler.mutex.Lock()
|
||||
handler.reason = reason
|
||||
handler.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetStatusAndReason sets the status and reason to be returned by the health handler
|
||||
func SetStatusAndReason(status Status, reason string) {
|
||||
handler.mutex.Lock()
|
||||
handler.status = status
|
||||
handler.reason = reason
|
||||
handler.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetStatusAndResetReason sets the status and resets the reason to a blank string
|
||||
func SetStatusAndResetReason(status Status) {
|
||||
handler.mutex.Lock()
|
||||
handler.status = status
|
||||
handler.reason = ""
|
||||
handler.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetHealthy sets the status to Up and the reason to a blank string
|
||||
func SetHealthy() {
|
||||
SetStatusAndResetReason(Up)
|
||||
}
|
||||
|
||||
// SetUnhealthy sets the status to Down and the reason to the string passed as parameter
|
||||
//
|
||||
// Unlike SetHealthy, this function enforces setting a reason, because it's good practice to give at least a bit
|
||||
// of information as to why an application is unhealthy, and this library attempts to promote good practices.
|
||||
func SetUnhealthy(reason string) {
|
||||
handler.mutex.Lock()
|
||||
handler.status = Down
|
||||
handler.reason = reason
|
||||
handler.mutex.Unlock()
|
||||
}
|
27
health_bench_test.go
Normal file
27
health_bench_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package health_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/health"
|
||||
)
|
||||
|
||||
func BenchmarkHealthHandler_ServeHTTP(b *testing.B) {
|
||||
h := health.Handler().WithJSON(true)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
request, _ := http.NewRequest("GET", "/health", http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
h.ServeHTTP(responseRecorder, request)
|
||||
if n := rand.Intn(100); n < 1 {
|
||||
health.SetStatus(health.Down)
|
||||
} else if n < 5 {
|
||||
health.SetStatus(health.Up)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.ReportAllocs()
|
||||
}
|
219
health_test.go
Normal file
219
health_test.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package health_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/health"
|
||||
)
|
||||
|
||||
func TestHealthHandler_ServeHTTP(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
type Scenario struct {
|
||||
Name string
|
||||
useJSON bool
|
||||
status health.Status
|
||||
reason string
|
||||
expectedResponseBody string
|
||||
expectedResponseCode int
|
||||
}
|
||||
scenarios := []Scenario{
|
||||
{
|
||||
Name: "text-up",
|
||||
useJSON: false,
|
||||
status: health.Up,
|
||||
expectedResponseBody: "UP",
|
||||
expectedResponseCode: 200,
|
||||
},
|
||||
{
|
||||
Name: "text-up-reason",
|
||||
useJSON: false,
|
||||
status: health.Up,
|
||||
reason: "reason",
|
||||
expectedResponseBody: "UP: reason",
|
||||
expectedResponseCode: 200,
|
||||
},
|
||||
{
|
||||
Name: "text-down",
|
||||
useJSON: false,
|
||||
status: health.Down,
|
||||
expectedResponseBody: "DOWN",
|
||||
expectedResponseCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "text-down-reason",
|
||||
useJSON: false,
|
||||
status: health.Down,
|
||||
reason: "reason",
|
||||
expectedResponseBody: "DOWN: reason",
|
||||
expectedResponseCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "json-up",
|
||||
useJSON: true,
|
||||
status: health.Up,
|
||||
expectedResponseBody: `{"status":"UP"}`,
|
||||
expectedResponseCode: 200,
|
||||
},
|
||||
{
|
||||
Name: "json-up-reason",
|
||||
useJSON: true,
|
||||
status: health.Up,
|
||||
reason: "Error",
|
||||
expectedResponseBody: `{"status":"UP","reason":"Error"}`,
|
||||
expectedResponseCode: 200,
|
||||
},
|
||||
{
|
||||
Name: "json-down",
|
||||
useJSON: true,
|
||||
status: health.Down,
|
||||
expectedResponseBody: `{"status":"DOWN"}`,
|
||||
expectedResponseCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "json-down-reason",
|
||||
useJSON: true,
|
||||
status: health.Down,
|
||||
reason: "Error",
|
||||
expectedResponseBody: `{"status":"DOWN","reason":"Error"}`,
|
||||
expectedResponseCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "json-down-reason-with-quotes",
|
||||
useJSON: true,
|
||||
status: health.Down,
|
||||
reason: `error "with" quotes`,
|
||||
expectedResponseBody: `{"status":"DOWN","reason":"error \"with\" quotes"}`,
|
||||
expectedResponseCode: 500,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
handler := health.Handler().WithJSON(scenario.useJSON)
|
||||
health.SetStatus(scenario.status)
|
||||
health.SetReason(scenario.reason)
|
||||
|
||||
request, _ := http.NewRequest("GET", "/health", http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.expectedResponseCode {
|
||||
t.Errorf("expected GET /health to return status code %d, got %d", scenario.expectedResponseCode, responseRecorder.Code)
|
||||
}
|
||||
body, _ := io.ReadAll(responseRecorder.Body)
|
||||
if string(body) != scenario.expectedResponseBody {
|
||||
t.Errorf("expected GET /health to return %s, got %s", scenario.expectedResponseBody, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthHandler_GetResponseStatusCodeAndBody(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
handler := health.Handler().WithJSON(true)
|
||||
health.SetStatus(health.Up)
|
||||
|
||||
statusCode, body := handler.GetResponseStatusCodeAndBody()
|
||||
if statusCode != 200 {
|
||||
t.Error("expected status code to be 200, got", statusCode)
|
||||
}
|
||||
if string(body) != `{"status":"UP"}` {
|
||||
t.Error("expected body to be {\"status\":\"UP\"}, got", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetStatus(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
health.SetStatus(health.Up)
|
||||
if health.GetStatus() != health.Up {
|
||||
t.Error("expected status to be 'Up', got", health.GetStatus())
|
||||
}
|
||||
health.SetStatus(health.Down)
|
||||
if health.GetStatus() != health.Down {
|
||||
t.Error("expected status to be 'Down', got", health.GetStatus())
|
||||
}
|
||||
health.SetStatus(health.Up)
|
||||
}
|
||||
|
||||
func TestSetReason(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
health.SetReason("hello")
|
||||
if health.GetReason() != "hello" {
|
||||
t.Error("expected reason to be 'hello', got", health.GetReason())
|
||||
}
|
||||
health.SetReason("world")
|
||||
if health.GetReason() != "world" {
|
||||
t.Error("expected reason to be 'world', got", health.GetReason())
|
||||
}
|
||||
health.SetReason("")
|
||||
if health.GetReason() != "" {
|
||||
t.Error("expected reason to be '', got", health.GetReason())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetStatusAndReason(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
health.SetStatusAndReason(health.Down, "for what")
|
||||
if health.GetStatus() != health.Down {
|
||||
t.Error("expected status to be 'Down', got", health.GetStatus())
|
||||
}
|
||||
if health.GetReason() != "for what" {
|
||||
t.Error("expected reason to be 'hello', got", health.GetReason())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetStatusAndResetReason(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
health.SetStatusAndReason(health.Down, "for what")
|
||||
if health.GetStatus() != health.Down {
|
||||
t.Error("expected status to be 'Down', got", health.GetStatus())
|
||||
}
|
||||
if health.GetReason() != "for what" {
|
||||
t.Error("expected reason to be 'for what', got", health.GetReason())
|
||||
}
|
||||
health.SetStatusAndResetReason(health.Up)
|
||||
if health.GetStatus() != health.Up {
|
||||
t.Error("expected status to be 'Up', got", health.GetStatus())
|
||||
}
|
||||
if health.GetReason() != "" {
|
||||
t.Error("expected reason to be '', got", health.GetReason())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHealthy(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
health.SetStatusAndReason(health.Down, "for what")
|
||||
if health.GetStatus() != health.Down {
|
||||
t.Error("expected status to be 'Down', got", health.GetStatus())
|
||||
}
|
||||
if health.GetReason() != "for what" {
|
||||
t.Error("expected reason to be 'for what', got", health.GetReason())
|
||||
}
|
||||
health.SetHealthy()
|
||||
if health.GetStatus() != health.Up {
|
||||
t.Error("expected status to be 'Up', got", health.GetStatus())
|
||||
}
|
||||
if health.GetReason() != "" {
|
||||
t.Error("expected reason to be '', got", health.GetReason())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetUnhealthy(t *testing.T) {
|
||||
defer health.SetHealthy()
|
||||
health.SetStatusAndReason(health.Up, "")
|
||||
if health.GetStatus() != health.Up {
|
||||
t.Error("expected status to be '', got", health.GetStatus())
|
||||
}
|
||||
if health.GetReason() != "" {
|
||||
t.Error("expected reason to be '', got", health.GetReason())
|
||||
}
|
||||
health.SetUnhealthy("for what")
|
||||
if health.GetStatus() != health.Down {
|
||||
t.Error("expected status to be 'Down', got", health.GetStatus())
|
||||
}
|
||||
if health.GetReason() != "for what" {
|
||||
t.Error("expected reason to be 'for what', got", health.GetReason())
|
||||
}
|
||||
}
|
8
status.go
Normal file
8
status.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package health
|
||||
|
||||
type Status string
|
||||
|
||||
var (
|
||||
Down Status = "DOWN" // For when the application is unhealthy
|
||||
Up Status = "UP" // For when the application is healthy
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue