github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/batch-status.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/charmbracelet/bubbles/spinner" 11 tea "github.com/charmbracelet/bubbletea" 12 "github.com/charmbracelet/lipgloss" 13 "github.com/dustin/go-humanize" 14 "github.com/minio/cli" 15 "github.com/minio/madmin-go/v3" 16 "github.com/minio/mc/pkg/probe" 17 "github.com/minio/pkg/v2/console" 18 "github.com/olekukonko/tablewriter" 19 ) 20 21 var batchStatusCmd = cli.Command{ 22 Name: "status", 23 Usage: "summarize job events on MinIO server in real-time", 24 Action: mainBatchStatus, 25 OnUsageError: onUsageError, 26 Before: setGlobalsFromContext, 27 Flags: globalFlags, 28 HideHelpCommand: true, 29 CustomHelpTemplate: `NAME: 30 {{.HelpName}} - {{.Usage}} 31 32 USAGE: 33 {{.HelpName}} TARGET JOBID 34 35 FLAGS: 36 {{range .VisibleFlags}}{{.}} 37 {{end}} 38 EXAMPLES: 39 1. Display current in-progress JOB events. 40 {{.Prompt}} {{.HelpName}} myminio/ KwSysDpxcBU9FNhGkn2dCf 41 `, 42 } 43 44 // checkBatchStatusSyntax - validate all the passed arguments 45 func checkBatchStatusSyntax(ctx *cli.Context) { 46 if len(ctx.Args()) != 2 { 47 showCommandHelpAndExit(ctx, 1) // last argument is exit code 48 } 49 } 50 51 func mainBatchStatus(ctx *cli.Context) error { 52 checkBatchStatusSyntax(ctx) 53 54 aliasedURL := ctx.Args().Get(0) 55 jobID := ctx.Args().Get(1) 56 57 // Create a new MinIO Admin Client 58 client, err := newAdminClient(aliasedURL) 59 fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") 60 61 ctxt, cancel := context.WithCancel(globalContext) 62 defer cancel() 63 64 _, e := client.DescribeBatchJob(ctxt, jobID) 65 nosuchJob := madmin.ToErrorResponse(e).Code == "XMinioAdminNoSuchJob" 66 if nosuchJob { 67 e = nil 68 if !globalJSON { 69 console.Infoln("Unable to find an active job, attempting to list from previously run jobs") 70 } 71 } 72 fatalIf(probe.NewError(e), "Unable to lookup job status") 73 74 ui := tea.NewProgram(initBatchJobMetricsUI(jobID)) 75 go func() { 76 opts := madmin.MetricsOptions{ 77 Type: madmin.MetricsBatchJobs, 78 ByJobID: jobID, 79 Interval: time.Second, 80 } 81 e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { 82 if globalJSON { 83 if metrics.Aggregated.BatchJobs == nil { 84 cancel() 85 return 86 } 87 88 job, ok := metrics.Aggregated.BatchJobs.Jobs[jobID] 89 if !ok { 90 cancel() 91 return 92 } 93 94 printMsg(metricsMessage{RealtimeMetrics: metrics}) 95 if job.Complete || job.Failed { 96 cancel() 97 return 98 } 99 } else { 100 ui.Send(metrics) 101 } 102 }) 103 if e != nil && !errors.Is(e, context.Canceled) { 104 fatalIf(probe.NewError(e).Trace(ctx.Args()...), "Unable to get current batch status") 105 } 106 }() 107 108 if !globalJSON { 109 if _, e := ui.Run(); e != nil { 110 cancel() 111 fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get current batch status") 112 } 113 } else { 114 <-ctxt.Done() 115 } 116 117 return nil 118 } 119 120 func initBatchJobMetricsUI(jobID string) *batchJobMetricsUI { 121 s := spinner.New() 122 s.Spinner = spinner.Points 123 s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 124 return &batchJobMetricsUI{ 125 spinner: s, 126 jobID: jobID, 127 } 128 } 129 130 type batchJobMetricsUI struct { 131 current madmin.JobMetric 132 spinner spinner.Model 133 quitting bool 134 jobID string 135 } 136 137 func (m *batchJobMetricsUI) Init() tea.Cmd { 138 return m.spinner.Tick 139 } 140 141 func (m *batchJobMetricsUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 142 switch msg := msg.(type) { 143 case tea.KeyMsg: 144 switch msg.String() { 145 case "ctrl+c": 146 m.quitting = true 147 return m, tea.Quit 148 default: 149 return m, nil 150 } 151 case madmin.RealtimeMetrics: 152 metrics := msg 153 if metrics.Aggregated.BatchJobs == nil { 154 m.quitting = true 155 return m, tea.Quit 156 } 157 158 job, ok := metrics.Aggregated.BatchJobs.Jobs[m.jobID] 159 if !ok { 160 m.quitting = true 161 return m, tea.Quit 162 } 163 164 m.current = job 165 if job.Complete || job.Failed { 166 m.quitting = true 167 return m, tea.Quit 168 } 169 return m, nil 170 case spinner.TickMsg: 171 var cmd tea.Cmd 172 m.spinner, cmd = m.spinner.Update(msg) 173 return m, cmd 174 default: 175 return m, nil 176 } 177 } 178 179 func (m *batchJobMetricsUI) View() string { 180 var s strings.Builder 181 182 // Set table header 183 table := tablewriter.NewWriter(&s) 184 table.SetAutoWrapText(false) 185 table.SetAutoFormatHeaders(true) 186 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 187 table.SetAlignment(tablewriter.ALIGN_LEFT) 188 table.SetCenterSeparator("") 189 table.SetColumnSeparator("") 190 table.SetRowSeparator("") 191 table.SetHeaderLine(false) 192 table.SetBorder(false) 193 table.SetTablePadding("\t") // pad with tabs 194 table.SetNoWhiteSpace(true) 195 196 var data [][]string 197 addLine := func(prefix string, value interface{}) { 198 data = append(data, []string{ 199 prefix, 200 whiteStyle.Render(fmt.Sprint(value)), 201 }) 202 } 203 204 if !m.quitting { 205 s.WriteString(m.spinner.View()) 206 } else { 207 if m.current.Complete { 208 s.WriteString(m.spinner.Style.Render((tickCell + tickCell + tickCell))) 209 } else if m.current.Failed { 210 s.WriteString(m.spinner.Style.Render((crossTickCell + crossTickCell + crossTickCell))) 211 } 212 } 213 s.WriteString("\n") 214 215 switch m.current.JobType { 216 case string(madmin.BatchJobReplicate): 217 accElapsedTime := m.current.LastUpdate.Sub(m.current.StartTime) 218 219 addLine("JobType: ", m.current.JobType) 220 addLine("Objects: ", m.current.Replicate.Objects) 221 addLine("Versions: ", m.current.Replicate.Objects) 222 addLine("FailedObjects: ", m.current.Replicate.ObjectsFailed) 223 if accElapsedTime > 0 { 224 bytesTransferredPerSec := float64(m.current.Replicate.BytesTransferred) / accElapsedTime.Seconds() 225 objectsPerSec := float64(int64(time.Second)*m.current.Replicate.Objects) / float64(accElapsedTime) 226 addLine("Throughput: ", fmt.Sprintf("%s/s", humanize.IBytes(uint64(bytesTransferredPerSec)))) 227 addLine("IOPs: ", fmt.Sprintf("%.2f objs/s", objectsPerSec)) 228 } 229 addLine("Transferred: ", humanize.IBytes(uint64(m.current.Replicate.BytesTransferred))) 230 addLine("Elapsed: ", accElapsedTime.String()) 231 addLine("CurrObjName: ", m.current.Replicate.Object) 232 case string(madmin.BatchJobExpire): 233 addLine("JobType: ", m.current.JobType) 234 addLine("Objects: ", m.current.Expired.Objects) 235 addLine("FailedObjects: ", m.current.Expired.ObjectsFailed) 236 addLine("CurrObjName: ", m.current.Expired.Object) 237 238 if !m.current.LastUpdate.IsZero() { 239 accElapsedTime := m.current.LastUpdate.Sub(m.current.StartTime) 240 addLine("Elapsed: ", accElapsedTime.String()) 241 } 242 243 } 244 245 table.AppendBulk(data) 246 table.Render() 247 248 if m.quitting { 249 s.WriteString("\n") 250 } 251 return s.String() 252 }