Skip to content

Commit

Permalink
Support non-JSON encodings by health.Handler (#284)
Browse files Browse the repository at this point in the history
* Implement MarshalXML() by health.Health

* Use goahttp.ResponseEncoder() to health.Handler()

* Fix godoc of health.Handler()

* Add an example to README

---------

Co-authored-by: Raphael Simon <simon.raphael@gmail.com>
  • Loading branch information
tchssk and raphael authored Sep 7, 2023
1 parent 35fa34b commit def24ce
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 23 deletions.
19 changes: 19 additions & 0 deletions health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ Date: Mon, 17 Jan 2022 23:23:20 GMT
}
```

Another Content-Type:

```bash
http http://localhost:8083/livez Accept:application/xml
HTTP/1.1 200 OK
Content-Length: 158
Content-Type: application/xml
Date: Thu, 07 Sep 2023 13:28:29 GMT

<health>
<uptime>20</uptime>
<version>91bb64a8103b494d0eac680f8e929e74882eea5f</version>
<status>
<ClickHouse>OK</ClickHouse>
<poller>OK</poller>
</status>
</health>
```

## Usage

```go
Expand Down
38 changes: 38 additions & 0 deletions health/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package health

import (
"context"
"encoding/xml"
"sort"
"time"

"goa.design/clue/log"
Expand Down Expand Up @@ -32,8 +34,44 @@ type (
checker struct {
deps []Pinger
}

// mp is used to marshal a map to xml.
mp map[string]string
)

func (h Health) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.Encode(struct {
XMLName xml.Name `xml:"health"`
Uptime int64 `xml:"uptime"`
Version string `xml:"version"`
Status mp `xml:"status"`
}{
Uptime: h.Uptime,
Version: h.Version,
Status: h.Status,
})
}

func (m mp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if len(m) == 0 {
return nil
}
if err := e.EncodeToken(start); err != nil {
return err
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if err := e.EncodeElement(m[k], xml.StartElement{Name: xml.Name{Local: k}}); err != nil {
return err
}
}
return e.EncodeToken(start.End())
}

// Version of service, initialized at compiled time.
var Version string

Expand Down
19 changes: 12 additions & 7 deletions health/handler.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package health

import (
"encoding/json"
"context"
"net/http"

goahttp "goa.design/goa/v3/http"
)

