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 }