Files
forge/routers/web/healthcheck/check.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

143 lines
4.8 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package healthcheck
import (
"context"
"net/http"
"os"
"time"
"github.com/hanzoai/git/models/db"
"github.com/hanzoai/git/modules/cache"
"github.com/hanzoai/git/modules/json"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/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
//
// HINT: HEALTH-CHECK-ENDPOINT: there is no clear definition about what "health" means.
// In most cases, end users don't need to check such endpoint, because even if database is down,
// the forge will reover after database is up again. Sysop should monitor database and cache status directly.
//
// And keep in mind: this health check should NEVER be used as a "restart" trigger, for example: Docker's "HEALTHCHECK".
// * If the forge is upgrading and migrating database, there will be a long time before this endpoint starts to return "pass" status.
// In this case, if the checker restarts the forge just because it doesn't get "pass" status in short time,
// the instance will just be restarted again and again before the migration finishes and the situation just goes worse.
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(r.Context(), 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 the forge database status
func checkDatabase(ctx context.Context, checks checks) status {
st := componentStatus{}
if err := db.GetEngine(ctx).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.Type.IsSQLite3() && st.Status == pass {
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 the forge cache status
func checkCache(checks checks) status {
st := componentStatus{}
if err := cache.GetCache().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)
}