1
0
mirror of https://github.com/minio/mc.git synced 2026-01-04 02:44:40 +03:00

update ping command with new terminal UI (#4163)

This commit is contained in:
Ashish Kumar Sinha
2022-08-08 12:34:14 +05:30
committed by GitHub
parent 6cee296d04
commit 63d0e213c3
3 changed files with 279 additions and 196 deletions

View File

@@ -35,6 +35,7 @@ version manage bucket versioning
replicate configure server side bucket replication
admin manage MinIO servers
update update mc to latest release
ping perform liveness check
```
## Docker Container

View File

@@ -19,39 +19,41 @@ package cmd
import (
"context"
"fmt"
"os"
"sort"
"errors"
"math"
"net"
"net/url"
"strconv"
"strings"
"text/tabwriter"
"text/template"
"time"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/fatih/color"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/madmin-go"
"github.com/minio/mc/pkg/probe"
"github.com/minio/pkg/console"
"github.com/olekukonko/tablewriter"
)
const (
pingInterval = time.Second // keep it similar to unix ping interval
)
var pingFlags = []cli.Flag{
cli.IntFlag{
Name: "count, c",
Usage: "perform liveliness check for count number of times",
Value: 4,
},
cli.DurationFlag{
cli.IntFlag{
Name: "error-count, e",
Usage: "exit after N consecutive ping errors",
},
cli.IntFlag{
Name: "interval, i",
Usage: "Wait interval between each request",
Value: pingInterval,
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",
},
}
@@ -81,10 +83,15 @@ EXAMPLES:
{{.Prompt}} {{.HelpName}} --count 5 myminio
3. Return Latency and liveness with wait interval set to 30 seconds.
{{.Prompt}} {{.HelpName}} --interval 30s myminio
{{.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() {
@@ -92,17 +99,6 @@ func checkPingSyntax(cliCtx *cli.Context) {
}
}
// PingResult is result for each ping
type PingResult struct {
madmin.AliveResult
}
// PingResults is result for each ping for all hosts
type PingResults struct {
Results map[string][]PingResult
Final bool
}
// JSON jsonified ping result message.
func (pr PingResult) JSON() string {
statusJSONBytes, e := json.MarshalIndent(pr, "", " ")
@@ -111,141 +107,72 @@ func (pr PingResult) JSON() string {
return string(statusJSONBytes)
}
// String colorized ping result message.
func (pr PingResult) String() (msg string) {
if pr.Error == nil {
coloredDot := console.Colorize("Info", dot)
// Print server title
msg += fmt.Sprintf("%s %s:", coloredDot, console.Colorize("PrintB", pr.Endpoint.String()))
msg += fmt.Sprintf(" time=%s\n", pr.ResponseTime)
return
}
coloredDot := console.Colorize("InfoFail", dot)
msg += fmt.Sprintf("%s %s:", coloredDot, console.Colorize("PrintB", pr.Endpoint.String()))
msg += fmt.Sprintf(" time=%s, error=%s\n", pr.ResponseTime, console.Colorize("InfoFail", pr.Error.Error()))
return msg
var colorMap = template.FuncMap{
"colorWhite": color.New(color.FgWhite).SprintfFunc(),
"colorRed": color.New(color.FgRed).SprintfFunc(),
}
type pingUI struct {
spinner spinner.Model
quitting bool
results PingResults
}
// 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}}{{"\t"}}{{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}}{{"\t"}}{{colorRed "roundtrip="}}{{colorRed .Roundtrip}}{{end}}
{{end}}`
func initPingUI() *pingUI {
s := spinner.New()
s.Spinner = spinner.Points
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return &pingUI{
spinner: s,
}
}
// 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}}{{"\t"}}{{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}}{{"\t"}}{{colorRed "roundtrip="}}{{colorRed .Roundtrip}}{{end}}{{end}}`
func (m *pingUI) Init() tea.Cmd {
return m.spinner.Tick
}
// PingTemplateDist - captures ping template
var PingTemplateDist = template.Must(template.New("ping-list").Funcs(colorMap).Parse(PingDist))
func (m *pingUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.quitting = true
return m, tea.Quit
default:
return m, nil
}
case PingResults:
m.results = msg
if msg.Final {
m.quitting = true
return m, tea.Quit
}
return m, nil
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
default:
return m, nil
}
}
// PingTemplate - captures ping template
var PingTemplate = template.Must(template.New("ping-list").Funcs(colorMap).Parse(Ping))
func (m *pingUI) View() string {
// String colorized service status message.
func (pr PingResult) String() string {
var s strings.Builder
// Set table header
table := tablewriter.NewWriter(&s)
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)
res := m.results
if len(res.Results) > 0 {
s.WriteString("\n")
}
trailerIfGreaterThan := func(in string, max int) string {
if len(in) < max {
return in
}
return in[:max] + "..."
}
table.SetHeader([]string{"Node", "Avg-Latency", "Count", ""})
data := make([][]string, 0, len(res.Results))
if len(res.Results) == 0 {
data = append(data, []string{
"...",
whiteStyle.Render("-- ms"),
whiteStyle.Render("--"),
"",
})
w := tabwriter.NewWriter(&s, 1, 8, 3, ' ', 0)
var e error
if len(pr.EndPointsStats) > 1 {
e = PingTemplateDist.Execute(w, pr)
} else {
for k, results := range res.Results {
data = append(data, []string{
trailerIfGreaterThan(k, 64),
getAvgLatency(results...).String(),
strconv.Itoa(len(results)),
"",
})
}
sort.Slice(data, func(i, j int) bool {
return data[i][0] < data[j][0]
})
table.AppendBulk(data)
table.Render()
}
if !m.quitting {
s.WriteString(fmt.Sprintf("\nPinging: %s", m.spinner.View()))
} else {
s.WriteString("\n")
e = PingTemplate.Execute(w, pr)
}
fatalIf(probe.NewError(e), "Unable to initialize template writer")
w.Flush()
return s.String()
}
func getAvgLatency(results ...PingResult) (avg time.Duration) {
if len(results) == 0 {
return avg
}
var totalDurationNS uint64
for _, result := range results {
totalDurationNS += uint64(result.ResponseTime.Nanoseconds())
}
return time.Duration(totalDurationNS / uint64(len(results)))
// Endpoint - container to hold server info
type Endpoint struct {
Scheme string `json:"scheme"`
Host string `json:"host"`
Port string `json:"port"`
}
// EndPointStats - container to hold server ping stats
type EndPointStats struct {
Endpoint Endpoint `json:"endpoint"`
Min string `json:"min"`
Max string `json:"max"`
Average string `json:"average"`
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
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) {
@@ -264,12 +191,151 @@ func fetchAdminInfo(admClnt *madmin.AdminClient) (madmin.InfoMessage, error) {
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
}
for result := range anonClient.Alive(ctx, madmin.AliveOpts{}, servers...) {
host, port, _ := extractHostPort(result.Endpoint.String())
endPoint := Endpoint{result.Endpoint.Scheme, host, port}
stat := getPingInfo(cliCtx, result, endPointMap)
endPointStat := EndPointStats{
Endpoint: endPoint,
Min: time.Duration(stat.min).Round(time.Microsecond).String(),
Max: time.Duration(stat.max).Round(time.Microsecond).String(),
Average: time.Duration(stat.avg).Round(time.Microsecond).String(),
CountErr: strconv.Itoa(stat.errorCount),
Error: stat.err,
Roundtrip: result.ResponseTime.Round(time.Microsecond).String(),
}
endPointStats = append(endPointStats, endPointStat)
endPointMap[result.Endpoint.Host] = stat
}
printMsg(PingResult{
Status: "success",
Counter: strconv.Itoa(index),
EndPointsStats: endPointStats,
})
time.Sleep(time.Duration(cliCtx.Int("interval")) * time.Second)
}
func getPingInfo(cliCtx *cli.Context, result madmin.AliveResult, serverMap map[string]serverStats) serverStats {
var errorString string
var sum, avg uint64
min := uint64(math.MaxUint64)
var max uint64
var counter, errorCount int
if result.Error != nil {
errorString = result.Error.Error()
if stat, ok := serverMap[result.Endpoint.Host]; ok {
min = stat.min
max = stat.max
sum = stat.sum
counter = stat.counter
avg = stat.avg
errorCount = stat.errorCount + 1
} else {
min = 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
}
min = uint64(math.Min(float64(minVal), float64(uint64(result.ResponseTime))))
max = uint64(math.Max(float64(stat.max), float64(uint64(result.ResponseTime))))
sum = stat.sum + uint64(result.ResponseTime.Nanoseconds())
counter = stat.counter + 1
} else {
min = uint64(math.Min(float64(min), float64(uint64(result.ResponseTime))))
max = uint64(math.Max(float64(max), float64(uint64(result.ResponseTime))))
sum = uint64(result.ResponseTime)
counter = 1
}
avg = sum / uint64(counter)
}
return serverStats{min, max, sum, avg, errorCount, errorString, counter}
}
// extractHostPort - extracts host/port from many address formats
// such as, ":9000", "localhost:9000", "http://localhost:9000/"
func extractHostPort(hostAddr string) (string, string, error) {
var addr, scheme string
if hostAddr == "" {
return "", "", errors.New("unable to process empty address")
}
// Simplify the work of url.Parse() and always send a url with
if !strings.HasPrefix(hostAddr, "http://") && !strings.HasPrefix(hostAddr, "https://") {
hostAddr = "//" + hostAddr
}
// Parse address to extract host and scheme field
u, err := url.Parse(hostAddr)
if err != nil {
return "", "", err
}
addr = u.Host
scheme = u.Scheme
// Use the given parameter again if url.Parse()
// didn't return any useful result.
if addr == "" {
addr = hostAddr
scheme = "http"
}
// At this point, addr can be one of the following form:
// ":9000"
// "localhost:9000"
// "localhost" <- in this case, we check for scheme
host, port, err := net.SplitHostPort(addr)
if err != nil {
if !strings.Contains(err.Error(), "missing port in address") {
return "", "", err
}
host = addr
switch scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
return "", "", errors.New("unable to guess port from scheme")
}
}
return host, port, nil
}
// mainPing is entry point for ping command.
func mainPing(cliCtx *cli.Context) error {
// check 'ping' cli arguments.
@@ -282,61 +348,46 @@ func mainPing(cliCtx *cli.Context) error {
defer cancel()
aliasedURL := cliCtx.Args().Get(0)
count := cliCtx.Int("count")
if count < 1 {
fatalIf(errInvalidArgument().Trace(cliCtx.Args()...), "ping count cannot be less than 1")
}
interval := cliCtx.Duration("interval")
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+"`.")
done := make(chan struct{})
ui := tea.NewProgram(initPingUI())
if !globalJSON {
go func() {
if e := ui.Start(); e != nil {
cancel()
os.Exit(1)
}
close(done)
}()
}
admInfo, e := fetchAdminInfo(admClient)
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get server info")
pingResults := PingResults{
Results: make(map[string][]PingResult),
}
for i := 0; i < count; i++ {
for result := range anonClient.Alive(ctx, madmin.AliveOpts{}, admInfo.Servers...) {
if globalJSON {
printMsg(PingResult{result})
} else {
hostResults, ok := pingResults.Results[result.Endpoint.Host]
if !ok {
pingResults.Results[result.Endpoint.Host] = []PingResult{{result}}
} else {
hostResults = append(hostResults, PingResult{result})
pingResults.Results[result.Endpoint.Host] = hostResults
// 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
}
ui.Send(pingResults)
ping(ctx, cliCtx, anonClient, admInfo, serverMap, index)
index++
}
}
time.Sleep(interval)
}
if !globalJSON {
pingResults.Final = true
ui.Send(pingResults)
<-done
}
return nil
}

