Skip to content
This repository has been archived by the owner on Dec 21, 2023. It is now read-only.

Commit

Permalink
feat: Provide option to specify readiness condition (#464)
Browse files Browse the repository at this point in the history
* feat: Allow to specify readiness condition in health handler

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>

* added unit tests

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>

* add health check path option

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>

* adapted unit test

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>

* added comments to new options

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>

* added comment

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>
  • Loading branch information
bacherfl committed May 12, 2022
1 parent 13539bf commit c5e1b75
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 4 deletions.
55 changes: 51 additions & 4 deletions pkg/api/utils/healthCheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import (
"net/http"
)

const defaultHealthEndpointPath = "/health"

type ReadinessConditionFunc func() bool

type StatusBody struct {
Status string `json:"status"`
}
Expand All @@ -16,7 +20,48 @@ func (s *StatusBody) ToJSON() ([]byte, error) {
return json.Marshal(s)
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
type HealthHandlerOption func(h *healthHandler)

// WithReadinessConditionFunc allows to specify a function that should determine if the endpoint should return an HTTP 200 (OK), or
// a 412 (Precondition failed) response
func WithReadinessConditionFunc(rc ReadinessConditionFunc) HealthHandlerOption {
return func(h *healthHandler) {
h.readinessConditionFunc = rc
}
}

// WithPath allows to specify the path under which the endpoint should be reachable
func WithPath(path string) HealthHandlerOption {
return func(h *healthHandler) {
h.path = path
}
}

type healthHandler struct {
readinessConditionFunc ReadinessConditionFunc
path string
}

func newHealthHandler(opts ...HealthHandlerOption) *healthHandler {
h := &healthHandler{
path: defaultHealthEndpointPath,
}
for _, o := range opts {
o(h)
}
return h
}

func (h *healthHandler) healthCheck(w http.ResponseWriter, r *http.Request) {
ready := true
if h.readinessConditionFunc != nil {
ready = h.readinessConditionFunc()
}

if !ready {
w.WriteHeader(http.StatusPreconditionFailed)
return
}
status := StatusBody{Status: "OK"}

body, err := status.ToJSON()
Expand All @@ -32,9 +77,11 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
}
}

func RunHealthEndpoint(port string) {

http.HandleFunc("/health", healthHandler)
// RunHealthEndpoint starts an http server on the specified port and provides a simple HTTP Get endpoint that can be used for health checks
// per default, the endpoint will be reachable under the path '/health'
func RunHealthEndpoint(port string, opts ...HealthHandlerOption) {
h := newHealthHandler(opts...)
http.HandleFunc(h.path, h.healthCheck)
err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
if err != nil {
log.Println(err)
Expand Down
69 changes: 69 additions & 0 deletions pkg/api/utils/healthCheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package api

import (
"github.com/stretchr/testify/require"
"net/http"
"testing"
"time"
)

func TestRunHealthEndpoint(t *testing.T) {
go RunHealthEndpoint("8080")

require.Eventually(t, func() bool {
get, err := http.Get("http://localhost:8080/health")
if err != nil {
return false
}
if get.StatusCode != http.StatusOK {
return false
}
return true
}, 2*time.Second, 50*time.Millisecond)
}

func TestRunHealthEndpoint_WithReadinessCondition(t *testing.T) {
ready := false
go RunHealthEndpoint("8080", WithPath("/ready"), WithReadinessConditionFunc(func() bool {
return ready
}))

require.Eventually(t, func() bool {
get, err := http.Get("http://localhost:8080/ready")
if err != nil {
return false
}
if get.StatusCode != http.StatusPreconditionFailed {
return false
}
return true
}, 2*time.Second, 50*time.Millisecond)

ready = true

require.Eventually(t, func() bool {
get, err := http.Get("http://localhost:8080/ready")
if err != nil {
return false
}
if get.StatusCode != http.StatusOK {
return false
}
return true
}, 2*time.Second, 50*time.Millisecond)
}

func TestRunHealthEndpointCustomPath(t *testing.T) {
go RunHealthEndpoint("8080", WithPath("/readiness"))

require.Eventually(t, func() bool {
get, err := http.Get("http://localhost:8080/readiness")
if err != nil {
return false
}
if get.StatusCode != http.StatusOK {
return false
}
return true
}, 2*time.Second, 50*time.Millisecond)
}

0 comments on commit c5e1b75

Please sign in to comment.