github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/top-api-spinner.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 "fmt" 22 "math" 23 "sort" 24 "strings" 25 "sync/atomic" 26 "time" 27 28 "github.com/charmbracelet/bubbles/spinner" 29 tea "github.com/charmbracelet/bubbletea" 30 "github.com/charmbracelet/lipgloss" 31 "github.com/dustin/go-humanize" 32 "github.com/minio/madmin-go/v3" 33 "github.com/olekukonko/tablewriter" 34 ) 35 36 type topAPIStats struct { 37 TotalCalls uint64 38 TotalBytesRX uint64 39 TotalBytesTX uint64 40 TotalErrors uint64 41 TotalDurationNanos uint64 42 MaxDurationNanos uint64 43 MinDurationNanos uint64 44 } 45 46 func (s *topAPIStats) addAPICall(n int) { 47 atomic.AddUint64(&s.TotalCalls, uint64(n)) 48 } 49 50 func (s *topAPIStats) addAPIBytesRX(n int) { 51 atomic.AddUint64(&s.TotalBytesRX, uint64(n)) 52 } 53 54 func (s *topAPIStats) addAPIBytesTX(n int) { 55 atomic.AddUint64(&s.TotalBytesTX, uint64(n)) 56 } 57 58 func (s *topAPIStats) addAPIErrors(n int) { 59 atomic.AddUint64(&s.TotalErrors, uint64(n)) 60 } 61 62 func (s *topAPIStats) addAPIDurationNanos(n int64) { 63 atomic.AddUint64(&s.TotalDurationNanos, uint64(n)) 64 if s.MinDurationNanos == 0 { 65 s.MinDurationNanos = uint64(n) 66 } 67 s.MinDurationNanos = uint64(math.Min(float64(n), float64(s.MinDurationNanos))) 68 s.MaxDurationNanos = uint64(math.Max(float64(n), float64(s.MaxDurationNanos))) 69 } 70 71 func (s *topAPIStats) loadAPICall() uint64 { 72 return atomic.LoadUint64(&s.TotalCalls) 73 } 74 75 func (s *topAPIStats) loadAPIBytesRX() uint64 { 76 return atomic.LoadUint64(&s.TotalBytesRX) 77 } 78 79 func (s *topAPIStats) loadAPIBytesTX() uint64 { 80 return atomic.LoadUint64(&s.TotalBytesTX) 81 } 82 83 func (s *topAPIStats) loadAPIErrors() uint64 { 84 return atomic.LoadUint64(&s.TotalErrors) 85 } 86 87 func (s *topAPIStats) loadAPIDurationNanos() uint64 { 88 return atomic.LoadUint64(&s.TotalDurationNanos) 89 } 90 91 func (s *topAPIStats) loadAPIMinDurationNanos() uint64 { 92 return atomic.LoadUint64(&s.MinDurationNanos) 93 } 94 95 func (s *topAPIStats) loadAPIMaxDurationNanos() uint64 { 96 return atomic.LoadUint64(&s.MaxDurationNanos) 97 } 98 99 type traceUI struct { 100 spinner spinner.Model 101 quitting bool 102 startTime time.Time 103 result topAPIResult 104 lastResult topAPIResult 105 apiStatsMap map[string]*topAPIStats 106 } 107 108 type topAPIResult struct { 109 final bool 110 apiCallInfo madmin.ServiceTraceInfo 111 } 112 113 func initTraceUI() *traceUI { 114 s := spinner.New() 115 s.Spinner = spinner.Points 116 s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 117 return &traceUI{ 118 spinner: s, 119 apiStatsMap: make(map[string]*topAPIStats), 120 } 121 } 122 123 func (m *traceUI) Init() tea.Cmd { 124 return m.spinner.Tick 125 } 126 127 func (m *traceUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 128 switch msg := msg.(type) { 129 case tea.KeyMsg: 130 switch msg.String() { 131 case "ctrl+c", "q", "esc": 132 m.quitting = true 133 return m, tea.Quit 134 default: 135 return m, nil 136 } 137 case topAPIResult: 138 m.result = msg 139 if m.result.apiCallInfo.Trace.FuncName != "" { 140 m.lastResult = m.result 141 } 142 if msg.final { 143 m.quitting = true 144 return m, tea.Quit 145 } 146 return m, nil 147 } 148 149 var cmd tea.Cmd 150 m.spinner, cmd = m.spinner.Update(msg) 151 return m, cmd 152 } 153 154 func (m *traceUI) View() string { 155 var s strings.Builder 156 s.WriteString("\n") 157 158 // Set table header 159 table := tablewriter.NewWriter(&s) 160 table.SetAutoWrapText(false) 161 table.SetAutoFormatHeaders(true) 162 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 163 table.SetAlignment(tablewriter.ALIGN_LEFT) 164 table.SetCenterSeparator("") 165 table.SetColumnSeparator("") 166 table.SetRowSeparator("") 167 table.SetHeaderLine(false) 168 table.SetBorder(false) 169 table.SetTablePadding("\t") // pad with tabs 170 table.SetNoWhiteSpace(true) 171 172 res := m.result.apiCallInfo 173 if m.startTime.IsZero() && !res.Trace.Time.IsZero() { 174 m.startTime = res.Trace.Time 175 } 176 if res.Trace.FuncName != "" && res.Trace.FuncName != "errorResponseHandler" { 177 traceSt, ok := m.apiStatsMap[res.Trace.FuncName] 178 if !ok { 179 traceSt = &topAPIStats{} 180 } 181 traceSt.addAPICall(1) 182 if res.Trace.HTTP != nil { 183 traceSt.addAPIBytesRX(res.Trace.HTTP.CallStats.InputBytes) 184 traceSt.addAPIBytesTX(res.Trace.HTTP.CallStats.OutputBytes) 185 traceSt.addAPIDurationNanos(res.Trace.Duration.Nanoseconds()) 186 } 187 if res.Trace.HTTP.RespInfo.StatusCode >= 499 { 188 traceSt.addAPIErrors(1) 189 } 190 m.apiStatsMap[res.Trace.FuncName] = traceSt 191 } 192 193 table.SetHeader([]string{"API", "RX", "TX", "CALLS", "ERRORS", "MinRT", "MaxRT", "AvgRT", "AvgTP"}) 194 data := make([][]string, 0, len(m.apiStatsMap)) 195 196 for k, stats := range m.apiStatsMap { 197 secs := time.Duration(stats.loadAPIDurationNanos()).Seconds() 198 bytes := float64(stats.loadAPIBytesRX() + stats.loadAPIBytesTX()) 199 data = append(data, []string{ 200 k, 201 whiteStyle.Render(humanize.IBytes(stats.loadAPIBytesRX())), 202 whiteStyle.Render(humanize.IBytes(stats.loadAPIBytesTX())), 203 whiteStyle.Render(fmt.Sprintf("%d", stats.loadAPICall())), 204 whiteStyle.Render(fmt.Sprintf("%d", stats.loadAPIErrors())), 205 whiteStyle.Render(fmt.Sprintf("%.03f s", time.Duration(stats.loadAPIMinDurationNanos()).Seconds())), 206 whiteStyle.Render(fmt.Sprintf("%.03f s", time.Duration(stats.loadAPIMaxDurationNanos()).Seconds())), 207 whiteStyle.Render(fmt.Sprintf("%.03f s", time.Duration(stats.loadAPIDurationNanos()/stats.loadAPICall()).Seconds())), 208 whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(uint64(bytes/secs)))), 209 }) 210 } 211 sort.Slice(data, func(i, j int) bool { 212 return data[i][0] < data[j][0] 213 }) 214 215 table.AppendBulk(data) 216 table.Render() 217 218 if !m.quitting { 219 s.WriteString(fmt.Sprintf("\nTopAPI: %s", m.spinner.View())) 220 } else { 221 var totalTX, totalRX, totalCalls uint64 222 lastReqTime := m.lastResult.apiCallInfo.Trace.Time 223 if m.lastResult.apiCallInfo.Trace.Time.IsZero() { 224 lastReqTime = time.Now() 225 } 226 for _, stats := range m.apiStatsMap { 227 totalRX += stats.loadAPIBytesRX() 228 totalTX += stats.loadAPIBytesTX() 229 totalCalls += stats.loadAPICall() 230 } 231 232 msg := fmt.Sprintf("\nSummary:\n\nTotal: %d CALLS, %s RX, %s TX", 233 totalCalls, 234 humanize.IBytes(totalRX), 235 humanize.IBytes(totalTX), 236 ) 237 if !m.startTime.IsZero() { 238 msg += fmt.Sprintf(" - in %.02fs", lastReqTime.Sub(m.startTime).Seconds()) 239 } 240 241 s.WriteString(msg) 242 s.WriteString("\n") 243 } 244 return s.String() 245 }