github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/ping.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"math"
    24  	"net/url"
    25  	"strconv"
    26  	"strings"
    27  	"text/tabwriter"
    28  	"text/template"
    29  	"time"
    30  
    31  	"github.com/fatih/color"
    32  	"github.com/minio/cli"
    33  	json "github.com/minio/colorjson"
    34  	"github.com/minio/madmin-go/v3"
    35  	"github.com/minio/mc/pkg/probe"
    36  	"github.com/minio/pkg/v2/console"
    37  )
    38  
    39  var pingFlags = []cli.Flag{
    40  	cli.IntFlag{
    41  		Name:  "count, c",
    42  		Usage: "perform liveliness check for count number of times",
    43  	},
    44  	cli.IntFlag{
    45  		Name:  "error-count, e",
    46  		Usage: "exit after N consecutive ping errors",
    47  	},
    48  	cli.BoolFlag{
    49  		Name:  "exit, x",
    50  		Usage: "exit when server(s) responds and reports being online",
    51  	},
    52  	cli.IntFlag{
    53  		Name:  "interval, i",
    54  		Usage: "wait interval between each request in seconds",
    55  		Value: 1,
    56  	},
    57  	cli.BoolFlag{
    58  		Name:  "distributed, a",
    59  		Usage: "ping all the servers in the cluster, use it when you have direct access to nodes/pods",
    60  	},
    61  }
    62  
    63  // return latency and liveness probe.
    64  var pingCmd = cli.Command{
    65  	Name:            "ping",
    66  	Usage:           "perform liveness check",
    67  	Action:          mainPing,
    68  	Before:          setGlobalsFromContext,
    69  	OnUsageError:    onUsageError,
    70  	Flags:           append(pingFlags, globalFlags...),
    71  	HideHelpCommand: true,
    72  	CustomHelpTemplate: `NAME:
    73    {{.HelpName}} - {{.Usage}}
    74  
    75  USAGE:
    76    {{.HelpName}} [FLAGS] TARGET [TARGET...]
    77  {{if .VisibleFlags}}
    78  FLAGS:
    79    {{range .VisibleFlags}}{{.}}
    80    {{end}}{{end}}
    81  EXAMPLES:
    82    1. Return Latency and liveness probe.
    83       {{.Prompt}} {{.HelpName}} myminio
    84  
    85    2. Return Latency and liveness probe 5 number of times.
    86       {{.Prompt}} {{.HelpName}} --count 5 myminio
    87  
    88    3. Return Latency and liveness with wait interval set to 30 seconds.
    89       {{.Prompt}} {{.HelpName}} --interval 30 myminio
    90  
    91    4. Stop pinging when error count > 20.
    92       {{.Prompt}} {{.HelpName}} --error-count 20 myminio
    93  `,
    94  }
    95  
    96  var stop bool
    97  
    98  // Validate command line arguments.
    99  func checkPingSyntax(cliCtx *cli.Context) {
   100  	if !cliCtx.Args().Present() {
   101  		showCommandHelpAndExit(cliCtx, 1) // last argument is exit code
   102  	}
   103  }
   104  
   105  // JSON jsonified ping result message.
   106  func (pr PingResult) JSON() string {
   107  	statusJSONBytes, e := json.MarshalIndent(pr, "", " ")
   108  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   109  
   110  	return string(statusJSONBytes)
   111  }
   112  
   113  var colorMap = template.FuncMap{
   114  	"colorWhite": color.New(color.FgWhite).SprintfFunc(),
   115  	"colorRed":   color.New(color.FgRed).SprintfFunc(),
   116  }
   117  
   118  // PingDist is the template for ping result in distributed mode
   119  const PingDist = `{{$x := .Counter}}{{range .EndPointsStats}}{{if eq "0  " .CountErr}}{{colorWhite $x}}{{colorWhite ": "}}{{colorWhite .Endpoint.Scheme}}{{colorWhite "://"}}{{colorWhite .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorWhite ":"}}{{colorWhite .Endpoint.Port}}{{end}}{{"\t"}}{{ colorWhite "min="}}{{colorWhite .Min}}{{"\t"}}{{colorWhite "max="}}{{colorWhite .Max}}{{"\t"}}{{colorWhite "average="}}{{colorWhite .Average}}{{"\t"}}{{colorWhite "errors="}}{{colorWhite .CountErr}}{{" "}}{{colorWhite "roundtrip="}}{{colorWhite .Roundtrip}}{{else}}{{colorRed $x}}{{colorRed ": "}}{{colorRed .Endpoint.Scheme}}{{colorRed "://"}}{{colorRed .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorRed ":"}}{{colorRed .Endpoint.Port}}{{end}}{{"\t"}}{{ colorRed "min="}}{{colorRed .Min}}{{"\t"}}{{colorRed "max="}}{{colorRed .Max}}{{"\t"}}{{colorRed "average="}}{{colorRed .Average}}{{"\t"}}{{colorRed "errors="}}{{colorRed .CountErr}}{{" "}}{{colorRed "roundtrip="}}{{colorRed .Roundtrip}}{{end}}
   120  {{end}}`
   121  
   122  // Ping is the template for ping result
   123  const Ping = `{{$x := .Counter}}{{range .EndPointsStats}}{{if eq "0  " .CountErr}}{{colorWhite $x}}{{colorWhite ": "}}{{colorWhite .Endpoint.Scheme}}{{colorWhite "://"}}{{colorWhite .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorWhite ":"}}{{colorWhite .Endpoint.Port}}{{end}}{{"\t"}}{{ colorWhite "min="}}{{colorWhite .Min}}{{"\t"}}{{colorWhite "max="}}{{colorWhite .Max}}{{"\t"}}{{colorWhite "average="}}{{colorWhite .Average}}{{"\t"}}{{colorWhite "errors="}}{{colorWhite .CountErr}}{{" "}}{{colorWhite "roundtrip="}}{{colorWhite .Roundtrip}}{{else}}{{colorRed $x}}{{colorRed ": "}}{{colorRed .Endpoint.Scheme}}{{colorRed "://"}}{{colorRed .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorRed ":"}}{{colorRed .Endpoint.Port}}{{end}}{{"\t"}}{{ colorRed "min="}}{{colorRed .Min}}{{"\t"}}{{colorRed "max="}}{{colorRed .Max}}{{"\t"}}{{colorRed "average="}}{{colorRed .Average}}{{"\t"}}{{colorRed "errors="}}{{colorRed .CountErr}}{{" "}}{{colorRed "roundtrip="}}{{colorRed .Roundtrip}}{{end}}{{end}}`
   124  
   125  // PingTemplateDist - captures ping template
   126  var PingTemplateDist = template.Must(template.New("ping-list").Funcs(colorMap).Parse(PingDist))
   127  
   128  // PingTemplate - captures ping template
   129  var PingTemplate = template.Must(template.New("ping-list").Funcs(colorMap).Parse(Ping))
   130  
   131  // String colorized service status message.
   132  func (pr PingResult) String() string {
   133  	var s strings.Builder
   134  	w := tabwriter.NewWriter(&s, 1, 8, 3, ' ', 0)
   135  	var e error
   136  	if len(pr.EndPointsStats) > 1 {
   137  		e = PingTemplateDist.Execute(w, pr)
   138  	} else {
   139  		e = PingTemplate.Execute(w, pr)
   140  	}
   141  	fatalIf(probe.NewError(e), "Unable to initialize template writer")
   142  	w.Flush()
   143  	return s.String()
   144  }
   145  
   146  // EndPointStats - container to hold server ping stats
   147  type EndPointStats struct {
   148  	Endpoint  *url.URL `json:"endpoint"`
   149  	Min       string   `json:"min"`
   150  	Max       string   `json:"max"`
   151  	Average   string   `json:"average"`
   152  	DNS       string   `json:"dns"`
   153  	CountErr  string   `json:"error-count,omitempty"`
   154  	Error     string   `json:"error,omitempty"`
   155  	Roundtrip string   `json:"roundtrip"`
   156  }
   157  
   158  // PingResult contains ping output
   159  type PingResult struct {
   160  	Status         string          `json:"status"`
   161  	Counter        string          `json:"counter"`
   162  	EndPointsStats []EndPointStats `json:"servers"`
   163  }
   164  
   165  type serverStats struct {
   166  	min        uint64
   167  	max        uint64
   168  	sum        uint64
   169  	avg        uint64
   170  	dns        uint64 // last DNS resolving time
   171  	errorCount int    // used to keep a track of consecutive errors
   172  	err        string
   173  	counter    int // used to find the average, acts as denominator
   174  }
   175  
   176  func fetchAdminInfo(admClnt *madmin.AdminClient) (madmin.InfoMessage, error) {
   177  	ctx, cancel := context.WithTimeout(globalContext, 3*time.Second)
   178  	// Fetch the service status of the specified MinIO server
   179  	info, e := admClnt.ServerInfo(ctx)
   180  	cancel()
   181  	if e == nil {
   182  		return info, nil
   183  	}
   184  
   185  	timer := time.NewTimer(time.Second)
   186  	defer timer.Stop()
   187  
   188  	for {
   189  		select {
   190  		case <-globalContext.Done():
   191  			return madmin.InfoMessage{}, globalContext.Err()
   192  		case <-timer.C:
   193  			ctx, cancel := context.WithTimeout(globalContext, 3*time.Second)
   194  			info, e := admClnt.ServerInfo(ctx)
   195  			cancel()
   196  			if e == nil {
   197  				return info, nil
   198  			}
   199  			timer.Reset(time.Second)
   200  		}
   201  	}
   202  }
   203  
   204  func ping(ctx context.Context, cliCtx *cli.Context, anonClient *madmin.AnonymousClient, admInfo madmin.InfoMessage, endPointMap map[string]serverStats, index int) {
   205  	var endPointStats []EndPointStats
   206  	var servers []madmin.ServerProperties
   207  	if cliCtx.Bool("distributed") {
   208  		servers = admInfo.Servers
   209  	}
   210  	allOK := true
   211  
   212  	for result := range anonClient.Alive(ctx, madmin.AliveOpts{}, servers...) {
   213  		stat := pingStats(cliCtx, result, endPointMap)
   214  
   215  		allOK = allOK && result.Online
   216  		endPointStat := EndPointStats{
   217  			Endpoint:  result.Endpoint,
   218  			Min:       trimToTwoDecimal(time.Duration(stat.min)),
   219  			Max:       trimToTwoDecimal(time.Duration(stat.max)),
   220  			Average:   trimToTwoDecimal(time.Duration(stat.avg)),
   221  			DNS:       time.Duration(stat.dns).String(),
   222  			CountErr:  pad(strconv.Itoa(stat.errorCount), " ", 3-len(strconv.Itoa(stat.errorCount)), false),
   223  			Error:     stat.err,
   224  			Roundtrip: trimToTwoDecimal(result.ResponseTime),
   225  		}
   226  		endPointStats = append(endPointStats, endPointStat)
   227  		endPointMap[result.Endpoint.Host] = stat
   228  
   229  	}
   230  	stop = stop || cliCtx.Bool("exit") && allOK
   231  
   232  	printMsg(PingResult{
   233  		Status:         "success",
   234  		Counter:        pad(strconv.Itoa(index), " ", 3-len(strconv.Itoa(index)), true),
   235  		EndPointsStats: endPointStats,
   236  	})
   237  	if !stop {
   238  		time.Sleep(time.Duration(cliCtx.Int("interval")) * time.Second)
   239  	}
   240  }
   241  
   242  func trimToTwoDecimal(d time.Duration) string {
   243  	var f float64
   244  	var unit string
   245  	switch {
   246  	case d >= time.Second:
   247  		f = float64(d) / float64(time.Second)
   248  
   249  		unit = pad("s", " ", 7-len(fmt.Sprintf("%.02f", f)), false)
   250  	default:
   251  		f = float64(d) / float64(time.Millisecond)
   252  		unit = pad("ms", " ", 6-len(fmt.Sprintf("%.02f", f)), false)
   253  	}
   254  	return fmt.Sprintf("%.02f%s", f, unit)
   255  }
   256  
   257  // pad adds the `count` number of p string to string s. left true adds to the
   258  // left and vice-versa. This is done for proper alignment of ping command
   259  // ex:- padding 2 white space to right '90.18s' - > '90.18s  '
   260  func pad(s, p string, count int, left bool) string {
   261  	ret := make([]byte, len(p)*count+len(s))
   262  
   263  	if left {
   264  		b := ret[:len(p)*count]
   265  		bp := copy(b, p)
   266  		for bp < len(b) {
   267  			copy(b[bp:], b[:bp])
   268  			bp *= 2
   269  		}
   270  		copy(ret[len(b):], s)
   271  	} else {
   272  		b := ret[len(s) : len(p)*count+len(s)]
   273  		bp := copy(b, p)
   274  		for bp < len(b) {
   275  			copy(b[bp:], b[:bp])
   276  			bp *= 2
   277  		}
   278  		copy(ret[:len(s)], s)
   279  	}
   280  	return string(ret)
   281  }
   282  
   283  func pingStats(cliCtx *cli.Context, result madmin.AliveResult, serverMap map[string]serverStats) serverStats {
   284  	var errorString string
   285  	var sum, avg, dns uint64
   286  	min := uint64(math.MaxUint64)
   287  	var max uint64
   288  	var counter, errorCount int
   289  
   290  	if result.Error != nil {
   291  		errorString = result.Error.Error()
   292  		if stat, ok := serverMap[result.Endpoint.Host]; ok {
   293  			min = stat.min
   294  			max = stat.max
   295  			sum = stat.sum
   296  			counter = stat.counter
   297  			avg = stat.avg
   298  			errorCount = stat.errorCount + 1
   299  
   300  		} else {
   301  			min = 0
   302  			errorCount = 1
   303  		}
   304  		if cliCtx.IsSet("error-count") && errorCount >= cliCtx.Int("error-count") {
   305  			stop = true
   306  		}
   307  
   308  	} else {
   309  		// reset consecutive error count
   310  		errorCount = 0
   311  		if stat, ok := serverMap[result.Endpoint.Host]; ok {
   312  			var minVal uint64
   313  			if stat.min == 0 {
   314  				minVal = uint64(result.ResponseTime)
   315  			} else {
   316  				minVal = stat.min
   317  			}
   318  			min = uint64(math.Min(float64(minVal), float64(uint64(result.ResponseTime))))
   319  			max = uint64(math.Max(float64(stat.max), float64(uint64(result.ResponseTime))))
   320  			sum = stat.sum + uint64(result.ResponseTime.Nanoseconds())
   321  			counter = stat.counter + 1
   322  
   323  		} else {
   324  			min = uint64(math.Min(float64(min), float64(uint64(result.ResponseTime))))
   325  			max = uint64(math.Max(float64(max), float64(uint64(result.ResponseTime))))
   326  			sum = uint64(result.ResponseTime)
   327  			counter = 1
   328  		}
   329  		avg = sum / uint64(counter)
   330  		dns = uint64(result.DNSResolveTime.Nanoseconds())
   331  	}
   332  	return serverStats{min, max, sum, avg, dns, errorCount, errorString, counter}
   333  }
   334  
   335  // mainPing is entry point for ping command.
   336  func mainPing(cliCtx *cli.Context) error {
   337  	// check 'ping' cli arguments.
   338  	checkPingSyntax(cliCtx)
   339  
   340  	console.SetColor("Info", color.New(color.FgGreen, color.Bold))
   341  	console.SetColor("InfoFail", color.New(color.FgRed, color.Bold))
   342  
   343  	ctx, cancel := context.WithCancel(globalContext)
   344  	defer cancel()
   345  
   346  	aliasedURL := cliCtx.Args().Get(0)
   347  	admClient, err := newAdminClient(aliasedURL)
   348  	fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client for `"+aliasedURL+"`.")
   349  
   350  	anonClient, err := newAnonymousClient(aliasedURL)
   351  	fatalIf(err.Trace(aliasedURL), "Unable to initialize anonymous client for `"+aliasedURL+"`.")
   352  
   353  	var admInfo madmin.InfoMessage
   354  	if cliCtx.Bool("distributed") {
   355  		var e error
   356  		admInfo, e = fetchAdminInfo(admClient)
   357  		fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get server info")
   358  	}
   359  
   360  	// map to contain server stats for all the servers
   361  	serverMap := make(map[string]serverStats)
   362  
   363  	index := 1
   364  	if cliCtx.IsSet("count") {
   365  		count := cliCtx.Int("count")
   366  		if count < 1 {
   367  			fatalIf(errInvalidArgument().Trace(cliCtx.Args()...), "ping count cannot be less than 1")
   368  		}
   369  		for index <= count {
   370  			// return if consecutive error count more then specified value
   371  			if stop {
   372  				return nil
   373  			}
   374  			ping(ctx, cliCtx, anonClient, admInfo, serverMap, index)
   375  			index++
   376  		}
   377  	} else {
   378  		for {
   379  			select {
   380  			case <-globalContext.Done():
   381  				return globalContext.Err()
   382  			default:
   383  				// return if consecutive error count more then specified value
   384  				if stop {
   385  					return nil
   386  				}
   387  				ping(ctx, cliCtx, anonClient, admInfo, serverMap, index)
   388  				index++
   389  			}
   390  		}
   391  	}
   392  	return nil
   393  }