Add health check endpoint (#18465)
* chore: add health check endpoint docs: update document about health check fix: fix up Sqlite3 ping. current ping will success even if the db file is missing fix: do not expose privacy information in output field * refactor: remove HealthChecker struct * Added `/api/healthz` to install routes. This was needed for using /api/healthz endpoint in Docker healthchecks, otherwise, Docker would never become healthy if using healthz endpoint and users would not be able to complete the installation of Gitea. * Update modules/cache/cache.go * fine tune * Remove unnecessary test code. Now there are 2 routes for installation (and maybe more in future) Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Marcos de Oliveira <marcossantos@furb.br>
This commit is contained in:
parent
3114cd30b8
commit
e933f31426
7 changed files with 261 additions and 13 deletions
|
@ -25,3 +25,47 @@ helm install gitea gitea-charts/gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
If you would like to customize your install, which includes kubernetes ingress, please refer to the complete [Gitea helm chart configuration details](https://gitea.com/gitea/helm-chart/)
|
If you would like to customize your install, which includes kubernetes ingress, please refer to the complete [Gitea helm chart configuration details](https://gitea.com/gitea/helm-chart/)
|
||||||
|
|
||||||
|
## Health check endpoint
|
||||||
|
|
||||||
|
Gitea comes with a health check endpoint `/api/healthz`, you can configure it in kubernetes like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/healthz
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 200
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 10
|
||||||
|
```
|
||||||
|
|
||||||
|
a successful health check response will respond with http code `200`, here's example:
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "pass",
|
||||||
|
"description": "Gitea: Git with a cup of tea",
|
||||||
|
"checks": {
|
||||||
|
"cache:ping": [
|
||||||
|
{
|
||||||
|
"status": "pass",
|
||||||
|
"time": "2022-02-19T09:16:08Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"database:ping": [
|
||||||
|
{
|
||||||
|
"status": "pass",
|
||||||
|
"time": "2022-02-19T09:16:08Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
for more information, please reference to kubernetes documentation [Define a liveness HTTP request](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request)
|
||||||
|
|
|
@ -25,3 +25,47 @@ helm install gitea gitea-charts/gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
若您想自訂安裝(包括使用 kubernetes ingress),請前往完整的 [Gitea helm chart configuration details](https://gitea.com/gitea/helm-chart/)
|
若您想自訂安裝(包括使用 kubernetes ingress),請前往完整的 [Gitea helm chart configuration details](https://gitea.com/gitea/helm-chart/)
|
||||||
|
|
||||||
|
##運行狀況檢查終端節點
|
||||||
|
|
||||||
|
Gitea 附帶了一個運行狀況檢查端點 `/api/healthz`,你可以像這樣在 kubernetes 中配置它:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/healthz
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 200
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 10
|
||||||
|
```
|
||||||
|
|
||||||
|
成功的運行狀況檢查回應將使用 HTTP 代碼 `200` 進行回應,下面是示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "pass",
|
||||||
|
"description": "Gitea: Git with a cup of tea",
|
||||||
|
"checks": {
|
||||||
|
"cache:ping": [
|
||||||
|
{
|
||||||
|
"status": "pass",
|
||||||
|
"time": "2022-02-19T09:16:08Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"database:ping": [
|
||||||
|
{
|
||||||
|
"status": "pass",
|
||||||
|
"time": "2022-02-19T09:16:08Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
有關更多信息,請參考kubernetes文檔[定義一個存活態 HTTP請求接口](https://kubernetes.io/zh/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)
|
||||||
|
|
37
modules/cache/cache.go
vendored
37
modules/cache/cache.go
vendored
|
@ -5,6 +5,7 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -34,25 +35,37 @@ func NewContext() error {
|
||||||
if conn, err = newCache(setting.CacheService.Cache); err != nil {
|
if conn, err = newCache(setting.CacheService.Cache); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
const testKey = "__gitea_cache_test"
|
if err = Ping(); err != nil {
|
||||||
const testVal = "test-value"
|
|
||||||
if err = conn.Put(testKey, testVal, 10); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
val := conn.Get(testKey)
|
|
||||||
if valStr, ok := val.(string); !ok || valStr != testVal {
|
|
||||||
// If the cache is full, the Get may not read the expected value stored by Put.
|
|
||||||
// Since we have checked that Put can success, so we just show a warning here, do not return an error to panic.
|
|
||||||
log.Warn("cache (adapter:%s, config:%s) doesn't seem to work correctly, set test value '%v' but get '%v'",
|
|
||||||
setting.CacheService.Cache.Adapter, setting.CacheService.Cache.Conn,
|
|
||||||
testVal, val,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ping checks if the cache service works or not, it not, it returns an error
|
||||||
|
func Ping() error {
|
||||||
|
if conn == nil {
|
||||||
|
return errors.New("cache not available")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
const testKey = "__gitea_cache_test"
|
||||||
|
const testVal = "test-value"
|
||||||
|
if err = conn.Put(testKey, testVal, 10); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
val := conn.Get(testKey)
|
||||||
|
if valStr, ok := val.(string); !ok || valStr != testVal {
|
||||||
|
// If the cache is full, the Get may not read the expected value stored by Put.
|
||||||
|
// Since we have checked that Put can success, so we just show a warning here, do not return an error to panic.
|
||||||
|
log.Warn("cache (adapter:%s, config:%s) doesn't seem to work correctly, set test value '%v' but get '%v'",
|
||||||
|
setting.CacheService.Cache.Adapter, setting.CacheService.Cache.Conn,
|
||||||
|
testVal, val,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetCache returns the currently configured cache
|
// GetCache returns the currently configured cache
|
||||||
func GetCache() mc.Cache {
|
func GetCache() mc.Cache {
|
||||||
return conn
|
return conn
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
|
"code.gitea.io/gitea/routers/web/healthcheck"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
|
||||||
"gitea.com/go-chi/session"
|
"gitea.com/go-chi/session"
|
||||||
|
@ -106,6 +107,7 @@ func Routes() *web.Route {
|
||||||
r.Use(Init)
|
r.Use(Init)
|
||||||
r.Get("/", Install)
|
r.Get("/", Install)
|
||||||
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
|
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
|
||||||
|
r.Get("/api/healthz", healthcheck.Check)
|
||||||
|
|
||||||
r.NotFound(web.Wrap(installNotFound))
|
r.NotFound(web.Wrap(installNotFound))
|
||||||
return r
|
return r
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
func TestRoutes(t *testing.T) {
|
func TestRoutes(t *testing.T) {
|
||||||
routes := Routes()
|
routes := Routes()
|
||||||
assert.NotNil(t, routes)
|
assert.NotNil(t, routes)
|
||||||
assert.Len(t, routes.R.Routes(), 1)
|
|
||||||
assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern)
|
assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern)
|
||||||
assert.Nil(t, routes.R.Routes()[0].SubRoutes)
|
assert.Nil(t, routes.R.Routes()[0].SubRoutes)
|
||||||
assert.Len(t, routes.R.Routes()[0].Handlers, 2)
|
assert.Len(t, routes.R.Routes()[0].Handlers, 2)
|
||||||
|
|
143
routers/web/healthcheck/check.go
Normal file
143
routers/web/healthcheck/check.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package healthcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/cache"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
type status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// pass healthy (acceptable aliases: "ok" to support Node's Terminus and "up" for Java's SpringBoot)
|
||||||
|
// fail unhealthy (acceptable aliases: "error" to support Node's Terminus and "down" for Java's SpringBoot), and
|
||||||
|
// warn healthy, with some concerns.
|
||||||
|
//
|
||||||
|
// ref https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check#section-3.1
|
||||||
|
// status: (required) indicates whether the service status is acceptable
|
||||||
|
// or not. API publishers SHOULD use following values for the field:
|
||||||
|
// The value of the status field is case-insensitive and is tightly
|
||||||
|
// related with the HTTP response code returned by the health endpoint.
|
||||||
|
// For "pass" status, HTTP response code in the 2xx-3xx range MUST be
|
||||||
|
// used. For "fail" status, HTTP response code in the 4xx-5xx range
|
||||||
|
// MUST be used. In case of the "warn" status, endpoints MUST return
|
||||||
|
// HTTP status in the 2xx-3xx range, and additional information SHOULD
|
||||||
|
// be provided, utilizing optional fields of the response.
|
||||||
|
pass status = "pass"
|
||||||
|
fail status = "fail"
|
||||||
|
warn status = "warn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s status) ToHTTPStatus() int {
|
||||||
|
if s == pass || s == warn {
|
||||||
|
return http.StatusOK
|
||||||
|
}
|
||||||
|
return http.StatusFailedDependency
|
||||||
|
}
|
||||||
|
|
||||||
|
type checks map[string][]componentStatus
|
||||||
|
|
||||||
|
// response is the data returned by the health endpoint, which will be marshaled to JSON format
|
||||||
|
type response struct {
|
||||||
|
Status status `json:"status"`
|
||||||
|
Description string `json:"description"` // a human-friendly description of the service
|
||||||
|
Checks checks `json:"checks,omitempty"` // The Checks Object, should be omitted on installation route
|
||||||
|
}
|
||||||
|
|
||||||
|
// componentStatus presents one status of a single check object
|
||||||
|
// an object that provides detailed health statuses of additional downstream systems and endpoints
|
||||||
|
// which can affect the overall health of the main API.
|
||||||
|
type componentStatus struct {
|
||||||
|
Status status `json:"status"`
|
||||||
|
Time string `json:"time"` // the date-time, in ISO8601 format
|
||||||
|
Output string `json:"output,omitempty"` // this field SHOULD be omitted for "pass" state.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is the health check API handler
|
||||||
|
func Check(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rsp := response{
|
||||||
|
Status: pass,
|
||||||
|
Description: setting.AppName,
|
||||||
|
Checks: make(checks),
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses := make([]status, 0)
|
||||||
|
if setting.InstallLock {
|
||||||
|
statuses = append(statuses, checkDatabase(rsp.Checks))
|
||||||
|
statuses = append(statuses, checkCache(rsp.Checks))
|
||||||
|
}
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s != pass {
|
||||||
|
rsp.Status = fail
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.MarshalIndent(rsp, "", " ")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(rsp.Status.ToHTTPStatus())
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// database checks gitea database status
|
||||||
|
func checkDatabase(checks checks) status {
|
||||||
|
st := componentStatus{}
|
||||||
|
if err := db.GetEngine(db.DefaultContext).Ping(); err != nil {
|
||||||
|
st.Status = fail
|
||||||
|
st.Time = getCheckTime()
|
||||||
|
log.Error("database ping failed with error: %v", err)
|
||||||
|
} else {
|
||||||
|
st.Status = pass
|
||||||
|
st.Time = getCheckTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.Database.UseSQLite3 && st.Status == pass {
|
||||||
|
if !setting.EnableSQLite3 {
|
||||||
|
st.Status = fail
|
||||||
|
st.Time = getCheckTime()
|
||||||
|
log.Error("SQLite3 health check failed with error: %v", "this Gitea binary is built without SQLite3 enabled")
|
||||||
|
} else {
|
||||||
|
if _, err := os.Stat(setting.Database.Path); err != nil {
|
||||||
|
st.Status = fail
|
||||||
|
st.Time = getCheckTime()
|
||||||
|
log.Error("SQLite3 file exists check failed with error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checks["database:ping"] = []componentStatus{st}
|
||||||
|
return st.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache checks gitea cache status
|
||||||
|
func checkCache(checks checks) status {
|
||||||
|
if !setting.CacheService.Enabled {
|
||||||
|
return pass
|
||||||
|
}
|
||||||
|
|
||||||
|
st := componentStatus{}
|
||||||
|
if err := cache.Ping(); err != nil {
|
||||||
|
st.Status = fail
|
||||||
|
st.Time = getCheckTime()
|
||||||
|
log.Error("cache ping failed with error: %v", err)
|
||||||
|
} else {
|
||||||
|
st.Status = pass
|
||||||
|
st.Time = getCheckTime()
|
||||||
|
}
|
||||||
|
checks["cache:ping"] = []componentStatus{st}
|
||||||
|
return st.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCheckTime() string {
|
||||||
|
return time.Now().UTC().Format(time.RFC3339)
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"code.gitea.io/gitea/routers/web/events"
|
"code.gitea.io/gitea/routers/web/events"
|
||||||
"code.gitea.io/gitea/routers/web/explore"
|
"code.gitea.io/gitea/routers/web/explore"
|
||||||
"code.gitea.io/gitea/routers/web/feed"
|
"code.gitea.io/gitea/routers/web/feed"
|
||||||
|
"code.gitea.io/gitea/routers/web/healthcheck"
|
||||||
"code.gitea.io/gitea/routers/web/misc"
|
"code.gitea.io/gitea/routers/web/misc"
|
||||||
"code.gitea.io/gitea/routers/web/org"
|
"code.gitea.io/gitea/routers/web/org"
|
||||||
"code.gitea.io/gitea/routers/web/repo"
|
"code.gitea.io/gitea/routers/web/repo"
|
||||||
|
@ -191,6 +192,8 @@ func Routes() *web.Route {
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
routes.Get("/api/healthz", healthcheck.Check)
|
||||||
|
|
||||||
// Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary
|
// Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary
|
||||||
common = append(common, context.Contexter())
|
common = append(common, context.Contexter())
|
||||||
|
|
||||||
|
|
Reference in a new issue