...

Source file src/edge-infra.dev/pkg/lib/runtime/healthz/healthz.go

Documentation: edge-infra.dev/pkg/lib/runtime/healthz

     1  package healthz
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"path"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/go-logr/logr"
    11  	"k8s.io/apimachinery/pkg/util/sets"
    12  )
    13  
    14  // Handler is an http.Handler that aggregates the results of the given
    15  // checkers to the root path, and supports calling individual checkers on
    16  // subpaths of the name of the checker.
    17  //
    18  // Adding checks on the fly is *not* threadsafe -- use a wrapper.
    19  type Handler struct {
    20  	Checks map[string]Checker
    21  	Log    logr.Logger
    22  }
    23  
    24  // checkStatus holds the output of a particular check.
    25  type checkStatus struct {
    26  	name     string
    27  	healthy  bool
    28  	excluded bool
    29  }
    30  
    31  func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) {
    32  	failed := false
    33  	excluded := getExcludedChecks(req)
    34  
    35  	parts := make([]checkStatus, 0, len(h.Checks))
    36  
    37  	// calculate the results...
    38  	for checkName, check := range h.Checks {
    39  		// no-op the check if we've specified we want to exclude the check
    40  		if excluded.Has(checkName) {
    41  			excluded.Delete(checkName)
    42  			parts = append(parts, checkStatus{name: checkName, healthy: true, excluded: true})
    43  			continue
    44  		}
    45  		if err := check(req); err != nil {
    46  			h.Log.Error(err, "healthz check failed", "checker", checkName)
    47  			parts = append(parts, checkStatus{name: checkName, healthy: false})
    48  			failed = true
    49  		} else {
    50  			parts = append(parts, checkStatus{name: checkName, healthy: true})
    51  		}
    52  	}
    53  
    54  	// ...default a check if none is present...
    55  	if len(h.Checks) == 0 {
    56  		parts = append(parts, checkStatus{name: "ping", healthy: true})
    57  	}
    58  
    59  	for _, c := range excluded.UnsortedList() {
    60  		h.Log.Error(nil, "cannot exclude health check, no matches for it", "checker", c)
    61  	}
    62  
    63  	// ...sort to be consistent...
    64  	sort.Slice(parts, func(i, j int) bool { return parts[i].name < parts[j].name })
    65  
    66  	// ...and write out the result
    67  	// TODO(help_wanted): this should also accept a request for JSON content (via a accept header)
    68  	_, forceVerbose := req.URL.Query()["verbose"]
    69  	h.writeStatusesAsText(resp, parts, excluded, failed, forceVerbose)
    70  }
    71  
    72  // writeStatusAsText writes out the given check statuses in some semi-arbitrary
    73  // bespoke text format that we copied from Kubernetes.  unknownExcludes lists
    74  // any checks that the user requested to have excluded, but weren't actually
    75  // known checks.  writeStatusAsText is always verbose on failure, and can be
    76  // forced to be verbose on success using the given argument.
    77  func (h *Handler) writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownExcludes sets.Set[string], failed, forceVerbose bool) {
    78  	resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
    79  	resp.Header().Set("X-Content-Type-Options", "nosniff")
    80  
    81  	// always write status code first
    82  	if failed {
    83  		resp.WriteHeader(http.StatusInternalServerError)
    84  	} else {
    85  		resp.WriteHeader(http.StatusOK)
    86  	}
    87  
    88  	// shortcut for easy non-verbose success
    89  	if !failed && !forceVerbose {
    90  		fmt.Fprint(resp, "ok")
    91  		return
    92  	}
    93  
    94  	// we're always verbose on failure, so from this point on we're guaranteed to be verbose
    95  
    96  	for _, checkOut := range parts {
    97  		switch {
    98  		case checkOut.excluded:
    99  			fmt.Fprintf(resp, "[+]%s excluded: ok\n", checkOut.name)
   100  		case checkOut.healthy:
   101  			fmt.Fprintf(resp, "[+]%s ok\n", checkOut.name)
   102  		default:
   103  			// don't include the error since this endpoint is public.  If someone wants more detail
   104  			// they should have explicit permission to the detailed checks.
   105  			fmt.Fprintf(resp, "[-]%s failed: reason withheld\n", checkOut.name)
   106  		}
   107  	}
   108  
   109  	if unknownExcludes.Len() > 0 {
   110  		fmt.Fprintf(resp, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(unknownExcludes.UnsortedList()...))
   111  	}
   112  
   113  	if failed {
   114  		// todo - statuses seems to be broken "statuses":[{},{}],
   115  		h.Log.Error(nil, "healthz check failed", "statuses", parts)
   116  		fmt.Fprintf(resp, "healthz check failed\n")
   117  	} else {
   118  		fmt.Fprint(resp, "healthz check passed\n")
   119  	}
   120  }
   121  
   122  func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
   123  	// clean up the request (duplicating the internal logic of http.ServeMux a bit)
   124  	// clean up the path a bit
   125  	reqPath := req.URL.Path
   126  	if reqPath == "" || reqPath[0] != '/' {
   127  		reqPath = "/" + reqPath
   128  	}
   129  	// path.Clean removes the trailing slash except for root for us
   130  	// (which is fine, since we're only serving one layer of sub-paths)
   131  	reqPath = path.Clean(reqPath)
   132  
   133  	// either serve the root endpoint...
   134  	if reqPath == "/" {
   135  		h.serveAggregated(resp, req)
   136  		return
   137  	}
   138  
   139  	// ...the default check (if nothing else is present)...
   140  	if len(h.Checks) == 0 && reqPath[1:] == "ping" {
   141  		CheckHandler{Checker: Ping}.ServeHTTP(resp, req)
   142  		return
   143  	}
   144  
   145  	// ...or an individual checker
   146  	checkName := reqPath[1:] // ignore the leading slash
   147  	checker, known := h.Checks[checkName]
   148  	if !known {
   149  		http.NotFoundHandler().ServeHTTP(resp, req)
   150  		return
   151  	}
   152  
   153  	CheckHandler{Checker: checker}.ServeHTTP(resp, req)
   154  }
   155  
   156  // CheckHandler is an http.Handler that serves a health check endpoint at the root path,
   157  // based on its checker.
   158  type CheckHandler struct {
   159  	Checker
   160  }
   161  
   162  func (h CheckHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
   163  	if err := h.Checker(req); err != nil {
   164  		http.Error(resp, fmt.Sprintf("internal server error: %v", err), http.StatusInternalServerError)
   165  	} else {
   166  		fmt.Fprint(resp, "ok")
   167  	}
   168  }
   169  
   170  // Checker knows how to perform a health check.
   171  type Checker func(req *http.Request) error
   172  
   173  // Ping returns true automatically when checked.
   174  var Ping Checker = func(_ *http.Request) error { return nil }
   175  
   176  // getExcludedChecks extracts the health check names to be excluded from the query param.
   177  func getExcludedChecks(r *http.Request) sets.Set[string] {
   178  	checks, found := r.URL.Query()["exclude"]
   179  	if found {
   180  		return sets.New[string](checks...)
   181  	}
   182  	return sets.New[string]()
   183  }
   184  
   185  // formatQuoted returns a formatted string of the health check names,
   186  // preserving the order passed in.
   187  func formatQuoted(names ...string) string {
   188  	quoted := make([]string, 0, len(names))
   189  	for _, name := range names {
   190  		quoted = append(quoted, fmt.Sprintf("%q", name))
   191  	}
   192  	return strings.Join(quoted, ",")
   193  }
   194  

View as plain text