// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package main import ( "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "os" "time" jwtgo "github.com/dgrijalva/jwt-go" log "github.com/sirupsen/logrus" ) const ( pass = "PASS" // Indicate that a test passed fail = "FAIL" // Indicate that a test failed livenessPath = "/minio/health/live" readinessPath = "/minio/health/ready" prometheusPath = "/minio/prometheus/metrics" prometheusPathV2 = "/minio/v2/metrics/cluster" timeout = time.Duration(30 * time.Second) ) type mintJSONFormatter struct { } func (f *mintJSONFormatter) Format(entry *log.Entry) ([]byte, error) { data := make(log.Fields, len(entry.Data)) for k, v := range entry.Data { switch v := v.(type) { case error: // Otherwise errors are ignored by `encoding/json` // https://github.com/sirupsen/logrus/issues/137 data[k] = v.Error() default: data[k] = v } } serialized, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err) } return append(serialized, '\n'), nil } // log successful test runs func successLogger(function string, args map[string]interface{}, startTime time.Time) *log.Entry { // calculate the test case duration duration := time.Since(startTime) // log with the fields as per mint fields := log.Fields{"name": "healthcheck", "function": function, "args": args, "duration": duration.Nanoseconds() / 1000000, "status": pass} return log.WithFields(fields) } // log failed test runs func failureLog(function string, args map[string]interface{}, startTime time.Time, alert string, message string, err error) *log.Entry { // calculate the test case duration duration := time.Since(startTime) var fields log.Fields // log with the fields as per mint if err != nil { fields = log.Fields{"name": "healthcheck", "function": function, "args": args, "duration": duration.Nanoseconds() / 1000000, "status": fail, "alert": alert, "message": message, "error": err} } else { fields = log.Fields{"name": "healthcheck", "function": function, "args": args, "duration": duration.Nanoseconds() / 1000000, "status": fail, "alert": alert, "message": message} } return log.WithFields(fields) } func testLivenessEndpoint(endpoint string) { startTime := time.Now() function := "testLivenessEndpoint" u, err := url.Parse(fmt.Sprintf("%s%s", endpoint, livenessPath)) if err != nil { // Could not parse URL successfully failureLog(function, nil, startTime, "", "URL Parsing for Healthcheck Liveness handler failed", err).Fatal() } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: u.Scheme == "https"}, } client := &http.Client{Transport: tr, Timeout: timeout} resp, err := client.Get(u.String()) if err != nil { // GET request errored failureLog(function, nil, startTime, "", "GET request failed", err).Fatal() } if resp.StatusCode != http.StatusOK { // Status not 200 OK failureLog(function, nil, startTime, "", fmt.Sprintf("GET /minio/health/live returned %s", resp.Status), err).Fatal() } defer resp.Body.Close() defer successLogger(function, nil, startTime).Info() } func testReadinessEndpoint(endpoint string) { startTime := time.Now() function := "testReadinessEndpoint" u, err := url.Parse(fmt.Sprintf("%s%s", endpoint, readinessPath)) if err != nil { // Could not parse URL successfully failureLog(function, nil, startTime, "", "URL Parsing for Healthcheck Readiness handler failed", err).Fatal() } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: u.Scheme == "https"}, } client := &http.Client{Transport: tr, Timeout: timeout} resp, err := client.Get(u.String()) if err != nil { // GET request errored failureLog(function, nil, startTime, "", "GET request to Readiness endpoint failed", err).Fatal() } if resp.StatusCode != http.StatusOK { // Status not 200 OK failureLog(function, nil, startTime, "", "GET /minio/health/ready returned non OK status", err).Fatal() } defer resp.Body.Close() defer successLogger(function, nil, startTime).Info() } const ( defaultPrometheusJWTExpiry = 100 * 365 * 24 * time.Hour ) func testPrometheusEndpoint(endpoint string) { startTime := time.Now() function := "testPrometheusEndpoint" u, err := url.Parse(fmt.Sprintf("%s%s", endpoint, prometheusPath)) if err != nil { // Could not parse URL successfully failureLog(function, nil, startTime, "", "URL Parsing for Healthcheck Prometheus handler failed", err).Fatal() } jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{ ExpiresAt: time.Now().UTC().Add(defaultPrometheusJWTExpiry).Unix(), Subject: os.Getenv("ACCESS_KEY"), Issuer: "prometheus", }) token, err := jwt.SignedString([]byte(os.Getenv("SECRET_KEY"))) if err != nil { failureLog(function, nil, startTime, "", "jwt generation failed", err).Fatal() } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: u.Scheme == "https"}, } client := &http.Client{Transport: tr, Timeout: timeout} req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { failureLog(function, nil, startTime, "", "Initializing GET request to Prometheus endpoint failed", err).Fatal() } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { // GET request errored failureLog(function, nil, startTime, "", "GET request to Prometheus endpoint failed", err).Fatal() } if resp.StatusCode != http.StatusOK { // Status not 200 OK failureLog(function, nil, startTime, "", "GET /minio/prometheus/metrics returned non OK status", err).Fatal() } defer resp.Body.Close() defer successLogger(function, nil, startTime).Info() } func testPrometheusEndpointV2(endpoint string) { startTime := time.Now() function := "testPrometheusEndpoint" u, err := url.Parse(fmt.Sprintf("%s%s", endpoint, prometheusPathV2)) if err != nil { // Could not parse URL successfully failureLog(function, nil, startTime, "", "URL Parsing for Healthcheck Prometheus handler failed", err).Fatal() } jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{ ExpiresAt: time.Now().UTC().Add(defaultPrometheusJWTExpiry).Unix(), Subject: os.Getenv("ACCESS_KEY"), Issuer: "prometheus", }) token, err := jwt.SignedString([]byte(os.Getenv("SECRET_KEY"))) if err != nil { failureLog(function, nil, startTime, "", "jwt generation failed", err).Fatal() } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: u.Scheme == "https"}, } client := &http.Client{Transport: tr, Timeout: timeout} req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { failureLog(function, nil, startTime, "", "Initializing GET request to Prometheus endpoint failed", err).Fatal() } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { // GET request errored failureLog(function, nil, startTime, "", "GET request to Prometheus endpoint failed", err).Fatal() } if resp.StatusCode != http.StatusOK { // Status not 200 OK failureLog(function, nil, startTime, "", "GET /minio/prometheus/metrics returned non OK status", err).Fatal() } defer resp.Body.Close() defer successLogger(function, nil, startTime).Info() } func main() { endpoint := os.Getenv("SERVER_ENDPOINT") secure := os.Getenv("ENABLE_HTTPS") if secure == "1" { endpoint = "https://" + endpoint } else { endpoint = "http://" + endpoint } // Output to stdout instead of the default stderr log.SetOutput(os.Stdout) // create custom formatter mintFormatter := mintJSONFormatter{} // set custom formatter log.SetFormatter(&mintFormatter) // log Info or above -- success cases are Info level, failures are Fatal level log.SetLevel(log.InfoLevel) // execute tests testLivenessEndpoint(endpoint) testReadinessEndpoint(endpoint) testPrometheusEndpoint(endpoint) testPrometheusEndpointV2(endpoint) }