View File

@@ -35,6 +35,7 @@ replicate configure server side bucket replication
admin manage MinIO servers
update update mc to latest release
support supportability tools like profile, register, callhome, inspect
ping perform liveness check
```
## 1. Download MinIO Client
@@ -325,10 +326,11 @@ mc version RELEASE.2020-04-25T00-43-23Z
| [**update** - manage software updates](#update) | [**watch** - watch for events](#watch) | [**retention** - set retention for object(s)](#retention) | [**sql** - run sql queries on objects](#sql) |
| [**head** - display first 'n' lines of an object](#head) | [**stat** - stat contents of objects and folders](#stat) | [**legalhold** - set legal hold for object(s)](#legalhold) | [**mv** - move objects](#mv) |
| [**du** - summarize disk usage recursively](#du) | [**tag** - manage tags for bucket and object(s)](#tag) | [**admin** - manage MinIO servers](#admin) | [**support** - generate profile data for debugging purposes](#support) |
| [**ping** - perform liveness check](#ping) | | | |
### Command `ls`
### Command `ls`
`ls` command lists files, buckets and objects. Use `--incomplete` flag to list partially copied content.
```
@@ -1964,4 +1966,33 @@ mc support logs show --last 5 --type application myminio node1
Enable logs for cluster with alias 'play'
```
mc support logs enable play
```
```
<a name="ping"></a>
### Command `ping`
`rb` command to perform liveness check
```
USAGE:
mc ping [FLAGS] TARGET
FLAGS:
--count value, -c value perform liveliness check for count number of times (default: 0)
--error-count value, -e value exit after N consecutive ping errors
--interval value, -i value wait interval between each request in seconds (default: 1)
--distributed, -a ping all the servers in the cluster, use it when you have direct access to nodes/pods
--help, -h show help
```
*Example: Perform liveness check on https://play.min.io.*
```
mc ping play
1: https://play.min.io: min=919.538ms max=919.538ms average=919.538ms errors=0 roundtrip=919.538ms
2: https://play.min.io: min=278.356ms max=919.538ms average=598.947ms errors=0 roundtrip=278.356ms
3: https://play.min.io: min=278.356ms max=919.538ms average=504.759ms errors=0 roundtrip=316.384ms
```