Skip to content

Commit 602ca4e

Browse files
authored
feat: add healthz endpoint (#803)
This adds the `/healthz` endpoint, which checks the health of the application. For now, this mainly verifies that the database can be accessed. Use this endpoint to automatically restart the backend with e.g. a docker compose health check or Kubernetes probes. It returns HTTP 204 for success and HTTP 500 with an error message when there is an error.
1 parent 9421a2b commit 602ca4e

File tree

7 files changed

+172
-0
lines changed

7 files changed

+172
-0
lines changed

api/docs.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,41 @@ const docTemplate = `{
4444
}
4545
}
4646
},
47+
"/healthz": {
48+
"get": {
49+
"description": "Returns the application health and, if not healthy, an error",
50+
"produces": [
51+
"application/json"
52+
],
53+
"tags": [
54+
"General"
55+
],
56+
"summary": "Get health",
57+
"responses": {
58+
"204": {
59+
"description": "No Content"
60+
},
61+
"500": {
62+
"description": "Internal Server Error",
63+
"schema": {
64+
"$ref": "#/definitions/httperrors.HTTPError"
65+
}
66+
}
67+
}
68+
},
69+
"options": {
70+
"description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs",
71+
"tags": [
72+
"General"
73+
],
74+
"summary": "Allowed HTTP verbs",
75+
"responses": {
76+
"204": {
77+
"description": "No Content"
78+
}
79+
}
80+
}
81+
},
4782
"/v1": {
4883
"get": {
4984
"description": "Returns general information about the v1 API",

api/swagger.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,41 @@
3333
}
3434
}
3535
},
36+
"/healthz": {
37+
"get": {
38+
"description": "Returns the application health and, if not healthy, an error",
39+
"produces": [
40+
"application/json"
41+
],
42+
"tags": [
43+
"General"
44+
],
45+
"summary": "Get health",
46+
"responses": {
47+
"204": {
48+
"description": "No Content"
49+
},
50+
"500": {
51+
"description": "Internal Server Error",
52+
"schema": {
53+
"$ref": "#/definitions/httperrors.HTTPError"
54+
}
55+
}
56+
}
57+
},
58+
"options": {
59+
"description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs",
60+
"tags": [
61+
"General"
62+
],
63+
"summary": "Allowed HTTP verbs",
64+
"responses": {
65+
"204": {
66+
"description": "No Content"
67+
}
68+
}
69+
}
70+
},
3671
"/v1": {
3772
"get": {
3873
"description": "Returns general information about the v1 API",

api/swagger.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,30 @@ paths:
12631263
summary: Allowed HTTP verbs
12641264
tags:
12651265
- General
1266+
/healthz:
1267+
get:
1268+
description: Returns the application health and, if not healthy, an error
1269+
produces:
1270+
- application/json
1271+
responses:
1272+
"204":
1273+
description: No Content
1274+
"500":
1275+
description: Internal Server Error
1276+
schema:
1277+
$ref: '#/definitions/httperrors.HTTPError'
1278+
summary: Get health
1279+
tags:
1280+
- General
1281+
options:
1282+
description: Returns an empty response with the HTTP Header "allow" set to the
1283+
allowed HTTP verbs
1284+
responses:
1285+
"204":
1286+
description: No Content
1287+
summary: Allowed HTTP verbs
1288+
tags:
1289+
- General
12661290
/v1:
12671291
delete:
12681292
description: Permanently deletes all resources

pkg/controllers/healthz.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package controllers
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/envelope-zero/backend/v3/pkg/httperrors"
7+
"github.com/envelope-zero/backend/v3/pkg/httputil"
8+
"github.com/gin-gonic/gin"
9+
)
10+
11+
// RegisterHealthzRoutes registers the routes for the healthz endpoint.
12+
func (co Controller) RegisterHealthzRoutes(r *gin.RouterGroup) {
13+
r.OPTIONS("", co.OptionsHealthz)
14+
r.GET("", co.GetHealthz)
15+
}
16+
17+
// OptionsHealthz returns the allowed HTTP verbs
18+
//
19+
// @Summary Allowed HTTP verbs
20+
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
21+
// @Tags General
22+
// @Success 204
23+
// @Router /healthz [options]
24+
func (co Controller) OptionsHealthz(c *gin.Context) {
25+
httputil.OptionsGet(c)
26+
}
27+
28+
type HealthResponse struct {
29+
Error error `json:"error" example:"The database cannot be accessed"`
30+
}
31+
32+
// GetHealthz returns data about the application health
33+
//
34+
// @Summary Get health
35+
// @Description Returns the application health and, if not healthy, an error
36+
// @Tags General
37+
// @Produce json
38+
// @Success 204
39+
// @Failure 500 {object} httperrors.HTTPError
40+
// @Router /healthz [get]
41+
func (co Controller) GetHealthz(c *gin.Context) {
42+
sqlDB, err := co.DB.DB()
43+
if err != nil {
44+
httperrors.Handler(c, err)
45+
return
46+
}
47+
48+
err = sqlDB.Ping()
49+
if err != nil {
50+
httperrors.Handler(c, err)
51+
return
52+
}
53+
54+
c.Status(http.StatusNoContent)
55+
}

pkg/controllers/healthz_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package controllers_test
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/envelope-zero/backend/v3/test"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func (suite *TestSuiteStandard) TestHealthzSuccess() {
11+
recorder := test.Request(suite.controller, suite.T(), http.MethodGet, "http://example.com/healthz", "")
12+
assertHTTPStatus(suite.T(), &recorder, http.StatusNoContent)
13+
}
14+
15+
func (suite *TestSuiteStandard) TestHealthzFail() {
16+
suite.CloseDB()
17+
18+
recorder := test.Request(suite.controller, suite.T(), http.MethodGet, "http://example.com/healthz", "")
19+
assertHTTPStatus(suite.T(), &recorder, http.StatusInternalServerError)
20+
assert.Contains(suite.T(), test.DecodeError(suite.T(), recorder.Body.Bytes()), "There is a problem with the database connection")
21+
}

pkg/controllers/test_options_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func (suite *TestSuiteStandard) TestOptionsHeaderResources() {
1313
path string
1414
response string
1515
}{
16+
{"http://example.com/healthz", "OPTIONS, GET"},
1617
{"http://example.com/v1/budgets", "OPTIONS, GET, POST"},
1718
{"http://example.com/v1/accounts", "OPTIONS, GET, POST"},
1819
{"http://example.com/v1/categories", "OPTIONS, GET, POST"},

pkg/router/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func AttachRoutes(co controllers.Controller, group *gin.RouterGroup) {
107107
}
108108

109109
group.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
110+
co.RegisterHealthzRoutes(group.Group("/healthz"))
110111

111112
// API v1 setup
112113
v1 := group.Group("/v1")

0 commit comments

Comments
 (0)