summaryrefslogtreecommitdiffstats
path: root/routers/web/healthcheck/check.go
blob: 83dfe625376bba70df89572886f037b6aded6491 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package healthcheck

import (
	"context"
	"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(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.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(rsp.Status.ToHTTPStatus())
	_, _ = w.Write(data)
}

// database checks gitea 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 !setting.EnableSQLite3 {
			st.Status = Fail
			st.Time = getCheckTime()
			log.Error("SQLite3 health check failed with error: %v", "this Forgejo 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 {
	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)
}