Skip to content

Commit

Permalink
Support mc top net (#4626)
Browse files Browse the repository at this point in the history
  • Loading branch information
jiuker authored Jul 18, 2023
1 parent c78ff23 commit 48dd257
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 4 deletions.
1 change: 1 addition & 0 deletions cmd/auto-complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ var completeCmds = map[string]complete.Predictor{
"/support/top/api": aliasCompleter,
"/support/top/drive": aliasCompleter,
"/support/top/disk": aliasCompleter,
"/support/top/net": aliasCompleter,

"/license/register": aliasCompleter,
"/license/info": aliasCompleter,
Expand Down
145 changes: 145 additions & 0 deletions cmd/support-top-net.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) 2015-2023 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"
"errors"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/minio/cli"
"github.com/minio/madmin-go/v3"
"github.com/minio/mc/pkg/probe"
)

var supportTopNetFlags = []cli.Flag{
cli.IntFlag{
Name: "count, c",
Usage: "show top count interfaces",
Value: 10,
},
cli.IntFlag{
Name: "interval",
Usage: "interval between requests in seconds",
Value: 1,
},
}

var supportTopNetCmd = cli.Command{
Name: "net",
Aliases: []string{"network"},
HiddenAliases: true,
Usage: "show real-time net metrics",
Action: mainSupportTopNet,
OnUsageError: onUsageError,
Before: setGlobalsFromContext,
Flags: append(supportTopNetFlags, supportGlobalFlags...),
HideHelpCommand: true,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Display net metrics
{{.Prompt}} {{.HelpName}} myminio/
`,
}

// checkSupportTopNetSyntax - validate all the passed arguments
func checkSupportTopNetSyntax(ctx *cli.Context) {
if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 {
showCommandHelpAndExit(ctx, 1) // last argument is exit code
}
}

func mainSupportTopNet(ctx *cli.Context) error {
checkSupportTopNetSyntax(ctx)

aliasedURL := ctx.Args().Get(0)
alias, _ := url2Alias(aliasedURL)
validateClusterRegistered(alias, false)

// Create a new MinIO Admin Client
client, err := newAdminClient(aliasedURL)
if err != nil {
fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.")
return nil
}

ctxt, cancel := context.WithCancel(globalContext)
defer cancel()

_, e := client.ServerInfo(ctxt)
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to initialize admin client.")

// MetricsOptions are options provided to Metrics call.
opts := madmin.MetricsOptions{
Type: madmin.MetricNet,
Interval: time.Duration(ctx.Int("interval")) * time.Second,
ByHost: true,
N: ctx.Int("count"),
}

p := tea.NewProgram(initTopNetUI())
go func() {
if globalJSON {
e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) {
printMsg(metricsMessage{RealtimeMetrics: metrics})
})
if e != nil && !errors.Is(e, context.Canceled) {
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch scanner metrics")
}
} else {
out := func(m madmin.RealtimeMetrics) {
for endPoint, metric := range m.ByHost {
if metric.Net != nil {
p.Send(topNetResult{
endPoint: endPoint,
stats: *metric.Net,
})
}
}
if len(m.Errors) != 0 && len(m.Hosts) != 0 {
p.Send(topNetResult{
endPoint: m.Hosts[0],
error: m.Errors[0],
})
}
}

e := client.Metrics(ctxt, opts, out)
if e != nil {
fatalIf(probe.NewError(e), "Unable to fetch top net events")
}
}
p.Quit()
}()

if _, e := p.Run(); e != nil {
cancel()
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch top net events")
}

return nil
}
3 changes: 2 additions & 1 deletion cmd/support-top.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2015-2022 MinIO, Inc.
// Copyright (c) 2015-2023 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
Expand All @@ -23,6 +23,7 @@ var supportTopSubcommands = []cli.Command{
supportTopAPICmd,
supportTopDriveCmd,
supportTopLocksCmd,
supportTopNetCmd,
}

var supportTopCmd = cli.Command{
Expand Down
147 changes: 147 additions & 0 deletions cmd/top-net-spinner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// 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 (
"fmt"
"sort"
"strings"

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v3"
"github.com/olekukonko/tablewriter"
)

type topNetUI struct {
spinner spinner.Model
quitting bool

sortAsc bool

currTopMap map[string]topNetResult
}

type topNetResult struct {
final bool
endPoint string
error string
stats madmin.NetMetrics
}

func (t topNetResult) GetTotalBytes() uint64 {
return t.stats.NetStats.RxBytes + t.stats.NetStats.TxBytes
}

func (m *topNetUI) Init() tea.Cmd {
return m.spinner.Tick
}

func (m *topNetUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q", "esc":
m.quitting = true
return m, tea.Quit
}
return m, nil
case topNetResult:
m.currTopMap[msg.endPoint] = 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
}
}

func (m *topNetUI) View() string {
var s strings.Builder
// Set table header
table := tablewriter.NewWriter(&s)
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_CENTER)
table.SetAlignment(tablewriter.ALIGN_CENTER)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)
table.SetHeader([]string{"SERVER", "INTERFACE", "RECEIVE", "TRANSMIT", ""})

data := make([]topNetResult, 0, len(m.currTopMap))

for _, metric := range m.currTopMap {
data = append(data, metric)
}

sort.Slice(data, func(i, j int) bool {
if m.sortAsc {
return data[i].GetTotalBytes() < data[j].GetTotalBytes()
}
return data[i].GetTotalBytes() >= data[j].GetTotalBytes()
})

dataRender := make([][]string, 0, len(data))
for _, d := range data {
if d.error == "" {
dataRender = append(dataRender, []string{
d.endPoint,
whiteStyle.Render(d.stats.InterfaceName),
whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(d.stats.NetStats.RxBytes))),
whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(d.stats.NetStats.TxBytes))),
"",
})
} else {
dataRender = append(dataRender, []string{
d.endPoint,
whiteStyle.Render(d.stats.NetStats.Name),
crossTickCell,
crossTickCell,
d.error,
})
}
}

table.AppendBulk(dataRender)
table.Render()
return s.String()
}

func initTopNetUI() *topNetUI {
s := spinner.New()
s.Spinner = spinner.Points
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return &topNetUI{
spinner: s,
currTopMap: make(map[string]topNetResult),
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ require (
github.com/gdamore/tcell/v2 v2.6.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/juju/ratelimit v1.0.2
github.com/minio/madmin-go/v3 v3.0.6
github.com/minio/madmin-go/v3 v3.0.7
github.com/muesli/reflow v0.3.0
github.com/navidys/tvxwidgets v0.3.0
github.com/olekukonko/tablewriter v0.0.5
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ github.com/minio/colorjson v1.0.5 h1:P//d521blE5cKDF5YvsHcpqb9TE8IwCdliSv1naPsgk
github.com/minio/colorjson v1.0.5/go.mod h1:Oq6oB83q+sL08u9wx68+91ELf0nV5G4c6l9pQcH5ElI=
github.com/minio/filepath v1.0.0 h1:fvkJu1+6X+ECRA6G3+JJETj4QeAYO9sV43I79H8ubDY=
github.com/minio/filepath v1.0.0/go.mod h1:/nRZA2ldl5z6jT9/KQuvZcQlxZIMQoFFQPvEXx9T/Bw=
github.com/minio/madmin-go/v3 v3.0.6 h1:rlU0UCwRhi/bI5R9Pg5df88ddqFNFA5mpmxScAanQCA=
github.com/minio/madmin-go/v3 v3.0.6/go.mod h1:lPrMoc1aeiIWmmrxBthkDqzMPQwC/Lu9ByuyM2wenJk=
github.com/minio/madmin-go/v3 v3.0.7 h1:nuRwrqarFrkzbUiA36H/HKAcuNr8J9TjKlWRlua7lNo=
github.com/minio/madmin-go/v3 v3.0.7/go.mod h1:lPrMoc1aeiIWmmrxBthkDqzMPQwC/Lu9ByuyM2wenJk=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.57 h1:xsFiOiWjpC1XAGbFEUOzj1/gMXGz7ljfxifwcb/5YXU=
Expand Down

0 comments on commit 48dd257

Please sign in to comment.