diff --git a/pkg/healthcheck/handler.go b/pkg/healthcheck/handler.go index d48d2b9e523..8437266b217 100644 --- a/pkg/healthcheck/handler.go +++ b/pkg/healthcheck/handler.go @@ -15,8 +15,10 @@ package healthcheck import ( + "encoding/json" "net/http" "sync/atomic" + "time" "go.uber.org/zap" ) @@ -46,23 +48,46 @@ func (s Status) String() string { } } +type upTimeStats struct { + StartedAt time.Time `json:"startedAt"` + UpTime string `json:"upTime"` +} + +type healthCheckResponse struct { + statusCode int + StatusMsg string `json:"status"` + upTimeStats +} + // HealthCheck provides an HTTP endpoint that returns the health status of the service type HealthCheck struct { - state int32 // atomic, keep at the top to be word-aligned - logger *zap.Logger - mapping map[Status]int - server *http.Server + state int32 // atomic, keep at the top to be word-aligned + status string + upTimeStats upTimeStats + logger *zap.Logger + mapping map[Status]healthCheckResponse + server *http.Server } // New creates a HealthCheck with the specified initial state. func New() *HealthCheck { hc := &HealthCheck{ state: int32(Unavailable), - mapping: map[Status]int{ - Unavailable: http.StatusServiceUnavailable, - Ready: http.StatusNoContent, + upTimeStats: upTimeStats{ + StartedAt: time.Now(), }, logger: zap.NewNop(), + mapping: map[Status]healthCheckResponse{ + Unavailable: { + statusCode: http.StatusServiceUnavailable, + StatusMsg: "Server not available", + }, + Ready: { + statusCode: http.StatusOK, + StatusMsg: "up", + }, + }, + server: nil, } return hc } @@ -75,12 +100,33 @@ func (hc *HealthCheck) SetLogger(logger *zap.Logger) { // Handler creates a new HTTP handler. func (hc *HealthCheck) Handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(hc.mapping[hc.Get()]) - // this is written only for response with an entity, so, it won't be used for a 204 - No content - w.Write([]byte("Server not available")) + + resp := hc.mapping[hc.Get()] + w.WriteHeader(resp.statusCode) + + w.Header().Set("Content-Type", "application/json") + w.Write(hc.createRespBody(resp)) }) } +func (hc *HealthCheck) createRespBody(resp healthCheckResponse) []byte { + + timeStats := hc.upTimeStats + if resp.statusCode == http.StatusOK { + timeStats.UpTime = upTime(timeStats.StartedAt) + } + + hc.upTimeStats = timeStats + resp.upTimeStats = timeStats + + healthCheckStatus, _ := json.Marshal(resp) + return healthCheckStatus +} + +func upTime(startTime time.Time) string { + return time.Since(startTime).String() +} + // Set a new health check status func (hc *HealthCheck) Set(state Status) { atomic.StoreInt32(&hc.state, int32(state)) diff --git a/pkg/healthcheck/internal_test.go b/pkg/healthcheck/internal_test.go index f7c7184ab7e..9d0963c4743 100644 --- a/pkg/healthcheck/internal_test.go +++ b/pkg/healthcheck/internal_test.go @@ -18,6 +18,9 @@ import ( "net/http" "net/http/httptest" "testing" + "encoding/json" + "io/ioutil" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -34,5 +37,30 @@ func TestHttpCall(t *testing.T) { resp, err := http.Get(server.URL + "/") require.NoError(t, err) - assert.Equal(t, http.StatusNoContent, resp.StatusCode) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + hr := getHealthCheckResponse(t, resp, ) + + assert.Equal(t, "up", hr.StatusMsg) + assert.Equal(t, hc.upTimeStats.UpTime, hr.upTimeStats.UpTime) + assert.True(t, hc.upTimeStats.StartedAt.Equal(hr.upTimeStats.StartedAt)) + + time.Sleep(300) + hc.Set(Unavailable) + + resp, err = http.Get(server.URL + "/") + require.NoError(t, err) + assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) + + hrNew := getHealthCheckResponse(t, resp) + assert.Equal(t, hc.upTimeStats.UpTime, hrNew.upTimeStats.UpTime) + +} +func getHealthCheckResponse(t *testing.T, resp *http.Response) healthCheckResponse { + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + var hr healthCheckResponse + err = json.Unmarshal(body, &hr) + require.NoError(t, err) + return hr }