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  }