4
"errors"
5
"fmt"
6
"io"
7
+ "strings"
8
"text/tabwriter"
9
"time"
10
+ "unicode/utf8"
11
12
humanize "github.com/dustin/go-humanize"
11
- "github.com/ipfs/boxo/provider"
13
+ boxoprovider "github.com/ipfs/boxo/provider"
14
cmds "github.com/ipfs/go-ipfs-cmds"
15
"github.com/ipfs/kubo/core/commands/cmdenv"
16
"github.com/libp2p/go-libp2p-kad-dht/fullrt"
17
+ "github.com/libp2p/go-libp2p-kad-dht/provider"
18
+ "github.com/libp2p/go-libp2p-kad-dht/provider/buffered"
19
+ "github.com/libp2p/go-libp2p-kad-dht/provider/dual"
20
+ "github.com/libp2p/go-libp2p-kad-dht/provider/stats"
21
+ "github.com/probe-lab/go-libdht/kad/key"
22
"golang.org/x/exp/constraints"
23
)
24
25
const (
26
provideQuietOptionName = "quiet"
27
+ provideLanOptionName = "lan"
28
+
29
+ provideStatAllOptionName = "all"
30
+ provideStatCompactOptionName = "compact"
31
+ provideStatNetworkOptionName = "network"
32
+ provideStatConnectivityOptionName = "connectivity"
33
+ provideStatOperationsOptionName = "operations"
34
+ provideStatTimingsOptionName = "timings"
35
+ provideStatScheduleOptionName = "schedule"
36
+ provideStatQueuesOptionName = "queues"
37
+ provideStatWorkersOptionName = "workers"
38
+
39
+ // lowWorkerThreshold is the threshold below which worker availability warnings are shown
40
+ lowWorkerThreshold = 2
41
)
42
43
var ProvideCmd = &cmds.Command{
44
Status: cmds.Experimental,
45
Helptext: cmds.HelpText{
25
- Tagline: "Control providing operations",
46
+ Tagline: "Control and monitor content providing",
47
ShortDescription: `
48
Control providing operations.
49
29
-NOTE: This command is experimental and not all provide-related commands have
30
-been migrated to this namespace yet. For example, 'ipfs routing
31
-provide|reprovide' are still under the routing namespace, 'ipfs stats
32
-reprovide' provides statistics. Additionally, 'ipfs bitswap reprovide' and
33
-'ipfs stats provide' are deprecated.
50
+OVERVIEW:
51
+
52
+The provider system advertises content by publishing provider records,
53
+allowing other nodes to discover which peers have specific content.
54
+Content is reprovided periodically (every Provide.DHT.Interval)
55
+according to Provide.Strategy.
56
+
57
+CONFIGURATION:
58
+
59
+Learn more: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide
60
+
61
+SEE ALSO:
62
+
63
+For ad-hoc one-time provide, see 'ipfs routing provide'
64
`,
65
},
66
77
ShortDescription: `
78
Clear all CIDs pending to be provided for the first time.
79
50
-Note: Kubo will automatically clear the queue when it detects a change of
51
-Provide.Strategy upon a restart. For more information about provide
52
-strategies, see:
53
-https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy
80
+BEHAVIOR:
81
+
82
+This command removes CIDs from the provide queue that are waiting to be
83
+advertised to the DHT for the first time. It does not affect content that
84
+is already being reprovided on schedule.
85
+
86
+AUTOMATIC CLEARING:
87
+
88
+Kubo will automatically clear the queue when it detects a change of
89
+Provide.Strategy upon a restart.
90
+
91
+Learn: https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy
92
`,
93
},
94
Options: []cmds.Option{
128
}
129
130
type provideStats struct {
93
- provider.ReproviderStats
94
- fullRT bool
131
+ Sweep *stats.Stats
132
+ Legacy *boxoprovider.ReproviderStats
133
+ FullRT bool // only used for legacy stats
134
+}
135
+
136
+// extractSweepingProvider extracts a SweepingProvider from the given provider interface.
137
+// It handles unwrapping buffered and dual providers, selecting LAN or WAN as specified.
138
+// Returns nil if the provider is not a sweeping provider type.
139
+func extractSweepingProvider(prov any, useLAN bool) *provider.SweepingProvider {
140
+ switch p := prov.(type) {
141
+ case *provider.SweepingProvider:
142
+ return p
143
+ case *dual.SweepingProvider:
144
+ if useLAN {
145
+ return p.LAN
146
+ }
147
+ return p.WAN
148
+ case *buffered.SweepingProvider:
149
+ // Recursively extract from the inner provider
150
+ return extractSweepingProvider(p.Provider, useLAN)
151
+ default:
152
+ return nil
153
+ }
154
}
155
156
var provideStatCmd = &cmds.Command{
157
Status: cmds.Experimental,
158
Helptext: cmds.HelpText{
100
- Tagline: "Returns statistics about the node's provider system.",
159
+ Tagline: "Show statistics about the provider system",
160
ShortDescription: `
102
-Returns statistics about the content the node is reproviding every
103
-Provide.DHT.Interval according to Provide.Strategy:
104
-https://github.com/ipfs/kubo/blob/master/docs/config.md#provide
161
+Returns statistics about the node's provider system.
162
+
163
+OVERVIEW:
164
+
165
+The provide system advertises content to the DHT by publishing provider
166
+records that map CIDs to your peer ID. These records expire after a fixed
167
+TTL to account for node churn, so content must be reprovided periodically
168
+to stay discoverable.
169
+
170
+Two provider types exist:
171
+
172
+- Sweep provider: Divides the DHT keyspace into regions and systematically
173
+ sweeps through them over the reprovide interval. Batches CIDs allocated
174
+ to the same DHT servers, reducing lookups from N (one per CID) to a
175
+ small static number based on DHT size (~3k for 10k DHT servers). Spreads
176
+ work evenly over time to prevent resource spikes and ensure announcements
177
+ happen just before records expire.
178
+
179
+- Legacy provider: Processes each CID individually with separate DHT
180
+ lookups. Attempts to reprovide all content as quickly as possible at the
181
+ start of each cycle. Works well for small datasets but struggles with
182
+ large collections.
183
+
184
+Learn more:
185
+- Config: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide
186
+- Metrics: https://github.com/ipfs/kubo/blob/master/docs/provide-stats.md
187
+
188
+DEFAULT OUTPUT:
189
+
190
+Shows a brief summary including queue sizes, scheduled items, average record
191
+holders, ongoing/total provides, and worker warnings.
192
+
193
+DETAILED OUTPUT:
194
+
195
+Use --all for detailed statistics with these sections: connectivity, queues,
196
+schedule, timings, network, operations, and workers. Individual sections can
197
+be displayed with their flags (e.g., --network, --operations). Multiple flags
198
+can be combined.
199
106
-This interface is not stable and may change from release to release.
200
+Use --compact for monitoring-friendly 2-column output (requires --all).
201
202
+EXAMPLES:
203
+
204
+Monitor provider statistics in real-time with 2-column layout:
205
+
206
+ watch ipfs provide stat --all --compact
207
+
208
+Get statistics in JSON format for programmatic processing:
209
+
210
+ ipfs provide stat --enc=json | jq
211
+
212
+NOTES:
213
+
214
+- This interface is experimental and may change between releases
215
+- Legacy provider shows basic stats only (no flags supported)
216
+- "Regions" are keyspace divisions for spreading reprovide work
217
+- For Dual DHT: use --lan for LAN provider stats (default is WAN)
218
`,
219
},
220
Arguments: []cmds.Argument{},
111
- Options: []cmds.Option{},
221
+ Options: []cmds.Option{
222
+ cmds.BoolOption(provideLanOptionName, "Show stats for LAN DHT only (for Sweep+Dual DHT only)"),
223
+ cmds.BoolOption(provideStatAllOptionName, "a", "Display all provide sweep stats"),
224
+ cmds.BoolOption(provideStatCompactOptionName, "Display stats in 2-column layout (requires --all)"),
225
+ cmds.BoolOption(provideStatConnectivityOptionName, "Display DHT connectivity status"),
226
+ cmds.BoolOption(provideStatNetworkOptionName, "Display network stats (peers, reachability, region size)"),
227
+ cmds.BoolOption(provideStatScheduleOptionName, "Display reprovide schedule (CIDs/regions scheduled, next reprovide time)"),
228
+ cmds.BoolOption(provideStatTimingsOptionName, "Display timing information (uptime, cycle start, reprovide interval)"),
229
+ cmds.BoolOption(provideStatWorkersOptionName, "Display worker pool stats (active/available/queued workers)"),
230
+ cmds.BoolOption(provideStatOperationsOptionName, "Display operation stats (ongoing/past provides, rates, errors)"),
231
+ cmds.BoolOption(provideStatQueuesOptionName, "Display provide and reprovide queue sizes"),
232
+ },
233
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
234
nd, err := cmdenv.GetNode(env)
235
if err != nil {
240
return ErrNotOnline
241
}
242
122
- provideSys, ok := nd.Provider.(provider.System)
123
- if !ok {
124
- return errors.New("stats not available with experimental sweeping provider (Provide.DHT.SweepEnabled=true)")
125
- }
243
+ lanStats, _ := req.Options[provideLanOptionName].(bool)
244
127
- stats, err := provideSys.Stat()
128
- if err != nil {
129
- return err
245
+ // Handle legacy provider
246
+ if legacySys, ok := nd.Provider.(boxoprovider.System); ok {
247
+ if lanStats {
248
+ return errors.New("LAN stats only available for Sweep provider with Dual DHT")
249
+ }
250
+ stats, err := legacySys.Stat()
251
+ if err != nil {
252
+ return err
253
+ }
254
+ _, fullRT := nd.DHTClient.(*fullrt.FullRT)
255
+ return res.Emit(provideStats{Legacy: &stats, FullRT: fullRT})
256
}
131
- _, fullRT := nd.DHTClient.(*fullrt.FullRT)
257
133
- if err := res.Emit(provideStats{stats, fullRT}); err != nil {
134
- return err
258
+ // Extract sweeping provider (handles buffered and dual unwrapping)
259
+ sweepingProvider := extractSweepingProvider(nd.Provider, lanStats)
260
+ if sweepingProvider == nil {
261
+ if lanStats {
262
+ return errors.New("LAN stats only available for Sweep provider with Dual DHT")
263
+ }
264
+ return fmt.Errorf("stats not available with current routing system %T", nd.Provider)
265
}
266
137
- return nil
267
+ s := sweepingProvider.Stats()
268
+ return res.Emit(provideStats{Sweep: &s})
269
},
270
Encoders: cmds.EncoderMap{
271
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, s provideStats) error {
272
wtr := tabwriter.NewWriter(w, 1, 2, 1, ' ', 0)
273
defer wtr.Flush()
274
144
- fmt.Fprintf(wtr, "TotalReprovides:\t%s\n", humanNumber(s.TotalReprovides))
145
- fmt.Fprintf(wtr, "AvgReprovideDuration:\t%s\n", humanDuration(s.AvgReprovideDuration))
146
- fmt.Fprintf(wtr, "LastReprovideDuration:\t%s\n", humanDuration(s.LastReprovideDuration))
147
- if !s.LastRun.IsZero() {
148
- fmt.Fprintf(wtr, "LastReprovide:\t%s\n", humanTime(s.LastRun))
149
- if s.fullRT {
150
- fmt.Fprintf(wtr, "NextReprovide:\t%s\n", humanTime(s.LastRun.Add(s.ReprovideInterval)))
275
+ all, _ := req.Options[provideStatAllOptionName].(bool)
276
+ compact, _ := req.Options[provideStatCompactOptionName].(bool)
277
+ connectivity, _ := req.Options[provideStatConnectivityOptionName].(bool)
278
+ queues, _ := req.Options[provideStatQueuesOptionName].(bool)
279
+ schedule, _ := req.Options[provideStatScheduleOptionName].(bool)
280
+ network, _ := req.Options[provideStatNetworkOptionName].(bool)
281
+ timings, _ := req.Options[provideStatTimingsOptionName].(bool)
282
+ operations, _ := req.Options[provideStatOperationsOptionName].(bool)
283
+ workers, _ := req.Options[provideStatWorkersOptionName].(bool)
284
+
285
+ flagCount := 0
286
+ for _, enabled := range []bool{all, connectivity, queues, schedule, network, timings, operations, workers} {
287
+ if enabled {
288
+ flagCount++
289
+ }
290
+ }
291
+
292
+ if s.Legacy != nil {
293
+ if flagCount > 0 {
294
+ return errors.New("cannot use flags with legacy provide stats")
295
+ }
296
+ fmt.Fprintf(wtr, "TotalReprovides:\t%s\n", humanNumber(s.Legacy.TotalReprovides))
297
+ fmt.Fprintf(wtr, "AvgReprovideDuration:\t%s\n", humanDuration(s.Legacy.AvgReprovideDuration))
298
+ fmt.Fprintf(wtr, "LastReprovideDuration:\t%s\n", humanDuration(s.Legacy.LastReprovideDuration))
299
+ if !s.Legacy.LastRun.IsZero() {
300
+ fmt.Fprintf(wtr, "LastReprovide:\t%s\n", humanTime(s.Legacy.LastRun))
301
+ if s.FullRT {
302
+ fmt.Fprintf(wtr, "NextReprovide:\t%s\n", humanTime(s.Legacy.LastRun.Add(s.Legacy.ReprovideInterval)))
303
+ }
304
+ }
305
+ return nil
306
+ }
307
+
308
+ if s.Sweep == nil {
309
+ return errors.New("no provide stats available")
310
+ }
311
+
312
+ // Sweep provider stats
313
+ if s.Sweep.Closed {
314
+ fmt.Fprintf(wtr, "Provider is closed\n")
315
+ return nil
316
+ }
317
+
318
+ if compact && !all {
319
+ return errors.New("--compact requires --all flag")
320
+ }
321
+
322
+ brief := flagCount == 0
323
+ showHeadings := flagCount > 1 || all
324
+
325
+ compactMode := all && compact
326
+ var cols [2][]string
327
+ col0MaxWidth := 0
328
+ // formatLine handles both normal and compact output modes:
329
+ // - Normal mode: all lines go to cols[0], col parameter is ignored
330
+ // - Compact mode: col 0 for left column, col 1 for right column
331
+ formatLine := func(col int, format string, a ...any) {
332
+ if compactMode {
333
+ s := fmt.Sprintf(format, a...)
334
+ cols[col] = append(cols[col], s)
335
+ if col == 0 {
336
+ col0MaxWidth = max(col0MaxWidth, utf8.RuneCountInString(s))
337
+ }
338
+ return
339
+ }
340
+ format = strings.Replace(format, ": ", ":\t", 1)
341
+ format = strings.Replace(format, ", ", ",\t", 1)
342
+ cols[0] = append(cols[0], fmt.Sprintf(format, a...))
343
+ }
344
+ addBlankLine := func(col int) {
345
+ if !brief {
346
+ formatLine(col, "")
347
+ }
348
+ }
349
+ sectionTitle := func(col int, title string) {
350
+ if !brief && showHeadings {
351
+ formatLine(col, title+":")
352
+ }
353
+ }
354
+
355
+ indent := " "
356
+ if brief || !showHeadings {
357
+ indent = ""
358
+ }
359
+
360
+ // Connectivity
361
+ if all || connectivity || brief && s.Sweep.Connectivity.Status != "online" {
362
+ sectionTitle(1, "Connectivity")
363
+ since := s.Sweep.Connectivity.Since
364
+ if since.IsZero() {
365
+ formatLine(1, "%sStatus: %s", indent, s.Sweep.Connectivity.Status)
366
+ } else {
367
+ formatLine(1, "%sStatus: %s (%s)", indent, s.Sweep.Connectivity.Status, humanTime(since))
368
+ }
369
+ addBlankLine(1)
370
+ }
371
+
372
+ // Queues
373
+ if all || queues || brief {
374
+ sectionTitle(1, "Queues")
375
+ formatLine(1, "%sProvide queue: %s CIDs, %s regions", indent, humanNumber(s.Sweep.Queues.PendingKeyProvides), humanNumber(s.Sweep.Queues.PendingRegionProvides))
376
+ formatLine(1, "%sReprovide queue: %s regions", indent, humanNumber(s.Sweep.Queues.PendingRegionReprovides))
377
+ addBlankLine(1)
378
+ }
379
+
380
+ // Schedule
381
+ if all || schedule || brief {
382
+ sectionTitle(0, "Schedule")
383
+ formatLine(0, "%sCIDs scheduled: %s", indent, humanNumber(s.Sweep.Schedule.Keys))
384
+ formatLine(0, "%sRegions scheduled: %s", indent, humanNumberOrNA(s.Sweep.Schedule.Regions))
385
+ if !brief {
386
+ formatLine(0, "%sAvg prefix length: %s", indent, humanFloatOrNA(s.Sweep.Schedule.AvgPrefixLength))
387
+ nextPrefix := key.BitString(s.Sweep.Schedule.NextReprovidePrefix)
388
+ if nextPrefix == "" {
389
+ nextPrefix = "N/A"
390
+ }
391
+ formatLine(0, "%sNext region prefix: %s", indent, nextPrefix)
392
+ nextReprovideAt := s.Sweep.Schedule.NextReprovideAt.Format("15:04:05")
393
+ if s.Sweep.Schedule.NextReprovideAt.IsZero() {
394
+ nextReprovideAt = "N/A"
395
+ }
396
+ formatLine(0, "%sNext region reprovide: %s", indent, nextReprovideAt)
397
+ }
398
+ addBlankLine(0)
399
+ }
400
+
401
+ // Timings
402
+ if all || timings {
403
+ sectionTitle(1, "Timings")
404
+ formatLine(1, "%sUptime: %s (%s)", indent, humanDuration(s.Sweep.Timing.Uptime), humanTime(time.Now().Add(-s.Sweep.Timing.Uptime)))
405
+ formatLine(1, "%sCurrent time offset: %s", indent, humanDuration(s.Sweep.Timing.CurrentTimeOffset))
406
+ formatLine(1, "%sCycle started: %s", indent, humanTime(s.Sweep.Timing.CycleStart))
407
+ formatLine(1, "%sReprovide interval: %s", indent, humanDuration(s.Sweep.Timing.ReprovidesInterval))
408
+ addBlankLine(1)
409
+ }
410
+
411
+ // Network
412
+ if all || network || brief {
413
+ sectionTitle(0, "Network")
414
+ formatLine(0, "%sAvg record holders: %s", indent, humanFloatOrNA(s.Sweep.Network.AvgHolders))
415
+ if !brief {
416
+ formatLine(0, "%sPeers swept: %s", indent, humanNumber(s.Sweep.Network.Peers))
417
+ formatLine(0, "%sFull keyspace coverage: %t", indent, s.Sweep.Network.CompleteKeyspaceCoverage)
418
+ if s.Sweep.Network.Peers > 0 {
419
+ formatLine(0, "%sReachable peers: %s (%s%%)", indent, humanNumber(s.Sweep.Network.Reachable), humanNumber(100*s.Sweep.Network.Reachable/s.Sweep.Network.Peers))
420
+ } else {
421
+ formatLine(0, "%sReachable peers: %s", indent, humanNumber(s.Sweep.Network.Reachable))
422
+ }
423
+ formatLine(0, "%sAvg region size: %s", indent, humanFloatOrNA(s.Sweep.Network.AvgRegionSize))
424
+ formatLine(0, "%sReplication factor: %s", indent, humanNumber(s.Sweep.Network.ReplicationFactor))
425
+ addBlankLine(0)
426
+ }
427
+ }
428
+
429
+ // Operations
430
+ if all || operations || brief {
431
+ sectionTitle(1, "Operations")
432
+ // Ongoing operations
433
+ formatLine(1, "%sOngoing provides: %s CIDs, %s regions", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyProvides), humanNumber(s.Sweep.Operations.Ongoing.RegionProvides))
434
+ formatLine(1, "%sOngoing reprovides: %s CIDs, %s regions", indent, humanNumber(s.Sweep.Operations.Ongoing.KeyReprovides), humanNumber(s.Sweep.Operations.Ongoing.RegionReprovides))
435
+ // Past operations summary
436
+ formatLine(1, "%sTotal CIDs provided: %s", indent, humanNumber(s.Sweep.Operations.Past.KeysProvided))
437
+ if !brief {
438
+ formatLine(1, "%sTotal records provided: %s", indent, humanNumber(s.Sweep.Operations.Past.RecordsProvided))
439
+ formatLine(1, "%sTotal provide errors: %s", indent, humanNumber(s.Sweep.Operations.Past.KeysFailed))
440
+ formatLine(1, "%sCIDs provided/min: %s", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysProvidedPerMinute))
441
+ formatLine(1, "%sCIDs reprovided/min: %s", indent, humanFloatOrNA(s.Sweep.Operations.Past.KeysReprovidedPerMinute))
442
+ formatLine(1, "%sRegion reprovide duration: %s", indent, humanDurationOrNA(s.Sweep.Operations.Past.RegionReprovideDuration))
443
+ formatLine(1, "%sAvg CIDs/reprovide: %s", indent, humanFloatOrNA(s.Sweep.Operations.Past.AvgKeysPerReprovide))
444
+ formatLine(1, "%sRegions reprovided (last cycle): %s", indent, humanNumber(s.Sweep.Operations.Past.RegionReprovidedLastCycle))
445
+ addBlankLine(1)
446
+ }
447
+ }
448
+
449
+ // Workers
450
+ displayWorkers := all || workers
451
+ if displayWorkers || brief {
452
+ availableReservedBurst := max(0, s.Sweep.Workers.DedicatedBurst-s.Sweep.Workers.ActiveBurst)
453
+ availableReservedPeriodic := max(0, s.Sweep.Workers.DedicatedPeriodic-s.Sweep.Workers.ActivePeriodic)
454
+ availableFreeWorkers := s.Sweep.Workers.Max - max(s.Sweep.Workers.DedicatedBurst, s.Sweep.Workers.ActiveBurst) - max(s.Sweep.Workers.DedicatedPeriodic, s.Sweep.Workers.ActivePeriodic)
455
+ availableBurst := availableFreeWorkers + availableReservedBurst
456
+ availablePeriodic := availableFreeWorkers + availableReservedPeriodic
457
+
458
+ if displayWorkers || availableBurst <= lowWorkerThreshold || availablePeriodic <= lowWorkerThreshold {
459
+ // Either we want to display workers information, or we are low on
460
+ // available workers and want to warn the user.
461
+ sectionTitle(0, "Workers")
462
+ specifyWorkers := " workers"
463
+ if compactMode {
464
+ specifyWorkers = ""
465
+ }
466
+ formatLine(0, "%sActive%s: %s / %s (max)", indent, specifyWorkers, humanNumber(s.Sweep.Workers.Active), humanNumber(s.Sweep.Workers.Max))
467
+ if brief {
468
+ // Brief mode - show condensed worker info
469
+ formatLine(0, "%sPeriodic%s: %s active, %s available, %s queued", indent, specifyWorkers,
470
+ humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(availablePeriodic), humanNumber(s.Sweep.Workers.QueuedPeriodic))
471
+ formatLine(0, "%sBurst%s: %s active, %s available, %s queued\n", indent, specifyWorkers,
472
+ humanNumber(s.Sweep.Workers.ActiveBurst), humanNumber(availableBurst), humanNumber(s.Sweep.Workers.QueuedBurst))
473
+ } else {
474
+ formatLine(0, "%sFree%s: %s", indent, specifyWorkers, humanNumber(availableFreeWorkers))
475
+ formatLine(0, "%sWorkers stats:%s %-9s %s", indent, " ", "Periodic", "Burst")
476
+ formatLine(0, "%s %-14s %-9s %s", indent, "Active:", humanNumber(s.Sweep.Workers.ActivePeriodic), humanNumber(s.Sweep.Workers.ActiveBurst))
477
+ formatLine(0, "%s %-14s %-9s %s", indent, "Dedicated:", humanNumber(s.Sweep.Workers.DedicatedPeriodic), humanNumber(s.Sweep.Workers.DedicatedBurst))
478
+ formatLine(0, "%s %-14s %-9s %s", indent, "Available:", humanNumber(availablePeriodic), humanNumber(availableBurst))
479
+ formatLine(0, "%s %-14s %-9s %s", indent, "Queued:", humanNumber(s.Sweep.Workers.QueuedPeriodic), humanNumber(s.Sweep.Workers.QueuedBurst))
480
+ formatLine(0, "%sMax connections/worker: %s", indent, humanNumber(s.Sweep.Workers.MaxProvideConnsPerWorker))
481
+ addBlankLine(0)
482
+ }
483
+ }
484
+ }
485
+ if compactMode {
486
+ col0Width := col0MaxWidth + 2
487
+ // Print both columns side by side
488
+ maxRows := max(len(cols[0]), len(cols[1]))
489
+ if maxRows == 0 {
490
+ return nil
491
+ }
492
+ for i := range maxRows - 1 { // last line is empty
493
+ var left, right string
494
+ if i < len(cols[0]) {
495
+ left = cols[0][i]
496
+ }
497
+ if i < len(cols[1]) {
498
+ right = cols[1][i]
499
+ }
500
+ fmt.Fprintf(wtr, "%-*s %s\n", col0Width, left, right)
501
+ }
502
+ } else {
503
+ if !brief {
504
+ cols[0] = cols[0][:len(cols[0])-1] // remove last blank line
505
+ }
506
+ for _, line := range cols[0] {
507
+ fmt.Fprintln(wtr, line)
508
}
509
}
510
return nil
514
}
515
516
func humanDuration(val time.Duration) string {
517
+ if val > time.Second {
518
+ return val.Truncate(100 * time.Millisecond).String()
519
+ }
520
return val.Truncate(time.Microsecond).String()
521
}
522
523
+func humanDurationOrNA(val time.Duration) string {
524
+ if val <= 0 {
525
+ return "N/A"
526
+ }
527
+ return humanDuration(val)
528
+}
529
+
530
func humanTime(val time.Time) string {
531
+ if val.IsZero() {
532
+ return "N/A"
533
+ }
534
return val.Format("2006-01-02 15:04:05")
535
}
536
544
return str
545
}
546
547
+// humanNumberOrNA is like humanNumber but returns "N/A" for non-positive values.
548
+func humanNumberOrNA[T constraints.Float | constraints.Integer](n T) string {
549
+ if n <= 0 {
550
+ return "N/A"
551
+ }
552
+ return humanNumber(n)
553
+}
554
+
555
+// humanFloatOrNA formats a float with 1 decimal place, returning "N/A" for non-positive values.
556
+// This is separate from humanNumberOrNA because it provides simple decimal formatting for
557
+// continuous metrics (averages, rates) rather than SI unit formatting used for discrete counts.
558
+func humanFloatOrNA(val float64) string {
559
+ if val <= 0 {
560
+ return "N/A"
561
+ }
562
+ return fmt.Sprintf("%.1f", val)
563
+}
564
+
565
func humanSI(val float64, decimals int) string {
566
v, unit := humanize.ComputeSI(val)
567
return fmt.Sprintf("%s%s", humanFull(v, decimals), unit)