@@ -26,12 +26,14 @@ import (
26
26
"io"
27
27
"os"
28
28
"sort"
29
+ "strconv"
29
30
"strings"
30
31
"time"
31
32
32
33
"github.com/charmbracelet/bubbles/spinner"
33
34
tea "github.com/charmbracelet/bubbletea"
34
35
"github.com/charmbracelet/lipgloss"
36
+ humanize "github.com/dustin/go-humanize"
35
37
"github.com/fatih/color"
36
38
"github.com/minio/cli"
37
39
json "github.com/minio/colorjson"
@@ -66,6 +68,10 @@ var adminScannerInfoFlags = []cli.Flag{
66
68
Hidden : true ,
67
69
Usage : "read previously saved json from file and replay" ,
68
70
},
71
+ cli.StringFlag {
72
+ Name : "bucket" ,
73
+ Usage : "show scan stats about a given bucket" ,
74
+ },
69
75
}
70
76
71
77
var adminScannerInfo = cli.Command {
@@ -103,11 +109,108 @@ func checkAdminScannerInfoSyntax(ctx *cli.Context) {
103
109
}
104
110
}
105
111
112
+ // bucketScanMsg container for content message structure
113
+ type bucketScanMsg struct {
114
+ Status string `json:"status"`
115
+ Stats []madmin.BucketScanInfo `json:"stats"`
116
+ }
117
+
118
+ func (b bucketScanMsg ) String () string {
119
+ var sb strings.Builder
120
+ sb .WriteString ("\n " )
121
+
122
+ sort .Slice (b .Stats , func (i , j int ) bool { return b .Stats [i ].LastUpdate .Before (b .Stats [j ].LastUpdate ) })
123
+
124
+ pt := newPrettyTable (" | " ,
125
+ Field {"Pool" , 5 },
126
+ Field {"Set" , 5 },
127
+ Field {"LastUpdate" , timeFieldMaxLen },
128
+ )
129
+
130
+ sb .WriteString (console .Colorize ("Headers" , pt .buildRow ("Pool" , "Set" , "Last Update" )) + "\n " )
131
+
132
+ now := time .Now ().UTC ()
133
+ for i := range b .Stats {
134
+ sb .WriteString (
135
+ pt .buildRow (
136
+ strconv .Itoa (b .Stats [i ].Pool + 1 ),
137
+ strconv .Itoa (b .Stats [i ].Set + 1 ),
138
+ humanize .RelTime (now , b .Stats [i ].LastUpdate , "" , "ago" ),
139
+ ) + "\n " )
140
+ }
141
+
142
+ var (
143
+ earliestESScan time.Time // the earliest ES that completed a bucket scan
144
+ latestESScan time.Time // the last ES that completed a bucket scan
145
+ fullScan = true
146
+ )
147
+
148
+ // Look for a bucket full scan inforation only if all
149
+ // erasure sets completed at least 16 cycles
150
+ for _ , st := range b .Stats {
151
+ if len (st .Completed ) < 16 {
152
+ fullScan = false
153
+ break
154
+ }
155
+ if earliestESScan .IsZero () {
156
+ // First stats
157
+ earliestESScan = st .Completed [0 ]
158
+ latestESScan = st .Completed [len (st .Completed )- 1 ]
159
+ continue
160
+ }
161
+ if earliestESScan .Before (st .Completed [0 ]) {
162
+ earliestESScan = st .Completed [0 ]
163
+ }
164
+ if latestESScan .After (st .Completed [len (st .Completed )- 1 ]) {
165
+ latestESScan = st .Completed [len (st .Completed )- 1 ]
166
+ }
167
+ }
168
+
169
+ sb .WriteString ("\n " )
170
+
171
+ if fullScan {
172
+ took := latestESScan .Sub (earliestESScan )
173
+ sb .WriteString (
174
+ fmt .Sprintf (
175
+ "%s %s (took %s)\n " ,
176
+ console .Colorize ("FullScan" , "Full bucket scan: " ),
177
+ humanize .RelTime (now , latestESScan , "" , "ago" ),
178
+ fmt .Sprintf ("%dd%dh%dm" , int (took .Hours ()/ 24 ), int (took .Hours ())% 24 , int (took .Minutes ())% 60 )),
179
+ )
180
+ }
181
+
182
+ sb .WriteString ("\n " )
183
+
184
+ return sb .String ()
185
+ }
186
+
187
+ func (b bucketScanMsg ) JSON () string {
188
+ b .Status = "success"
189
+ jsonMessageBytes , e := json .MarshalIndent (b , "" , " " )
190
+ fatalIf (probe .NewError (e ), "Unable to marshal into JSON." )
191
+
192
+ return string (jsonMessageBytes )
193
+ }
194
+
106
195
func mainAdminScannerInfo (ctx * cli.Context ) error {
196
+ console .SetColor ("Headers" , color .New (color .Bold , color .FgHiGreen ))
197
+ console .SetColor ("FullScan" , color .New (color .Bold , color .FgHiGreen ))
198
+
107
199
checkAdminScannerInfoSyntax (ctx )
108
200
109
201
aliasedURL := ctx .Args ().Get (0 )
110
202
203
+ // Create a new MinIO Admin Client
204
+ client , err := newAdminClient (aliasedURL )
205
+ fatalIf (err .Trace (aliasedURL ), "Unable to initialize admin client." )
206
+
207
+ if bucket := ctx .String ("bucket" ); bucket != "" {
208
+ bucketStats , err := client .BucketScanInfo (globalContext , bucket )
209
+ fatalIf (probe .NewError (err ).Trace (aliasedURL ), "Unable to get bucket stats." )
210
+ printMsg (bucketScanMsg {Stats : bucketStats })
211
+ return nil
212
+ }
213
+
111
214
ui := tea .NewProgram (initScannerMetricsUI (ctx .Int ("max-paths" )))
112
215
ctxt , cancel := context .WithCancel (globalContext )
113
216
defer cancel ()
@@ -147,10 +250,6 @@ func mainAdminScannerInfo(ctx *cli.Context) error {
147
250
os .Exit (0 )
148
251
}
149
252
150
- // Create a new MinIO Admin Client
151
- client , err := newAdminClient (aliasedURL )
152
- fatalIf (err .Trace (aliasedURL ), "Unable to initialize admin client." )
153
-
154
253
opts := madmin.MetricsOptions {
155
254
Type : madmin .MetricsScanner ,
156
255
N : ctx .Int ("n" ),
@@ -305,37 +404,43 @@ func (m *scannerMetricsUI) View() string {
305
404
306
405
title := metricsTitle
307
406
ui := metricsUint64
308
- const wantCycles = 16
309
407
addRow ("" )
310
- if len (sc .CyclesCompletedAt ) < 2 {
311
- addRow ("Last full scan time: Unknown (not enough data)" )
408
+
409
+ if sc .CurrentCycle == 0 && sc .CurrentStarted .IsZero () && sc .CyclesCompletedAt == nil {
410
+ addRowF (" " + title ("Scanning:" )+ " %d bucket(s)" , sc .OngoingBuckets )
312
411
} else {
313
- addRow ("Overall Statistics" )
314
- addRow ("------------------" )
315
- sort .Slice (sc .CyclesCompletedAt , func (i , j int ) bool {
316
- return sc .CyclesCompletedAt [i ].After (sc .CyclesCompletedAt [j ])
317
- })
318
- if len (sc .CyclesCompletedAt ) >= wantCycles {
319
- sinceLast := sc .CyclesCompletedAt [0 ].Sub (sc .CyclesCompletedAt [wantCycles - 1 ])
320
- perMonth := float64 (30 * 24 * time .Hour ) / float64 (sinceLast )
321
- cycleTime := console .Colorize ("metrics-number" , fmt .Sprintf ("%dd%dh%dm" , int (sinceLast .Hours ()/ 24 ), int (sinceLast .Hours ())% 24 , int (sinceLast .Minutes ())% 60 ))
322
- perms := console .Colorize ("metrics-number" , fmt .Sprintf ("%.02f" , perMonth ))
323
- addRowF (title ("Last full scan time:" )+ " %s; Estimated %s/month" , cycleTime , perms )
412
+ const wantCycles = 16
413
+ if len (sc .CyclesCompletedAt ) < 2 {
414
+ addRow ("Last full scan time: Unknown (not enough data)" )
324
415
} else {
325
- sinceLast := sc .CyclesCompletedAt [0 ].Sub (sc .CyclesCompletedAt [1 ]) * time .Duration (wantCycles )
326
- perMonth := float64 (30 * 24 * time .Hour ) / float64 (sinceLast )
327
- cycleTime := console .Colorize ("metrics-number" , fmt .Sprintf ("%dd%dh%dm" , int (sinceLast .Hours ()/ 24 ), int (sinceLast .Hours ())% 24 , int (sinceLast .Minutes ())% 60 ))
328
- perms := console .Colorize ("metrics-number" , fmt .Sprintf ("%.02f" , perMonth ))
329
- addRowF (title ("Est. full scan time:" )+ " %s; Estimated %s/month" , cycleTime , perms )
416
+ addRow ("Overall Statistics" )
417
+ addRow ("------------------" )
418
+ sort .Slice (sc .CyclesCompletedAt , func (i , j int ) bool {
419
+ return sc .CyclesCompletedAt [i ].After (sc .CyclesCompletedAt [j ])
420
+ })
421
+ if len (sc .CyclesCompletedAt ) >= wantCycles {
422
+ sinceLast := sc .CyclesCompletedAt [0 ].Sub (sc .CyclesCompletedAt [wantCycles - 1 ])
423
+ perMonth := float64 (30 * 24 * time .Hour ) / float64 (sinceLast )
424
+ cycleTime := console .Colorize ("metrics-number" , fmt .Sprintf ("%dd%dh%dm" , int (sinceLast .Hours ()/ 24 ), int (sinceLast .Hours ())% 24 , int (sinceLast .Minutes ())% 60 ))
425
+ perms := console .Colorize ("metrics-number" , fmt .Sprintf ("%.02f" , perMonth ))
426
+ addRowF (title ("Last full scan time:" )+ " %s; Estimated %s/month" , cycleTime , perms )
427
+ } else {
428
+ sinceLast := sc .CyclesCompletedAt [0 ].Sub (sc .CyclesCompletedAt [1 ]) * time .Duration (wantCycles )
429
+ perMonth := float64 (30 * 24 * time .Hour ) / float64 (sinceLast )
430
+ cycleTime := console .Colorize ("metrics-number" , fmt .Sprintf ("%dd%dh%dm" , int (sinceLast .Hours ()/ 24 ), int (sinceLast .Hours ())% 24 , int (sinceLast .Minutes ())% 60 ))
431
+ perms := console .Colorize ("metrics-number" , fmt .Sprintf ("%.02f" , perMonth ))
432
+ addRowF (title ("Est. full scan time:" )+ " %s; Estimated %s/month" , cycleTime , perms )
433
+ }
434
+ }
435
+ if sc .CurrentCycle > 0 {
436
+ addRowF (title ("Current cycle:" )+ " %s; Started: %v" , ui (sc .CurrentCycle ), console .Colorize ("metrics-date" , sc .CurrentStarted ))
437
+ } else {
438
+ addRowF (title ("Current cycle:" ) + " (between cycles)" )
330
439
}
331
440
}
332
- if sc .CurrentCycle > 0 {
333
- addRowF (title ("Current cycle:" )+ " %s; Started: %v" , ui (sc .CurrentCycle ), console .Colorize ("metrics-date" , sc .CurrentStarted ))
334
- addRowF (title ("Active drives:" )+ " %s" , ui (uint64 (len (sc .ActivePaths ))))
335
- } else {
336
- addRowF (title ("Current cycle:" ) + " (between cycles)" )
337
- addRowF (title ("Active drives:" )+ " %s" , ui (uint64 (len (sc .ActivePaths ))))
338
- }
441
+
442
+ addRowF (title ("Active drives:" )+ " %s" , ui (uint64 (len (sc .ActivePaths ))))
443
+
339
444
getRate := func (x madmin.TimedAction ) string {
340
445
if x .AccTime > 0 {
341
446
return fmt .Sprintf ("; Rate: %v/day" , ui (uint64 (float64 (24 * time .Hour )/ (float64 (time .Minute )/ float64 (x .Count )))))
0 commit comments