Skip to content

Commit

Permalink
Merge pull request #1 from jaegertracing/master
Browse files Browse the repository at this point in the history
Return info msg for `/health` endpoint (jaegertracing#1465)
  • Loading branch information
code98 committed Apr 14, 2019
2 parents 48555a8 + 54416c6 commit bcf9222
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 18 deletions.
6 changes: 5 additions & 1 deletion crossdock/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,14 @@ func (h *clientHandler) isInitialized() bool {
return atomic.LoadUint64(&h.initialized) != 0
}

func is2xxStatusCode(statusCode int) bool {
return statusCode >= 200 && statusCode <= 299
}

func httpHealthCheck(logger *zap.Logger, service, healthURL string) {
for i := 0; i < 240; i++ {
res, err := http.Get(healthURL)
if err == nil && res.StatusCode == 204 {
if err == nil && is2xxStatusCode(res.StatusCode) {
logger.Info("Health check successful", zap.String("service", service))
return
}
Expand Down
78 changes: 62 additions & 16 deletions pkg/healthcheck/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
package healthcheck

import (
"encoding/json"
"fmt"
"net/http"
"sync/atomic"
"time"

"go.uber.org/zap"
)
Expand Down Expand Up @@ -46,24 +49,43 @@ func (s Status) String() string {
}
}

type healthCheckResponse struct {
statusCode int
StatusMsg string `json:"status"`
UpSince time.Time `json:"upSince"`
Uptime string `json:"uptime"`
}

type state struct {
status Status
upSince time.Time
}

// 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 atomic.Value // stores state struct
logger *zap.Logger
responses 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,
},
logger: zap.NewNop(),
responses: map[Status]healthCheckResponse{
Unavailable: {
statusCode: http.StatusServiceUnavailable,
StatusMsg: "Server not available",
},
Ready: {
statusCode: http.StatusOK,
StatusMsg: "Server available",
},
},
server: nil,
}
hc.state.Store(state{status: Unavailable})
return hc
}

Expand All @@ -75,21 +97,45 @@ 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"))
state := hc.getState()
template := hc.responses[state.status]
w.WriteHeader(template.statusCode)

w.Header().Set("Content-Type", "application/json")
w.Write(hc.createRespBody(state, template))
})
}

func (hc *HealthCheck) createRespBody(state state, template healthCheckResponse) []byte {
resp := template // clone
if state.status == Ready {
resp.UpSince = state.upSince
resp.Uptime = fmt.Sprintf("%v", time.Since(state.upSince))
}
healthCheckStatus, _ := json.Marshal(resp)
return healthCheckStatus
}

// Set a new health check status
func (hc *HealthCheck) Set(state Status) {
atomic.StoreInt32(&hc.state, int32(state))
hc.logger.Info("Health Check state change", zap.Stringer("status", hc.Get()))
func (hc *HealthCheck) Set(status Status) {
oldState := hc.getState()
newState := state{status: status}
if status == Ready {
if oldState.status != Ready {
newState.upSince = time.Now()
}
}
hc.state.Store(newState)
hc.logger.Info("Health Check state change", zap.Stringer("status", status))
}

// Get the current status of this health check
func (hc *HealthCheck) Get() Status {
return Status(atomic.LoadInt32(&hc.state))
return hc.getState().status
}

func (hc *HealthCheck) getState() state {
return hc.state.Load().(state)
}

// Ready is a shortcut for Set(Ready) (kept for backwards compatibility)
Expand Down
35 changes: 34 additions & 1 deletion pkg/healthcheck/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
package healthcheck

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -34,5 +37,35 @@ 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 := parseHealthCheckResponse(t, resp)
assert.Equal(t, "Server available", hr.StatusMsg)
// de-serialized timestamp loses monotonic clock value, so assert.Equals() doesn't work.
// https://github.com/stretchr/testify/issues/502
if want, have := hc.getState().upSince, hr.UpSince; !assert.True(t, want.Equal(have)) {
t.Logf("want='%v', have='%v'", want, have)
}
assert.NotZero(t, hr.Uptime)
t.Logf("uptime=%v", hr.Uptime)

time.Sleep(time.Millisecond)
hc.Set(Unavailable)

resp, err = http.Get(server.URL + "/")
require.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)

hrNew := parseHealthCheckResponse(t, resp)
assert.Zero(t, hrNew.Uptime)
assert.Zero(t, hrNew.UpSince)
}

func parseHealthCheckResponse(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
}

0 comments on commit bcf9222

Please sign in to comment.