mirror of
https://github.com/minio/minio.git
synced 2025-05-21 09:33:50 -04:00
Send health diagnostics data as part of callhome (#16006)
This commit is contained in:
parent
8a07000e58
commit
5246e3be84
@ -1911,67 +1911,7 @@ func getKubernetesInfo(dctx context.Context) madmin.KubernetesInfo {
|
|||||||
return ki
|
return ki
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
|
func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *url.Values, healthInfoCh chan madmin.HealthInfo, healthInfo madmin.HealthInfo) {
|
||||||
// ----------
|
|
||||||
// Get server health info
|
|
||||||
func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "HealthInfo")
|
|
||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.Form
|
|
||||||
healthInfo := madmin.HealthInfo{
|
|
||||||
Version: madmin.HealthInfoVersion,
|
|
||||||
Minio: madmin.MinioHealthInfo{
|
|
||||||
Info: madmin.MinioInfo{
|
|
||||||
DeploymentID: globalDeploymentID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
healthInfoCh := make(chan madmin.HealthInfo)
|
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
|
|
||||||
setCommonHeaders(w)
|
|
||||||
|
|
||||||
setEventStreamHeaders(w)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
errResp := func(err error) {
|
|
||||||
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
|
||||||
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
|
||||||
encodedErrorResponse := encodeResponse(errorResponse)
|
|
||||||
healthInfo.Error = string(encodedErrorResponse)
|
|
||||||
logger.LogIf(ctx, enc.Encode(healthInfo))
|
|
||||||
}
|
|
||||||
|
|
||||||
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
|
|
||||||
if dstr := r.Form.Get("deadline"); dstr != "" {
|
|
||||||
var err error
|
|
||||||
deadline, err = time.ParseDuration(dstr)
|
|
||||||
if err != nil {
|
|
||||||
errResp(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
|
|
||||||
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
|
|
||||||
if err != nil { // returns a locked lock
|
|
||||||
errResp(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer nsLock.Unlock(lkctx.Cancel)
|
|
||||||
|
|
||||||
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
|
|
||||||
defer healthCancel()
|
|
||||||
|
|
||||||
hostAnonymizer := createHostAnonymizer()
|
hostAnonymizer := createHostAnonymizer()
|
||||||
// anonAddr - Anonymizes hosts in given input string.
|
// anonAddr - Anonymizes hosts in given input string.
|
||||||
anonAddr := func(addr string) string {
|
anonAddr := func(addr string) string {
|
||||||
@ -2196,7 +2136,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWriteMinioConfig := func() {
|
getAndWriteMinioConfig := func() {
|
||||||
if query.Get("minioconfig") == "true" {
|
if query.Get("minioconfig") == "true" {
|
||||||
config, err := readServerConfig(ctx, objectAPI)
|
config, err := readServerConfig(healthCtx, objectAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
healthInfo.Minio.Config = madmin.MinioConfig{
|
healthInfo.Minio.Config = madmin.MinioConfig{
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
@ -2244,7 +2184,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
getAndWriteSysConfig()
|
getAndWriteSysConfig()
|
||||||
|
|
||||||
if query.Get("minioinfo") == "true" {
|
if query.Get("minioinfo") == "true" {
|
||||||
infoMessage := getServerInfo(ctx, r)
|
infoMessage := getServerInfo(healthCtx, nil)
|
||||||
servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers))
|
servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers))
|
||||||
for _, server := range infoMessage.Servers {
|
for _, server := range infoMessage.Servers {
|
||||||
anonEndpoint := anonAddr(server.Endpoint)
|
anonEndpoint := anonAddr(server.Endpoint)
|
||||||
@ -2294,6 +2234,67 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
partialWrite(healthInfo)
|
partialWrite(healthInfo)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
|
||||||
|
// ----------
|
||||||
|
// Get server health info
|
||||||
|
func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "HealthInfo")
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.Form
|
||||||
|
healthInfoCh := make(chan madmin.HealthInfo)
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
|
||||||
|
healthInfo := madmin.HealthInfo{
|
||||||
|
Version: madmin.HealthInfoVersion,
|
||||||
|
Minio: madmin.MinioHealthInfo{
|
||||||
|
Info: madmin.MinioInfo{
|
||||||
|
DeploymentID: globalDeploymentID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
errResp := func(err error) {
|
||||||
|
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
||||||
|
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
||||||
|
encodedErrorResponse := encodeResponse(errorResponse)
|
||||||
|
healthInfo.Error = string(encodedErrorResponse)
|
||||||
|
logger.LogIf(ctx, enc.Encode(healthInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
|
||||||
|
if dstr := query.Get("deadline"); dstr != "" {
|
||||||
|
var err error
|
||||||
|
deadline, err = time.ParseDuration(dstr)
|
||||||
|
if err != nil {
|
||||||
|
errResp(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
|
||||||
|
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
|
||||||
|
if err != nil { // returns a locked lock
|
||||||
|
errResp(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer nsLock.Unlock(lkctx.Cancel)
|
||||||
|
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
|
||||||
|
defer healthCancel()
|
||||||
|
|
||||||
|
go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
|
||||||
|
|
||||||
|
setCommonHeaders(w)
|
||||||
|
setEventStreamHeaders(w)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
106
cmd/callhome.go
106
cmd/callhome.go
@ -18,29 +18,20 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// callhomeSchemaVersion1 is callhome schema version 1
|
|
||||||
callhomeSchemaVersion1 = "1"
|
|
||||||
|
|
||||||
// callhomeSchemaVersion is current callhome schema version.
|
|
||||||
callhomeSchemaVersion = callhomeSchemaVersion1
|
|
||||||
)
|
|
||||||
|
|
||||||
// CallhomeInfo - Contains callhome information
|
|
||||||
type CallhomeInfo struct {
|
|
||||||
SchemaVersion string `json:"schema_version"`
|
|
||||||
AdminInfo madmin.InfoMessage `json:"admin_info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
var callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||||
|
|
||||||
// initCallhome will start the callhome task in the background.
|
// initCallhome will start the callhome task in the background.
|
||||||
@ -118,26 +109,91 @@ func runCallhome(ctx context.Context, objAPI ObjectLayer) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func performCallhome(ctx context.Context) {
|
func performCallhome(ctx context.Context) {
|
||||||
err := sendCallhomeInfo(
|
deadline := 10 * time.Second // Default deadline is 10secs for callhome
|
||||||
CallhomeInfo{
|
objectAPI := newObjectLayerFn()
|
||||||
SchemaVersion: callhomeSchemaVersion,
|
if objectAPI == nil {
|
||||||
AdminInfo: getServerInfo(ctx, nil),
|
logger.LogIf(ctx, errors.New("Callhome: object layer not ready"))
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
healthCtx, healthCancel := context.WithTimeout(ctx, deadline)
|
||||||
|
defer healthCancel()
|
||||||
|
|
||||||
|
healthInfoCh := make(chan madmin.HealthInfo)
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
for _, k := range madmin.HealthDataTypesList {
|
||||||
|
query.Set(string(k), "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
healthInfo := madmin.HealthInfo{
|
||||||
|
Version: madmin.HealthInfoVersion,
|
||||||
|
Minio: madmin.MinioHealthInfo{
|
||||||
|
Info: madmin.MinioInfo{
|
||||||
|
DeploymentID: globalDeploymentID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case hi, hasMore := <-healthInfoCh:
|
||||||
|
if !hasMore {
|
||||||
|
// Received all data. Send to SUBNET and return
|
||||||
|
err := sendHealthInfo(ctx, healthInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
healthInfo = hi
|
||||||
|
case <-healthCtx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
callhomeURL = "https://subnet.min.io/api/callhome"
|
healthURL = "https://subnet.min.io/api/health/upload"
|
||||||
callhomeURLDev = "http://localhost:9000/api/callhome"
|
healthURLDev = "http://localhost:9000/api/health/upload"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sendCallhomeInfo(ch CallhomeInfo) error {
|
func sendHealthInfo(ctx context.Context, healthInfo madmin.HealthInfo) error {
|
||||||
url := callhomeURL
|
url := healthURL
|
||||||
if globalIsCICD {
|
if globalIsCICD {
|
||||||
url = callhomeURLDev
|
url = healthURLDev
|
||||||
}
|
}
|
||||||
_, err := globalSubnetConfig.Post(url, ch)
|
|
||||||
|
filename := fmt.Sprintf("health_%s.json.gz", UTCNow().Format("20060102150405"))
|
||||||
|
url += "?filename=" + filename
|
||||||
|
|
||||||
|
_, err := globalSubnetConfig.Upload(url, filename, createHealthJSONGzip(ctx, healthInfo))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createHealthJSONGzip(ctx context.Context, healthInfo madmin.HealthInfo) []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
gzWriter := gzip.NewWriter(&b)
|
||||||
|
|
||||||
|
header := struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}{Version: healthInfo.Version}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(gzWriter)
|
||||||
|
if e := enc.Encode(header); e != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Could not encode health info header: %w", e))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e := enc.Encode(healthInfo); e != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Could not encode health info: %w", e))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gzWriter.Flush()
|
||||||
|
gzWriter.Close()
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -33,25 +34,38 @@ const (
|
|||||||
respBodyLimit = 1 << 20 // 1 MiB
|
respBodyLimit = 1 << 20 // 1 MiB
|
||||||
)
|
)
|
||||||
|
|
||||||
// Post submit 'payload' to specified URL
|
// Upload given file content (payload) to specified URL
|
||||||
func (c Config) Post(reqURL string, payload interface{}) (string, error) {
|
func (c Config) Upload(reqURL string, filename string, payload []byte) (string, error) {
|
||||||
if !c.Registered() {
|
if !c.Registered() {
|
||||||
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
|
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
|
||||||
}
|
}
|
||||||
body, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
var body bytes.Buffer
|
||||||
return "", err
|
writer := multipart.NewWriter(&body)
|
||||||
}
|
part, e := writer.CreateFormFile("file", filename)
|
||||||
r, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
|
if e != nil {
|
||||||
if err != nil {
|
return "", e
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, e = part.Write(payload); e != nil {
|
||||||
|
return "", e
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
r, e := http.NewRequest(http.MethodPost, reqURL, &body)
|
||||||
|
if e != nil {
|
||||||
|
return "", e
|
||||||
|
}
|
||||||
|
r.Header.Add("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
return c.submitPost(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) submitPost(r *http.Request) (string, error) {
|
||||||
configLock.RLock()
|
configLock.RLock()
|
||||||
r.Header.Set("Authorization", c.APIKey)
|
r.Header.Set(xhttp.SubnetAPIKey, c.APIKey)
|
||||||
configLock.RUnlock()
|
configLock.RUnlock()
|
||||||
|
r.Header.Set(xhttp.MinioDeploymentID, xhttp.GlobalDeploymentID)
|
||||||
r.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
@ -76,3 +90,22 @@ func (c Config) Post(reqURL string, payload interface{}) (string, error) {
|
|||||||
|
|
||||||
return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr)
|
return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post submit 'payload' to specified URL
|
||||||
|
func (c Config) Post(reqURL string, payload interface{}) (string, error) {
|
||||||
|
if !c.Registered() {
|
||||||
|
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
r, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return c.submitPost(r)
|
||||||
|
}
|
||||||
|
@ -210,6 +210,9 @@ const (
|
|||||||
|
|
||||||
// MinIOCompressed is returned when object is compressed
|
// MinIOCompressed is returned when object is compressed
|
||||||
MinIOCompressed = "X-Minio-Compressed"
|
MinIOCompressed = "X-Minio-Compressed"
|
||||||
|
|
||||||
|
// SUBNET related
|
||||||
|
SubnetAPIKey = "x-subnet-api-key"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common http query params S3 API
|
// Common http query params S3 API
|
||||||
|
Loading…
x
Reference in New Issue
Block a user