From 5956f389161c1a0f471c3aa3c10ea0384aee0535 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Fri, 27 Mar 2026 09:21:24 -0400 Subject: [PATCH 1/9] feat: add CapacityForecast client method Add struct and client method for the new GET /admin/v4/capacity-forecast endpoint. Returns storage capacity predictions based on historical daily snapshots. --- capacity-forecast.go | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 capacity-forecast.go diff --git a/capacity-forecast.go b/capacity-forecast.go new file mode 100644 index 00000000..cf952c82 --- /dev/null +++ b/capacity-forecast.go @@ -0,0 +1,63 @@ +// +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package madmin + +import ( + "context" + "encoding/json" + "net/http" +) + +// CapacityForecast contains storage capacity predictions based on +// historical daily snapshots collected by the scanner. +type CapacityForecast struct { + CurrentUsedBytes uint64 `json:"currentUsedBytes"` + CurrentTotalBytes uint64 `json:"currentTotalBytes"` + CurrentUsedPercent float64 `json:"currentUsedPercent"` + DaysUntil80Pct float64 `json:"daysUntil80Pct"` + DaysUntil90Pct float64 `json:"daysUntil90Pct"` + DaysUntil100Pct float64 `json:"daysUntil100Pct"` + GrowthRatePerDay int64 `json:"growthRatePerDay"` + DataPointCount int `json:"dataPointCount"` +} + +// CapacityForecast returns a storage capacity forecast based on +// historical daily snapshots. +func (adm *AdminClient) CapacityForecast(ctx context.Context) (CapacityForecast, error) { + resp, err := adm.executeMethod(ctx, + http.MethodGet, + requestData{ + relPath: adminAPIPrefix + "/capacity-forecast", + }) + defer closeResponse(resp) + if err != nil { + return CapacityForecast{}, err + } + + if resp.StatusCode != http.StatusOK { + return CapacityForecast{}, httpRespToErrorResponse(resp) + } + + var f CapacityForecast + if err = json.NewDecoder(resp.Body).Decode(&f); err != nil { + return CapacityForecast{}, err + } + + return f, nil +} From d7a0a1d11bbaabd4eb493e0524a4ca2254c66587 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Mon, 13 Apr 2026 17:14:50 -0400 Subject: [PATCH 2/9] feat: add worst case, confidence, recency fields Add MinDaysUntilFull, RSquared, Variance, RecentGrowthRatePerDay, and RecentDaysUntilFull to CapacityForecast struct for Phase 2 of the prediction algorithm. --- capacity-forecast.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/capacity-forecast.go b/capacity-forecast.go index cf952c82..a4b867bc 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -35,6 +35,18 @@ type CapacityForecast struct { DaysUntil100Pct float64 `json:"daysUntil100Pct"` GrowthRatePerDay int64 `json:"growthRatePerDay"` DataPointCount int `json:"dataPointCount"` + + // Worst-case prediction based on the largest single-day growth + // observed between any two consecutive data points. + MinDaysUntilFull float64 `json:"minDaysUntilFull"` + + // Confidence metrics for the linear regression. + RSquared float64 `json:"rSquared"` // 0-1, goodness of fit + Variance float64 `json:"variance"` // variance of daily usedFraction deltas + + // Short-window (14-day) regression for recency-weighted predictions. + RecentGrowthRatePerDay float64 `json:"recentGrowthRatePerDay"` + RecentDaysUntilFull float64 `json:"recentDaysUntilFull"` } // CapacityForecast returns a storage capacity forecast based on From c6f2943d260739a26c02d7a54784a46f4698d164 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Mon, 13 Apr 2026 18:13:04 -0400 Subject: [PATCH 3/9] feat: add DayMinDelta and DayMaxDelta fields to CapacityForecast Expose concrete min/max daily usedFraction deltas so consumers can show the actual extremes alongside the statistical summary. --- capacity-forecast.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/capacity-forecast.go b/capacity-forecast.go index a4b867bc..bd222346 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -44,6 +44,11 @@ type CapacityForecast struct { RSquared float64 `json:"rSquared"` // 0-1, goodness of fit Variance float64 `json:"variance"` // variance of daily usedFraction deltas + // Concrete min/max daily changes in usedFraction between consecutive + // data points. DayMinDelta can be negative (space was freed). + DayMinDelta float64 `json:"dayMinDelta"` + DayMaxDelta float64 `json:"dayMaxDelta"` + // Short-window (14-day) regression for recency-weighted predictions. RecentGrowthRatePerDay float64 `json:"recentGrowthRatePerDay"` RecentDaysUntilFull float64 `json:"recentDaysUntilFull"` From 54ad2472ea8530ea23852c48170d7d30f49dd3c8 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Tue, 14 Apr 2026 10:31:42 -0400 Subject: [PATCH 4/9] refactor: use *float64 pointers for optional days-until fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A -1 sentinel collides with legitimate negative values — e.g. days until 80%% full can legitimately be -5 when usage already crossed the threshold 5 days ago. Use *float64 with nil for "unknown" so the type system represents unset state cleanly. Addresses review feedback from klauspost. --- capacity-forecast.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/capacity-forecast.go b/capacity-forecast.go index bd222346..3a6295d7 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -26,19 +26,26 @@ import ( // CapacityForecast contains storage capacity predictions based on // historical daily snapshots collected by the scanner. +// +// Days-until-threshold fields are pointers: nil means "unknown" (for example, +// not enough history yet, or usage is not growing). A concrete value may be +// negative when the threshold was already crossed in the past. type CapacityForecast struct { CurrentUsedBytes uint64 `json:"currentUsedBytes"` CurrentTotalBytes uint64 `json:"currentTotalBytes"` CurrentUsedPercent float64 `json:"currentUsedPercent"` - DaysUntil80Pct float64 `json:"daysUntil80Pct"` - DaysUntil90Pct float64 `json:"daysUntil90Pct"` - DaysUntil100Pct float64 `json:"daysUntil100Pct"` - GrowthRatePerDay int64 `json:"growthRatePerDay"` - DataPointCount int `json:"dataPointCount"` + + // Days until each usage threshold is reached. nil = unknown. + DaysUntil80Pct *float64 `json:"daysUntil80Pct,omitempty"` + DaysUntil90Pct *float64 `json:"daysUntil90Pct,omitempty"` + DaysUntil100Pct *float64 `json:"daysUntil100Pct,omitempty"` + + GrowthRatePerDay int64 `json:"growthRatePerDay"` + DataPointCount int `json:"dataPointCount"` // Worst-case prediction based on the largest single-day growth - // observed between any two consecutive data points. - MinDaysUntilFull float64 `json:"minDaysUntilFull"` + // observed between any two consecutive data points. nil = unknown. + MinDaysUntilFull *float64 `json:"minDaysUntilFull,omitempty"` // Confidence metrics for the linear regression. RSquared float64 `json:"rSquared"` // 0-1, goodness of fit @@ -50,8 +57,8 @@ type CapacityForecast struct { DayMaxDelta float64 `json:"dayMaxDelta"` // Short-window (14-day) regression for recency-weighted predictions. - RecentGrowthRatePerDay float64 `json:"recentGrowthRatePerDay"` - RecentDaysUntilFull float64 `json:"recentDaysUntilFull"` + RecentGrowthRatePerDay float64 `json:"recentGrowthRatePerDay"` + RecentDaysUntilFull *float64 `json:"recentDaysUntilFull,omitempty"` } // CapacityForecast returns a storage capacity forecast based on From 2edda8d0647d7a932193897b11e65a891b40165b Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Tue, 14 Apr 2026 18:23:50 -0400 Subject: [PATCH 5/9] docs: update CapacityForecast comments for Kalman filter Update struct and field comments to reflect the switch from OLS linear regression to a Kalman filter. No code or type changes. --- capacity-forecast.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/capacity-forecast.go b/capacity-forecast.go index 3a6295d7..64811f47 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -25,7 +25,7 @@ import ( ) // CapacityForecast contains storage capacity predictions based on -// historical daily snapshots collected by the scanner. +// historical daily snapshots processed through a Kalman filter. // // Days-until-threshold fields are pointers: nil means "unknown" (for example, // not enough history yet, or usage is not growing). A concrete value may be @@ -47,8 +47,8 @@ type CapacityForecast struct { // observed between any two consecutive data points. nil = unknown. MinDaysUntilFull *float64 `json:"minDaysUntilFull,omitempty"` - // Confidence metrics for the linear regression. - RSquared float64 `json:"rSquared"` // 0-1, goodness of fit + // Confidence from Kalman filter covariance (0-1, higher = more confident). + RSquared float64 `json:"rSquared"` Variance float64 `json:"variance"` // variance of daily usedFraction deltas // Concrete min/max daily changes in usedFraction between consecutive @@ -56,7 +56,7 @@ type CapacityForecast struct { DayMinDelta float64 `json:"dayMinDelta"` DayMaxDelta float64 `json:"dayMaxDelta"` - // Short-window (14-day) regression for recency-weighted predictions. + // Recency-weighted predictions from the Kalman filter. RecentGrowthRatePerDay float64 `json:"recentGrowthRatePerDay"` RecentDaysUntilFull *float64 `json:"recentDaysUntilFull,omitempty"` } From 8808b140ad903adfd335cc4452dec7a3ee691564 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Mon, 11 May 2026 14:08:36 -0400 Subject: [PATCH 6/9] capacity-forecast: drop RSquared field from CapacityForecast The Kalman filter is the forecaster; an OLS R-squared offered no information about the filter's own predictions and only measured the linearity of the raw data. Drop the field to keep the API focused on what the filter actually computes. --- capacity-forecast.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/capacity-forecast.go b/capacity-forecast.go index 64811f47..b264866e 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -47,8 +47,6 @@ type CapacityForecast struct { // observed between any two consecutive data points. nil = unknown. MinDaysUntilFull *float64 `json:"minDaysUntilFull,omitempty"` - // Confidence from Kalman filter covariance (0-1, higher = more confident). - RSquared float64 `json:"rSquared"` Variance float64 `json:"variance"` // variance of daily usedFraction deltas // Concrete min/max daily changes in usedFraction between consecutive From 9fef0bc45e29ff184eeca3c546357e95ed5ff1c0 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Mon, 11 May 2026 21:45:28 -0400 Subject: [PATCH 7/9] capacity-forecast: rename fields with explicit units Per review feedback, embed the unit in the names so readers do not have to look at the implementation to discover what they hold: - GrowthRatePerDay -> GrowthBytesPerDay - DataPointCount -> DailySnapshotCount The doc comments now also clarify that GrowthBytesPerDay is independent of DailySnapshotCount. --- capacity-forecast.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/capacity-forecast.go b/capacity-forecast.go index b264866e..c907d6ae 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -40,8 +40,17 @@ type CapacityForecast struct { DaysUntil90Pct *float64 `json:"daysUntil90Pct,omitempty"` DaysUntil100Pct *float64 `json:"daysUntil100Pct,omitempty"` - GrowthRatePerDay int64 `json:"growthRatePerDay"` - DataPointCount int `json:"dataPointCount"` + // GrowthBytesPerDay is the Kalman filter slope expressed in bytes per + // day, projected from the daily snapshots in the circular buffer. + // It is independent of DailySnapshotCount: the filter is recency + // weighted, not a delta between two endpoints. + GrowthBytesPerDay int64 `json:"growthBytesPerDay"` + + // DailySnapshotCount is the number of valid daily snapshots currently + // held in the year-long circular buffer (range 0..365). The forecast + // fields above are only populated when this count reaches the + // minimum required for the filter to produce stable estimates. + DailySnapshotCount int `json:"dailySnapshotCount"` // Worst-case prediction based on the largest single-day growth // observed between any two consecutive data points. nil = unknown. From e09bc75cadaae0f367a7ff8f781bedbbbf1a4459 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Mon, 11 May 2026 22:47:51 -0400 Subject: [PATCH 8/9] capacity-forecast: describe the days-until projection Per review feedback, document the nature of the trend used for the DaysUntil80/90/100Pct fields: the Kalman filter integrates up to 365 daily samples with recency-weighted emphasis, so a fresh shift in usage dominates over older history. --- capacity-forecast.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/capacity-forecast.go b/capacity-forecast.go index c907d6ae..7f32937a 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -35,7 +35,11 @@ type CapacityForecast struct { CurrentTotalBytes uint64 `json:"currentTotalBytes"` CurrentUsedPercent float64 `json:"currentUsedPercent"` - // Days until each usage threshold is reached. nil = unknown. + // Days until each usage threshold is reached. The projection comes from + // a Kalman filter that integrates up to 365 daily samples with greater + // weight on recent observations, so a fresh shift in usage dominates + // over older history. nil = unknown (slope is non-positive, or there + // is not enough history). DaysUntil80Pct *float64 `json:"daysUntil80Pct,omitempty"` DaysUntil90Pct *float64 `json:"daysUntil90Pct,omitempty"` DaysUntil100Pct *float64 `json:"daysUntil100Pct,omitempty"` From e45516892e4f3a23eca500e4acdac3fe09605448 Mon Sep 17 00:00:00 2001 From: Cesar Celis Date: Tue, 12 May 2026 08:35:11 -0400 Subject: [PATCH 9/9] capacity-forecast: expose min/max daily deltas in bytes The previous fields expressed the change as a fraction in [-1, +1], which the reviewer found unclear. Bytes match the other size fields (CurrentUsedBytes, CurrentTotalBytes, GrowthBytesPerDay) and tell the user directly how much data was added or freed that day. --- capacity-forecast.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/capacity-forecast.go b/capacity-forecast.go index 7f32937a..36915bf1 100644 --- a/capacity-forecast.go +++ b/capacity-forecast.go @@ -62,10 +62,11 @@ type CapacityForecast struct { Variance float64 `json:"variance"` // variance of daily usedFraction deltas - // Concrete min/max daily changes in usedFraction between consecutive - // data points. DayMinDelta can be negative (space was freed). - DayMinDelta float64 `json:"dayMinDelta"` - DayMaxDelta float64 `json:"dayMaxDelta"` + // Smallest and largest day-to-day changes in used bytes observed + // between consecutive snapshots. A negative value means space was + // freed between those two days. + DayMinDeltaBytes int64 `json:"dayMinDeltaBytes"` + DayMaxDeltaBytes int64 `json:"dayMaxDeltaBytes"` // Recency-weighted predictions from the Kalman filter. RecentGrowthRatePerDay float64 `json:"recentGrowthRatePerDay"`