Skip to content

Commit f2d65b6

Browse files
authored
scanner-status: Support both old & new scanner metrics (minio#4683)
When the scanner uses parallel erasure sets scanning, the cycle concept for the whole scan is no longer relevant. This PR will show the number of ongoing bucket scans. ``` mc admin scanner status --bucket <bucket-name> ALIAS ``` Will print a detailed report of a particular bucket scan. Like, when the last scan progress was saved for that bucket for all erasure sets. Also it will print when a full scan of a bucket finished. (A full bucket scan means the last time frame that was needed for each of erasure sets to do sixteen cycles)
1 parent 3092b6b commit f2d65b6

File tree

1 file changed

+135
-30
lines changed

1 file changed

+135
-30
lines changed

cmd/admin-scanner-status.go

Lines changed: 135 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ import (
2626
"io"
2727
"os"
2828
"sort"
29+
"strconv"
2930
"strings"
3031
"time"
3132

3233
"github.com/charmbracelet/bubbles/spinner"
3334
tea "github.com/charmbracelet/bubbletea"
3435
"github.com/charmbracelet/lipgloss"
36+
humanize "github.com/dustin/go-humanize"
3537
"github.com/fatih/color"
3638
"github.com/minio/cli"
3739
json "github.com/minio/colorjson"
@@ -66,6 +68,10 @@ var adminScannerInfoFlags = []cli.Flag{
6668
Hidden: true,
6769
Usage: "read previously saved json from file and replay",
6870
},
71+
cli.StringFlag{
72+
Name: "bucket",
73+
Usage: "show scan stats about a given bucket",
74+
},
6975
}
7076

7177
var adminScannerInfo = cli.Command{
@@ -103,11 +109,108 @@ func checkAdminScannerInfoSyntax(ctx *cli.Context) {
103109
}
104110
}
105111

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+
106195
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+
107199
checkAdminScannerInfoSyntax(ctx)
108200

109201
aliasedURL := ctx.Args().Get(0)
110202

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+
111214
ui := tea.NewProgram(initScannerMetricsUI(ctx.Int("max-paths")))
112215
ctxt, cancel := context.WithCancel(globalContext)
113216
defer cancel()
@@ -147,10 +250,6 @@ func mainAdminScannerInfo(ctx *cli.Context) error {
147250
os.Exit(0)
148251
}
149252

150-
// Create a new MinIO Admin Client
151-
client, err := newAdminClient(aliasedURL)
152-
fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.")
153-
154253
opts := madmin.MetricsOptions{
155254
Type: madmin.MetricsScanner,
156255
N: ctx.Int("n"),
@@ -305,37 +404,43 @@ func (m *scannerMetricsUI) View() string {
305404

306405
title := metricsTitle
307406
ui := metricsUint64
308-
const wantCycles = 16
309407
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)
312411
} 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)")
324415
} 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)")
330439
}
331440
}
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+
339444
getRate := func(x madmin.TimedAction) string {
340445
if x.AccTime > 0 {
341446
return fmt.Sprintf("; Rate: %v/day", ui(uint64(float64(24*time.Hour)/(float64(time.Minute)/float64(x.Count)))))

0 commit comments

Comments
 (0)