// Handler returns a HTTP handler that serves health check requests. The
// response body is the JSON encoded health status returned by chk.Check(). The
// response status is 200 if chk.Check() returns a nil error, 503 otherwise.
// response body is the health status returned by chk.Check(). By default
// it's encoded as JSON, but you can specify a different encoding in the
// HTTP Accept header. The response status is 200 if chk.Check() returns
// a nil error, 503 otherwise.
func Handler(chk Checker) http.HandlerFunc {
encoder := goahttp.ResponseEncoder
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
h, healthy := chk.Check(r.Context())
b, _ := json.Marshal(h)
ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept"))
enc := encoder(ctx, w)
h, healthy := chk.Check(ctx)
if healthy {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
w.Write(b) // nolint: errcheck
enc.Encode(h) // nolint: errcheck
})
}
81 changes: 65 additions & 16 deletions health/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,107 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestHandler(t *testing.T) {
type (
assertFunc func(*testing.T, string)
)
var (
assertContains = func(contains string) assertFunc {
return func(t *testing.T, body string) {
assert.Contains(t, body, contains)
}
}
assertEqual = func(expected string) assertFunc {
return func(t *testing.T, body string) {
assert.Equal(t, expected, body)
}
}
assertNotContains = func(contains string) assertFunc {
return func(t *testing.T, body string) {
assert.NotContains(t, body, contains)
}
}
)
cases := []struct {
name string
deps []Pinger
expectedStatus int
expectedJSON string
name string
deps []Pinger
expectedStatus int
assertFuncsPerAccept map[string][]assertFunc
}{
{
name: "empty",
expectedStatus: http.StatusOK,
expectedJSON: `{"uptime":0,"version":""}`,
assertFuncsPerAccept: map[string][]assertFunc{
"": {assertEqual(`{"uptime":0,"version":""}`)},
"application/xml": {assertEqual("<health><uptime>0</uptime><version></version></health>")},
"application/gob": {assertNotContains("status"), assertNotContains("dependency")},
},
},
{
name: "ok",
deps: singleHealthyDep("dependency"),
expectedStatus: http.StatusOK,
expectedJSON: `{"uptime":0,"version":"","status":{"dependency":"OK"}}`,
assertFuncsPerAccept: map[string][]assertFunc{
"": {assertEqual(`{"uptime":0,"version":"","status":{"dependency":"OK"}}`)},
"application/xml": {assertEqual("<health><uptime>0</uptime><version></version><status><dependency>OK</dependency></status></health>")},
"application/gob": {assertContains("Status"), assertContains("dependency"), assertNotContains("NOT OK")},
},
},
{
name: "not ok",
deps: singleUnhealthyDep("dependency", fmt.Errorf("dependency is not ok")),
expectedStatus: http.StatusServiceUnavailable,
expectedJSON: `{"uptime":0,"version":"","status":{"dependency":"NOT OK"}}`,
assertFuncsPerAccept: map[string][]assertFunc{
"": {assertEqual(`{"uptime":0,"version":"","status":{"dependency":"NOT OK"}}`)},
"application/xml": {assertEqual("<health><uptime>0</uptime><version></version><status><dependency>NOT OK</dependency></status></health>")},
"application/gob": {assertContains("Status"), assertContains("dependency"), assertContains("NOT OK")},
},
},
{
name: "multiple dependencies",
deps: multipleHealthyDeps("dependency1", "dependency2"),
expectedStatus: http.StatusOK,
expectedJSON: `{"uptime":0,"version":"","status":{"dependency1":"OK","dependency2":"OK"}}`,
assertFuncsPerAccept: map[string][]assertFunc{
"": {assertEqual(`{"uptime":0,"version":"","status":{"dependency1":"OK","dependency2":"OK"}}`)},
"application/xml": {assertEqual("<health><uptime>0</uptime><version></version><status><dependency1>OK</dependency1><dependency2>OK</dependency2></status></health>")},
"application/gob": {assertContains("dependency1"), assertContains("dependency2"), assertNotContains("NOT OK")},
},
},
{
name: "multiple dependencies not ok",
deps: multipleUnhealthyDeps(fmt.Errorf("dependency2 is not ok"), "dependency1", "dependency2"),
expectedStatus: http.StatusServiceUnavailable,
expectedJSON: `{"uptime":0,"version":"","status":{"dependency1":"OK","dependency2":"NOT OK"}}`,
assertFuncsPerAccept: map[string][]assertFunc{
"": {assertEqual(`{"uptime":0,"version":"","status":{"dependency1":"OK","dependency2":"NOT OK"}}`)},
"application/xml": {assertEqual("<health><uptime>0</uptime><version></version><status><dependency1>OK</dependency1><dependency2>NOT OK</dependency2></status></health>")},
"application/gob": {assertContains("dependency1"), assertContains("dependency2"), assertContains("NOT OK")},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
checker := NewChecker(c.deps...)
handler := Handler(checker)
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != c.expectedStatus {
t.Errorf("got status: %d, expected %d", w.Code, c.expectedStatus)
}
if w.Body.String() != c.expectedJSON {
t.Errorf("got body: %s, expected %s", w.Body.String(), c.expectedJSON)
for accept, fns := range c.assertFuncsPerAccept {
t.Run("Accept:"+accept, func(t *testing.T) {
req.Header.Set("Accept", accept)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != c.expectedStatus {
t.Errorf("got status: %d, expected %d", w.Code, c.expectedStatus)
}
body := strings.TrimSpace(w.Body.String())
for _, fn := range fns {
fn(t, body)
}
})
}
})
}
Expand Down

0 comments on commit def24ce

Please sign in to comment.