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

Commit c5e1b75

Browse files
authored
feat: Provide option to specify readiness condition (#464)
* 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>
1 parent 13539bf commit c5e1b75

File tree

2 files changed

+120
-4
lines changed

2 files changed

+120
-4
lines changed

pkg/api/utils/healthCheck.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import (
77
"net/http"
88
)
99

10+
const defaultHealthEndpointPath = "/health"
11+
12+
type ReadinessConditionFunc func() bool
13+
1014
type StatusBody struct {
1115
Status string `json:"status"`
1216
}
@@ -16,7 +20,48 @@ func (s *StatusBody) ToJSON() ([]byte, error) {
1620
return json.Marshal(s)
1721
}
1822

19-
func healthHandler(w http.ResponseWriter, r *http.Request) {
23+
type HealthHandlerOption func(h *healthHandler)
24+
25+
// WithReadinessConditionFunc allows to specify a function that should determine if the endpoint should return an HTTP 200 (OK), or
26+
// a 412 (Precondition failed) response
27+
func WithReadinessConditionFunc(rc ReadinessConditionFunc) HealthHandlerOption {
28+
return func(h *healthHandler) {
29+
h.readinessConditionFunc = rc
30+
}
31+
}
32+
33+
// WithPath allows to specify the path under which the endpoint should be reachable
34+
func WithPath(path string) HealthHandlerOption {
35+
return func(h *healthHandler) {
36+
h.path = path
37+
}
38+
}
39+
40+
type healthHandler struct {
41+
readinessConditionFunc ReadinessConditionFunc
42+
path string
43+
}
44+
45+
func newHealthHandler(opts ...HealthHandlerOption) *healthHandler {
46+
h := &healthHandler{
47+
path: defaultHealthEndpointPath,
48+
}
49+
for _, o := range opts {
50+
o(h)
51+
}
52+
return h
53+
}
54+
55+
func (h *healthHandler) healthCheck(w http.ResponseWriter, r *http.Request) {
56+
ready := true
57+
if h.readinessConditionFunc != nil {
58+
ready = h.readinessConditionFunc()
59+
}
60+
61+
if !ready {
62+
w.WriteHeader(http.StatusPreconditionFailed)
63+
return
64+
}
2065
status := StatusBody{Status: "OK"}
2166

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

35-
func RunHealthEndpoint(port string) {
36-
37-
http.HandleFunc("/health", healthHandler)
80+
// RunHealthEndpoint starts an http server on the specified port and provides a simple HTTP Get endpoint that can be used for health checks
81+
// per default, the endpoint will be reachable under the path '/health'
82+
func RunHealthEndpoint(port string, opts ...HealthHandlerOption) {
83+
h := newHealthHandler(opts...)
84+
http.HandleFunc(h.path, h.healthCheck)
3885
err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
3986
if err != nil {
4087
log.Println(err)

pkg/api/utils/healthCheck_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package api
2+
3+
import (
4+
"github.com/stretchr/testify/require"
5+
"net/http"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestRunHealthEndpoint(t *testing.T) {
11+
go RunHealthEndpoint("8080")
12+
13+
require.Eventually(t, func() bool {
14+
get, err := http.Get("http://localhost:8080/health")
15+
if err != nil {
16+
return false
17+
}
18+
if get.StatusCode != http.StatusOK {
19+
return false
20+
}
21+
return true
22+
}, 2*time.Second, 50*time.Millisecond)
23+
}
24+
25+
func TestRunHealthEndpoint_WithReadinessCondition(t *testing.T) {
26+
ready := false
27+
go RunHealthEndpoint("8080", WithPath("/ready"), WithReadinessConditionFunc(func() bool {
28+
return ready
29+
}))
30+
31+
require.Eventually(t, func() bool {
32+
get, err := http.Get("http://localhost:8080/ready")
33+
if err != nil {
34+
return false
35+
}
36+
if get.StatusCode != http.StatusPreconditionFailed {
37+
return false
38+
}
39+
return true
40+
}, 2*time.Second, 50*time.Millisecond)
41+
42+
ready = true
43+
44+
require.Eventually(t, func() bool {
45+
get, err := http.Get("http://localhost:8080/ready")
46+
if err != nil {
47+
return false
48+
}
49+
if get.StatusCode != http.StatusOK {
50+
return false
51+
}
52+
return true
53+
}, 2*time.Second, 50*time.Millisecond)
54+
}
55+
56+
func TestRunHealthEndpointCustomPath(t *testing.T) {
57+
go RunHealthEndpoint("8080", WithPath("/readiness"))
58+
59+
require.Eventually(t, func() bool {
60+
get, err := http.Get("http://localhost:8080/readiness")
61+
if err != nil {
62+
return false
63+
}
64+
if get.StatusCode != http.StatusOK {
65+
return false
66+
}
67+
return true
68+
}, 2*time.Second, 50*time.Millisecond)
69+
}

0 commit comments

Comments
 (0)