1
0
mirror of https://github.com/minio/mc.git synced 2026-01-04 02:44:40 +03:00
Files
mc/cmd/ping.go
2024-11-13 05:46:18 -08:00

394 lines
13 KiB
Go

// Copyright (c) 2015-2022 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 <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"fmt"
"math"
"net/url"
"strconv"
"strings"
"text/tabwriter"
"text/template"
"time"
"github.com/fatih/color"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/madmin-go/v3"
"github.com/minio/mc/pkg/probe"
"github.com/minio/pkg/v3/console"
)
var pingFlags = []cli.Flag{
cli.IntFlag{
Name: "count, c",
Usage: "perform liveliness check for count number of times",
},
cli.IntFlag{
Name: "error-count, e",
Usage: "exit after N consecutive ping errors",
},
cli.BoolFlag{
Name: "exit, x",
Usage: "exit when server(s) responds and reports being online",
},
cli.IntFlag{
Name: "interval, i",
Usage: "wait interval between each request in seconds",
Value: 1,
},
cli.BoolFlag{
Name: "distributed, a",
Usage: "ping all the servers in the cluster, use it when you have direct access to nodes/pods",
},
}
// return latency and liveness probe.
var pingCmd = cli.Command{
Name: "ping",
Usage: "perform liveness check",
Action: mainPing,
Before: setGlobalsFromContext,
OnUsageError: onUsageError,
Flags: append(pingFlags, globalFlags...),
HideHelpCommand: true,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET [TARGET...]
{{if .VisibleFlags}}
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
EXAMPLES:
1. Return Latency and liveness probe.
{{.Prompt}} {{.HelpName}} myminio
2. Return Latency and liveness probe 5 number of times.
{{.Prompt}} {{.HelpName}} --count 5 myminio
3. Return Latency and liveness with wait interval set to 30 seconds.
{{.Prompt}} {{.HelpName}} --interval 30 myminio
4. Stop pinging when error count > 20.
{{.Prompt}} {{.HelpName}} --error-count 20 myminio
`,
}
var stop bool
// Validate command line arguments.
func checkPingSyntax(cliCtx *cli.Context) {
if !cliCtx.Args().Present() {
showCommandHelpAndExit(cliCtx, 1) // last argument is exit code
}
}
// JSON jsonified ping result message.
func (pr PingResult) JSON() string {
statusJSONBytes, e := json.MarshalIndent(pr, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(statusJSONBytes)
}
var colorMap = template.FuncMap{
"colorWhite": color.New(color.FgWhite).SprintfFunc(),
"colorRed": color.New(color.FgRed).SprintfFunc(),
}
// PingDist is the template for ping result in distributed mode
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}}
{{end}}`
// Ping is the template for ping result
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}}`
// PingTemplateDist - captures ping template
var PingTemplateDist = template.Must(template.New("ping-list").Funcs(colorMap).Parse(PingDist))
// PingTemplate - captures ping template
var PingTemplate = template.Must(template.New("ping-list").Funcs(colorMap).Parse(Ping))
// String colorized service status message.
func (pr PingResult) String() string {
var s strings.Builder
w := tabwriter.NewWriter(&s, 1, 8, 3, ' ', 0)
var e error
if len(pr.EndPointsStats) > 1 {
e = PingTemplateDist.Execute(w, pr)
} else {
e = PingTemplate.Execute(w, pr)
}
fatalIf(probe.NewError(e), "Unable to initialize template writer")
w.Flush()
return s.String()
}
// EndPointStats - container to hold server ping stats
type EndPointStats struct {
Endpoint *url.URL `json:"endpoint"`
Min string `json:"min"`
Max string `json:"max"`
Average string `json:"average"`
DNS string `json:"dns"`
CountErr string `json:"error-count,omitempty"`
Error string `json:"error,omitempty"`
Roundtrip string `json:"roundtrip"`
}
// PingResult contains ping output
type PingResult struct {
Status string `json:"status"`
Counter string `json:"counter"`
EndPointsStats []EndPointStats `json:"servers"`
}
type serverStats struct {
min uint64
max uint64
sum uint64
avg uint64
dns uint64 // last DNS resolving time
errorCount int // used to keep a track of consecutive errors
err string
counter int // used to find the average, acts as denominator
}
func fetchAdminInfo(admClnt *madmin.AdminClient) (madmin.InfoMessage, error) {
ctx, cancel := context.WithTimeout(globalContext, 3*time.Second)
// Fetch the service status of the specified MinIO server
info, e := admClnt.ServerInfo(ctx)
cancel()
if e == nil {
return info, nil
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
for {
select {
case <-globalContext.Done():
return madmin.InfoMessage{}, globalContext.Err()
case <-timer.C:
ctx, cancel := context.WithTimeout(globalContext, 3*time.Second)
info, e := admClnt.ServerInfo(ctx)
cancel()
if e == nil {
return info, nil
}
timer.Reset(time.Second)
}
}
}
func ping(ctx context.Context, cliCtx *cli.Context, anonClient *madmin.AnonymousClient, admInfo madmin.InfoMessage, endPointMap map[string]serverStats, index int) {
var endPointStats []EndPointStats
var servers []madmin.ServerProperties
if cliCtx.Bool("distributed") {
servers = admInfo.Servers
}
allOK := true
for result := range anonClient.Alive(ctx, madmin.AliveOpts{}, servers...) {
stat := pingStats(cliCtx, result, endPointMap)
allOK = allOK && result.Online
endPointStat := EndPointStats{
Endpoint: result.Endpoint,
Min: trimToTwoDecimal(time.Duration(stat.min)),
Max: trimToTwoDecimal(time.Duration(stat.max)),
Average: trimToTwoDecimal(time.Duration(stat.avg)),
DNS: time.Duration(stat.dns).String(),
CountErr: pad(strconv.Itoa(stat.errorCount), " ", 3-len(strconv.Itoa(stat.errorCount)), false),
Error: stat.err,
Roundtrip: trimToTwoDecimal(result.ResponseTime),
}
endPointStats = append(endPointStats, endPointStat)
endPointMap[result.Endpoint.Host] = stat
}
stop = stop || cliCtx.Bool("exit") && allOK
printMsg(PingResult{
Status: "success",
Counter: pad(strconv.Itoa(index), " ", 3-len(strconv.Itoa(index)), true),
EndPointsStats: endPointStats,
})
if !stop {
time.Sleep(time.Duration(cliCtx.Int("interval")) * time.Second)
}
}
func trimToTwoDecimal(d time.Duration) string {
var f float64
var unit string
switch {
case d >= time.Second:
f = float64(d) / float64(time.Second)
unit = pad("s", " ", 7-len(fmt.Sprintf("%.02f", f)), false)
default:
f = float64(d) / float64(time.Millisecond)
unit = pad("ms", " ", 6-len(fmt.Sprintf("%.02f", f)), false)
}
return fmt.Sprintf("%.02f%s", f, unit)
}
// pad adds the `count` number of p string to string s. left true adds to the
// left and vice-versa. This is done for proper alignment of ping command
// ex:- padding 2 white space to right '90.18s' - > '90.18s '
func pad(s, p string, count int, left bool) string {
ret := make([]byte, len(p)*count+len(s))
if left {
b := ret[:len(p)*count]
bp := copy(b, p)
for bp < len(b) {
copy(b[bp:], b[:bp])
bp *= 2
}
copy(ret[len(b):], s)
} else {
b := ret[len(s) : len(p)*count+len(s)]
bp := copy(b, p)
for bp < len(b) {
copy(b[bp:], b[:bp])
bp *= 2
}
copy(ret[:len(s)], s)
}
return string(ret)
}
func pingStats(cliCtx *cli.Context, result madmin.AliveResult, serverMap map[string]serverStats) serverStats {
var errorString string
var sum, avg, dns uint64
minPing := uint64(math.MaxUint64)
var maxPing uint64
var counter, errorCount int
if result.Error != nil {
errorString = result.Error.Error()
if stat, ok := serverMap[result.Endpoint.Host]; ok {
minPing = stat.min
maxPing = stat.max
sum = stat.sum
counter = stat.counter
avg = stat.avg
errorCount = stat.errorCount + 1
} else {
minPing = 0
errorCount = 1
}
if cliCtx.IsSet("error-count") && errorCount >= cliCtx.Int("error-count") {
stop = true
}
} else {
// reset consecutive error count
errorCount = 0
if stat, ok := serverMap[result.Endpoint.Host]; ok {
var minVal uint64
if stat.min == 0 {
minVal = uint64(result.ResponseTime)
} else {
minVal = stat.min
}
minPing = uint64(math.Min(float64(minVal), float64(uint64(result.ResponseTime))))
maxPing = uint64(math.Max(float64(stat.max), float64(uint64(result.ResponseTime))))
sum = stat.sum + uint64(result.ResponseTime.Nanoseconds())
counter = stat.counter + 1
} else {
minPing = uint64(math.Min(float64(minPing), float64(uint64(result.ResponseTime))))
maxPing = uint64(math.Max(float64(maxPing), float64(uint64(result.ResponseTime))))
sum = uint64(result.ResponseTime)
counter = 1
}
avg = sum / uint64(counter)
dns = uint64(result.DNSResolveTime.Nanoseconds())
}
return serverStats{minPing, maxPing, sum, avg, dns, errorCount, errorString, counter}
}
// mainPing is entry point for ping command.
func mainPing(cliCtx *cli.Context) error {
// check 'ping' cli arguments.
checkPingSyntax(cliCtx)
console.SetColor("Info", color.New(color.FgGreen, color.Bold))
console.SetColor("InfoFail", color.New(color.FgRed, color.Bold))
ctx, cancel := context.WithCancel(globalContext)
defer cancel()
aliasedURL := cliCtx.Args().Get(0)
admClient, err := newAdminClient(aliasedURL)
fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client for `"+aliasedURL+"`.")
anonClient, err := newAnonymousClient(aliasedURL)
fatalIf(err.Trace(aliasedURL), "Unable to initialize anonymous client for `"+aliasedURL+"`.")
var admInfo madmin.InfoMessage
if cliCtx.Bool("distributed") {
var e error
admInfo, e = fetchAdminInfo(admClient)
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get server info")
}
// map to contain server stats for all the servers
serverMap := make(map[string]serverStats)
index := 1
if cliCtx.IsSet("count") {
count := cliCtx.Int("count")
if count < 1 {
fatalIf(errInvalidArgument().Trace(cliCtx.Args()...), "ping count cannot be less than 1")
}
for index <= count {
// return if consecutive error count more then specified value
if stop {
return nil
}
ping(ctx, cliCtx, anonClient, admInfo, serverMap, index)
index++
}
} else {
for {
select {
case <-globalContext.Done():
return globalContext.Err()
default:
// return if consecutive error count more then specified value
if stop {
return nil
}
ping(ctx, cliCtx, anonClient, admInfo, serverMap, index)
index++
}
}
}
return nil
}