code.gitea.io/gitea@v1.21.7/routers/web/healthcheck/check.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package healthcheck
     5  
     6  import (
     7  	"context"
     8  	"net/http"
     9  	"os"
    10  	"time"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	"code.gitea.io/gitea/modules/cache"
    14  	"code.gitea.io/gitea/modules/json"
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/setting"
    17  )
    18  
    19  type status string
    20  
    21  const (
    22  	// pass healthy (acceptable aliases: "ok" to support Node's Terminus and "up" for Java's SpringBoot)
    23  	// fail unhealthy (acceptable aliases: "error" to support Node's Terminus and "down" for Java's SpringBoot), and
    24  	// warn healthy, with some concerns.
    25  	//
    26  	// ref https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check#section-3.1
    27  	// status: (required) indicates whether the service status is acceptable
    28  	// or not.  API publishers SHOULD use following values for the field:
    29  	// The value of the status field is case-insensitive and is tightly
    30  	// related with the HTTP response code returned by the health endpoint.
    31  	// For "pass" status, HTTP response code in the 2xx-3xx range MUST be
    32  	// used.  For "fail" status, HTTP response code in the 4xx-5xx range
    33  	// MUST be used.  In case of the "warn" status, endpoints MUST return
    34  	// HTTP status in the 2xx-3xx range, and additional information SHOULD
    35  	// be provided, utilizing optional fields of the response.
    36  	pass status = "pass"
    37  	fail status = "fail"
    38  	warn status = "warn"
    39  )
    40  
    41  func (s status) ToHTTPStatus() int {
    42  	if s == pass || s == warn {
    43  		return http.StatusOK
    44  	}
    45  	return http.StatusFailedDependency
    46  }
    47  
    48  type checks map[string][]componentStatus
    49  
    50  // response is the data returned by the health endpoint, which will be marshaled to JSON format
    51  type response struct {
    52  	Status      status `json:"status"`
    53  	Description string `json:"description"`      // a human-friendly description of the service
    54  	Checks      checks `json:"checks,omitempty"` // The Checks Object, should be omitted on installation route
    55  }
    56  
    57  // componentStatus presents one status of a single check object
    58  // an object that provides detailed health statuses of additional downstream systems and endpoints
    59  // which can affect the overall health of the main API.
    60  type componentStatus struct {
    61  	Status status `json:"status"`
    62  	Time   string `json:"time"`             // the date-time, in ISO8601 format
    63  	Output string `json:"output,omitempty"` // this field SHOULD be omitted for "pass" state.
    64  }
    65  
    66  // Check is the health check API handler
    67  func Check(w http.ResponseWriter, r *http.Request) {
    68  	rsp := response{
    69  		Status:      pass,
    70  		Description: setting.AppName,
    71  		Checks:      make(checks),
    72  	}
    73  
    74  	statuses := make([]status, 0)
    75  	if setting.InstallLock {
    76  		statuses = append(statuses, checkDatabase(r.Context(), rsp.Checks))
    77  		statuses = append(statuses, checkCache(rsp.Checks))
    78  	}
    79  	for _, s := range statuses {
    80  		if s != pass {
    81  			rsp.Status = fail
    82  			break
    83  		}
    84  	}
    85  
    86  	data, _ := json.MarshalIndent(rsp, "", "  ")
    87  	w.Header().Set("Content-Type", "application/json")
    88  	w.WriteHeader(rsp.Status.ToHTTPStatus())
    89  	_, _ = w.Write(data)
    90  }
    91  
    92  // database checks gitea database status
    93  func checkDatabase(ctx context.Context, checks checks) status {
    94  	st := componentStatus{}
    95  	if err := db.GetEngine(ctx).Ping(); err != nil {
    96  		st.Status = fail
    97  		st.Time = getCheckTime()
    98  		log.Error("database ping failed with error: %v", err)
    99  	} else {
   100  		st.Status = pass
   101  		st.Time = getCheckTime()
   102  	}
   103  
   104  	if setting.Database.Type.IsSQLite3() && st.Status == pass {
   105  		if !setting.EnableSQLite3 {
   106  			st.Status = fail
   107  			st.Time = getCheckTime()
   108  			log.Error("SQLite3 health check failed with error: %v", "this Gitea binary is built without SQLite3 enabled")
   109  		} else {
   110  			if _, err := os.Stat(setting.Database.Path); err != nil {
   111  				st.Status = fail
   112  				st.Time = getCheckTime()
   113  				log.Error("SQLite3 file exists check failed with error: %v", err)
   114  			}
   115  		}
   116  	}
   117  
   118  	checks["database:ping"] = []componentStatus{st}
   119  	return st.Status
   120  }
   121  
   122  // cache checks gitea cache status
   123  func checkCache(checks checks) status {
   124  	if !setting.CacheService.Enabled {
   125  		return pass
   126  	}
   127  
   128  	st := componentStatus{}
   129  	if err := cache.GetCache().Ping(); err != nil {
   130  		st.Status = fail
   131  		st.Time = getCheckTime()
   132  		log.Error("cache ping failed with error: %v", err)
   133  	} else {
   134  		st.Status = pass
   135  		st.Time = getCheckTime()
   136  	}
   137  	checks["cache:ping"] = []componentStatus{st}
   138  	return st.Status
   139  }
   140  
   141  func getCheckTime() string {
   142  	return time.Now().UTC().Format(time.RFC3339)
   143  }