Merge pull request #18080 from ldufr/ldufresne/retention-size-percentage
Some checks are pending
buf.build / lint and publish (push) Waiting to run
CI / Go tests (push) Waiting to run
CI / More Go tests (push) Waiting to run
CI / Go tests with previous Go version (push) Waiting to run
CI / UI tests (push) Waiting to run
CI / Go tests on Windows (push) Waiting to run
CI / Mixins tests (push) Waiting to run
CI / Compliance testing (push) Waiting to run
CI / Build Prometheus for common architectures (push) Waiting to run
CI / Build Prometheus for all architectures (push) Waiting to run
CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
CI / Check generated parser (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / fuzzing (push) Waiting to run
CI / codeql (push) Waiting to run
CI / Publish main branch artifacts (push) Blocked by required conditions
CI / Publish release artefacts (push) Blocked by required conditions
CI / Publish UI on npm Registry (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

Add percentage based retention
This commit is contained in:
Julien 2026-02-24 15:50:36 +01:00 committed by GitHub
commit 9d38077e50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 342 additions and 22 deletions

View file

@ -717,6 +717,9 @@ func main() {
if cfgFile.StorageConfig.TSDBConfig.Retention.Size > 0 {
cfg.tsdb.MaxBytes = cfgFile.StorageConfig.TSDBConfig.Retention.Size
}
if cfgFile.StorageConfig.TSDBConfig.Retention.Percentage > 0 {
cfg.tsdb.MaxPercentage = cfgFile.StorageConfig.TSDBConfig.Retention.Percentage
}
}
}
@ -770,9 +773,9 @@ func main() {
cfg.web.RoutePrefix = "/" + strings.Trim(cfg.web.RoutePrefix, "/")
if !agentMode {
if cfg.tsdb.RetentionDuration == 0 && cfg.tsdb.MaxBytes == 0 {
if cfg.tsdb.RetentionDuration == 0 && cfg.tsdb.MaxBytes == 0 && cfg.tsdb.MaxPercentage == 0 {
cfg.tsdb.RetentionDuration = defaultRetentionDuration
logger.Info("No time or size retention was set so using the default time retention", "duration", defaultRetentionDuration)
logger.Info("No time, size or percentage retention was set so using the default time retention", "duration", defaultRetentionDuration)
}
// Check for overflows. This limits our max retention to 100y.
@ -785,6 +788,20 @@ func main() {
logger.Warn("Time retention value is too high. Limiting to: " + y.String())
}
if cfg.tsdb.MaxPercentage > 100 {
cfg.tsdb.MaxPercentage = 100
logger.Warn("Percentage retention value is too high. Limiting to: 100%")
}
if cfg.tsdb.MaxPercentage > 0 {
if cfg.tsdb.MaxBytes > 0 {
logger.Warn("storage.tsdb.retention.size is ignored, because storage.tsdb.retention.percentage is specified")
}
if prom_runtime.FsSize(localStoragePath) == 0 {
fmt.Fprintln(os.Stderr, fmt.Errorf("unable to detect total capacity of metric storage at %s, please disable retention percentage (%d%%)", localStoragePath, cfg.tsdb.MaxPercentage))
os.Exit(2)
}
}
// Max block size settings.
if cfg.tsdb.MaxBlockDuration == 0 {
maxBlockDuration, err := model.ParseDuration("31d")
@ -958,6 +975,7 @@ func main() {
cfg.web.Context = ctxWeb
cfg.web.TSDBRetentionDuration = cfg.tsdb.RetentionDuration
cfg.web.TSDBMaxBytes = cfg.tsdb.MaxBytes
cfg.web.TSDBMaxPercentage = cfg.tsdb.MaxPercentage
cfg.web.TSDBDir = localStoragePath
cfg.web.LocalStorage = localStorage
cfg.web.Storage = fanoutStorage
@ -1377,7 +1395,7 @@ func main() {
return fmt.Errorf("opening storage failed: %w", err)
}
switch fsType := prom_runtime.Statfs(localStoragePath); fsType {
switch fsType := prom_runtime.FsType(localStoragePath); fsType {
case "NFS_SUPER_MAGIC":
logger.Warn("This filesystem is not supported and may lead to data corruption and data loss. Please carefully read https://prometheus.io/docs/prometheus/latest/storage/ to learn more about supported filesystems.", "fs_type", fsType)
default:
@ -1389,6 +1407,7 @@ func main() {
"MinBlockDuration", cfg.tsdb.MinBlockDuration,
"MaxBlockDuration", cfg.tsdb.MaxBlockDuration,
"MaxBytes", cfg.tsdb.MaxBytes,
"MaxPercentage", cfg.tsdb.MaxPercentage,
"NoLockfile", cfg.tsdb.NoLockfile,
"RetentionDuration", cfg.tsdb.RetentionDuration,
"WALSegmentSize", cfg.tsdb.WALSegmentSize,
@ -1438,7 +1457,7 @@ func main() {
return fmt.Errorf("opening storage failed: %w", err)
}
switch fsType := prom_runtime.Statfs(localStoragePath); fsType {
switch fsType := prom_runtime.FsType(localStoragePath); fsType {
case "NFS_SUPER_MAGIC":
logger.Warn(fsType, "msg", "This filesystem is not supported and may lead to data corruption and data loss. Please carefully read https://prometheus.io/docs/prometheus/latest/storage/ to learn more about supported filesystems.")
default:
@ -1963,6 +1982,7 @@ type tsdbOptions struct {
MaxBlockChunkSegmentSize units.Base2Bytes
RetentionDuration model.Duration
MaxBytes units.Base2Bytes
MaxPercentage uint
NoLockfile bool
WALCompressionType compression.Type
HeadChunksWriteQueueSize int
@ -1991,6 +2011,7 @@ func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
MaxBlockChunkSegmentSize: int64(opts.MaxBlockChunkSegmentSize),
RetentionDuration: int64(time.Duration(opts.RetentionDuration) / time.Millisecond),
MaxBytes: int64(opts.MaxBytes),
MaxPercentage: opts.MaxPercentage,
NoLockfile: opts.NoLockfile,
WALCompression: opts.WALCompressionType,
HeadChunksWriteQueueSize: opts.HeadChunksWriteQueueSize,

View file

@ -1092,6 +1092,9 @@ type TSDBRetentionConfig struct {
// Maximum number of bytes that can be stored for blocks.
Size units.Base2Bytes `yaml:"size,omitempty"`
// Maximum percentage of disk used for TSDB storage.
Percentage uint `yaml:"percentage,omitempty"`
}
// TSDBConfig configures runtime reloadable configuration options.

View file

@ -1737,8 +1737,9 @@ var expectedConf = &Config{
OutOfOrderTimeWindowFlag: model.Duration(30 * time.Minute),
StaleSeriesCompactionThreshold: 0.5,
Retention: &TSDBRetentionConfig{
Time: model.Duration(24 * time.Hour),
Size: 1 * units.GiB,
Time: model.Duration(24 * time.Hour),
Size: 1 * units.GiB,
Percentage: 28,
},
},
},

View file

@ -457,6 +457,7 @@ storage:
retention:
time: 1d
size: 1GB
percentage: 28
tracing:
endpoint: "localhost:4317"

View file

@ -3696,6 +3696,14 @@ with this feature.
# This option takes precedence over the deprecated command-line flag --storage.tsdb.retention.size.
[ size: <size> | default = 0 ]
# Maximum percent of total disk space allowed for storage of blocks. Alternative to `size` and
# behaves the same as if size was calculated by hand as a percentage of the total storage capacity.
# Prometheus will fail to start if this config is enabled, but it fails to query the total storage capacity.
# The total disk space allowed will automatically adapt to volume resize.
# If set to 0 or not set, percentage-based retention is disabled.
#
# This is an experimental feature, this behaviour could change or be removed in the future.
[ percentage: <uint> | default = 0 ]
```
### `<exemplars>`

View file

@ -47,6 +47,7 @@ import (
"github.com/prometheus/prometheus/tsdb/wlog"
"github.com/prometheus/prometheus/util/compression"
"github.com/prometheus/prometheus/util/features"
prom_runtime "github.com/prometheus/prometheus/util/runtime"
)
const (
@ -126,6 +127,11 @@ type Options struct {
// the current size of the database.
MaxBytes int64
// Maximum % of disk space to use for blocks to be retained.
// 0 or less means disabled.
// If both MaxBytes and MaxPercentage are set, percentage prevails.
MaxPercentage uint
// NoLockfile disables creation and consideration of a lock file.
NoLockfile bool
@ -257,6 +263,9 @@ type Options struct {
// StaleSeriesCompactionThreshold is a number between 0.0-1.0 indicating the % of stale series in
// the in-memory Head block. If the % of stale series crosses this threshold, stale series compaction is run immediately.
StaleSeriesCompactionThreshold float64
// FsSizeFunc is a function returning the total disk size for a given path.
FsSizeFunc FsSizeFunc
}
type NewCompactorFunc func(ctx context.Context, r prometheus.Registerer, l *slog.Logger, ranges []int64, pool chunkenc.Pool, opts *Options) (Compactor, error)
@ -267,6 +276,8 @@ type BlockQuerierFunc func(b BlockReader, mint, maxt int64) (storage.Querier, er
type BlockChunkQuerierFunc func(b BlockReader, mint, maxt int64) (storage.ChunkQuerier, error)
type FsSizeFunc func(path string) uint64
// DB handles reads and writes of time series falling into
// a hashed partition of a seriedb.
type DB struct {
@ -328,6 +339,8 @@ type DB struct {
blockQuerierFunc BlockQuerierFunc
blockChunkQuerierFunc BlockChunkQuerierFunc
fsSizeFunc FsSizeFunc
}
type dbMetrics struct {
@ -344,6 +357,7 @@ type dbMetrics struct {
tombCleanTimer prometheus.Histogram
blocksBytes prometheus.Gauge
maxBytes prometheus.Gauge
maxPercentage prometheus.Gauge
retentionDuration prometheus.Gauge
staleSeriesCompactionsTriggered prometheus.Counter
staleSeriesCompactionsFailed prometheus.Counter
@ -424,6 +438,10 @@ func newDBMetrics(db *DB, r prometheus.Registerer) *dbMetrics {
Name: "prometheus_tsdb_retention_limit_bytes",
Help: "Max number of bytes to be retained in the tsdb blocks, configured 0 means disabled",
})
m.maxPercentage = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "prometheus_tsdb_retention_limit_percentage",
Help: "Max percentage of total storage space to be retained in the tsdb blocks, configured 0 means disabled",
})
m.retentionDuration = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "prometheus_tsdb_retention_limit_seconds",
Help: "How long to retain samples in storage.",
@ -464,6 +482,7 @@ func newDBMetrics(db *DB, r prometheus.Registerer) *dbMetrics {
m.tombCleanTimer,
m.blocksBytes,
m.maxBytes,
m.maxPercentage,
m.retentionDuration,
m.staleSeriesCompactionsTriggered,
m.staleSeriesCompactionsFailed,
@ -669,6 +688,7 @@ func (db *DBReadOnly) loadDataAsQueryable(maxt int64) (storage.SampleAndChunkQue
head: head,
blockQuerierFunc: NewBlockQuerier,
blockChunkQuerierFunc: NewBlockChunkQuerier,
fsSizeFunc: prom_runtime.FsSize,
}, nil
}
@ -1007,6 +1027,12 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
db.blockChunkQuerierFunc = opts.BlockChunkQuerierFunc
}
if opts.FsSizeFunc == nil {
db.fsSizeFunc = prom_runtime.FsSize
} else {
db.fsSizeFunc = opts.FsSizeFunc
}
var wal, wbl *wlog.WL
segmentSize := wlog.DefaultSegmentSize
// Wal is enabled.
@ -1066,6 +1092,7 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn
db.metrics = newDBMetrics(db, r)
maxBytes := max(opts.MaxBytes, 0)
db.metrics.maxBytes.Set(float64(maxBytes))
db.metrics.maxPercentage.Set(float64(max(opts.MaxPercentage, 0)))
db.metrics.retentionDuration.Set((time.Duration(opts.RetentionDuration) * time.Millisecond).Seconds())
// Calling db.reload() calls db.reloadBlocks() which requires cmtx to be locked.
@ -1258,6 +1285,10 @@ func (db *DB) ApplyConfig(conf *config.Config) error {
db.opts.MaxBytes = int64(conf.StorageConfig.TSDBConfig.Retention.Size)
db.metrics.maxBytes.Set(float64(db.opts.MaxBytes))
}
if conf.StorageConfig.TSDBConfig.Retention.Percentage > 0 {
db.opts.MaxPercentage = conf.StorageConfig.TSDBConfig.Retention.Percentage
db.metrics.maxPercentage.Set(float64(db.opts.MaxPercentage))
}
db.retentionMtx.Unlock()
}
} else {
@ -1303,11 +1334,11 @@ func (db *DB) getRetentionDuration() int64 {
return db.opts.RetentionDuration
}
// getMaxBytes returns the current max bytes setting in a thread-safe manner.
func (db *DB) getMaxBytes() int64 {
// getRetentionSettings returns max bytes and max percentage settings in a thread-safe manner.
func (db *DB) getRetentionSettings() (int64, uint) {
db.retentionMtx.RLock()
defer db.retentionMtx.RUnlock()
return db.opts.MaxBytes
return db.opts.MaxBytes, db.opts.MaxPercentage
}
// dbAppender wraps the DB's head appender and triggers compactions on commit
@ -1967,9 +1998,25 @@ func BeyondTimeRetention(db *DB, blocks []*Block) (deletable map[ulid.ULID]struc
// BeyondSizeRetention returns those blocks which are beyond the size retention
// set in the db options.
func BeyondSizeRetention(db *DB, blocks []*Block) (deletable map[ulid.ULID]struct{}) {
// Size retention is disabled or no blocks to work with.
maxBytes := db.getMaxBytes()
if len(blocks) == 0 || maxBytes <= 0 {
// No blocks to work with
if len(blocks) == 0 {
return deletable
}
maxBytes, maxPercentage := db.getRetentionSettings()
// Max percentage prevails over max size.
if maxPercentage > 0 {
diskSize := db.fsSizeFunc(db.dir)
if diskSize <= 0 {
db.logger.Warn("Unable to retrieve filesystem size of database directory, skip percentage limitation and default to fixed size limitation", "dir", db.dir)
} else {
maxBytes = int64(uint64(maxPercentage) * diskSize / 100)
}
}
// Size retention is disabled.
if maxBytes <= 0 {
return deletable
}

View file

@ -9603,3 +9603,39 @@ func TestStaleSeriesCompactionWithZeroSeries(t *testing.T) {
// Should still have no blocks since there was nothing to compact.
require.Empty(t, db.Blocks())
}
func TestBeyondSizeRetentionWithPercentage(t *testing.T) {
const maxBlock = 100
const numBytesChunks = 1024
const diskSize = maxBlock * numBytesChunks
opts := DefaultOptions()
opts.MaxPercentage = 10
opts.FsSizeFunc = func(_ string) uint64 {
return uint64(diskSize)
}
db := newTestDB(t, withOpts(opts))
require.Zero(t, db.Head().Size())
blocks := make([]*Block, 0, opts.MaxPercentage+1)
for range opts.MaxPercentage {
blocks = append(blocks, &Block{
numBytesChunks: numBytesChunks,
meta: BlockMeta{ULID: ulid.Make()},
})
}
deletable := BeyondSizeRetention(db, blocks)
require.Empty(t, deletable)
ulid := ulid.Make()
blocks = append(blocks, &Block{
numBytesChunks: numBytesChunks,
meta: BlockMeta{ULID: ulid},
})
deletable = BeyondSizeRetention(db, blocks)
require.Len(t, deletable, 1)
require.Contains(t, deletable, ulid)
}

View file

@ -11,12 +11,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build openbsd || windows || netbsd || solaris
//go:build openbsd || netbsd || solaris
package runtime
// Statfs returns the file system type (Unix only)
// syscall.Statfs_t isn't available on openbsd
func Statfs(path string) string {
// FsType returns the file system type or "unknown" if unsupported.
func FsType(path string) string {
return "unknown"
}
// FsSize returns the file system size or 0 if unsupported.
func FsSize(path string) uint64 {
return 0
}

View file

@ -20,8 +20,7 @@ import (
"syscall"
)
// Statfs returns the file system type (Unix only).
func Statfs(path string) string {
func FsType(path string) string {
// Types of file systems that may be returned by `statfs`
fsTypes := map[int64]string{
0xadf5: "ADFS_SUPER_MAGIC",
@ -67,6 +66,7 @@ func Statfs(path string) string {
0x012FF7B4: "XENIX_SUPER_MAGIC",
0x58465342: "XFS_SUPER_MAGIC",
0x012FD16D: "_XIAFS_SUPER_MAGIC",
0x794c7630: "OVERLAYFS_SUPER_MAGIC",
}
var fs syscall.Statfs_t
@ -82,3 +82,12 @@ func Statfs(path string) string {
}
return strconv.FormatInt(localType, 16)
}
func FsSize(path string) uint64 {
var fs syscall.Statfs_t
err := syscall.Statfs(path, &fs)
if err != nil {
return 0
}
return uint64(fs.Bsize) * fs.Blocks
}

View file

@ -20,8 +20,8 @@ import (
"syscall"
)
// Statfs returns the file system type (Unix only)
func Statfs(path string) string {
// FsType returns the file system type (Unix only).
func FsType(path string) string {
// Types of file systems that may be returned by `statfs`
fsTypes := map[int32]string{
0xadf5: "ADFS_SUPER_MAGIC",
@ -63,6 +63,7 @@ func Statfs(path string) string {
0x012FF7B4: "XENIX_SUPER_MAGIC",
0x58465342: "XFS_SUPER_MAGIC",
0x012FD16D: "_XIAFS_SUPER_MAGIC",
0x794c7630: "OVERLAYFS_SUPER_MAGIC",
}
var fs syscall.Statfs_t
@ -75,3 +76,13 @@ func Statfs(path string) string {
}
return strconv.Itoa(int(fs.Type))
}
// FsSize returns the file system size (Unix only).
func FsSize(path string) uint64 {
var fs syscall.Statfs_t
err := syscall.Statfs(path, &fs)
if err != nil {
return 0
}
return uint64(fs.Bsize) * fs.Blocks
}

View file

@ -20,8 +20,7 @@ import (
"syscall"
)
// Statfs returns the file system type (Unix only)
func Statfs(path string) string {
func FsType(path string) string {
// Types of file systems that may be returned by `statfs`
fsTypes := map[uint32]string{
0xadf5: "ADFS_SUPER_MAGIC",
@ -63,6 +62,7 @@ func Statfs(path string) string {
0x012FF7B4: "XENIX_SUPER_MAGIC",
0x58465342: "XFS_SUPER_MAGIC",
0x012FD16D: "_XIAFS_SUPER_MAGIC",
0x794c7630: "OVERLAYFS_SUPER_MAGIC",
}
var fs syscall.Statfs_t
@ -75,3 +75,12 @@ func Statfs(path string) string {
}
return strconv.Itoa(int(fs.Type))
}
func FsSize(path string) uint64 {
var fs syscall.Statfs_t
err := syscall.Statfs(path, &fs)
if err != nil {
return 0
}
return uint64(fs.Bsize) * fs.Blocks
}

View file

@ -0,0 +1,58 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows && !openbsd && !netbsd && !solaris
package runtime
import (
"os"
"testing"
"github.com/grafana/regexp"
"github.com/stretchr/testify/require"
)
var regexpFsType = regexp.MustCompile("^[A-Z][A-Z0-9_]*_MAGIC$")
func TestFsType(t *testing.T) {
var fsType string
path, err := os.Getwd()
require.NoError(t, err)
fsType = FsType(path)
require.Regexp(t, regexpFsType, fsType)
fsType = FsType("/no/where/to/be/found")
require.Equal(t, "0", fsType)
fsType = FsType(" %% not event a real path\n\n")
require.Equal(t, "0", fsType)
}
func TestFsSize(t *testing.T) {
var size uint64
path, err := os.Getwd()
require.NoError(t, err)
size = FsSize(path)
require.Positive(t, size)
size = FsSize("/no/where/to/be/found")
require.Equal(t, uint64(0), size)
size = FsSize(" %% not event a real path\n\n")
require.Equal(t, uint64(0), size)
}

View file

@ -0,0 +1,56 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package runtime
import (
"os"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var (
dll = windows.MustLoadDLL("kernel32.dll")
getDiskFreeSpaceExW = dll.MustFindProc("GetDiskFreeSpaceExW")
)
func FsType(path string) string {
return "unknown"
}
func FsSize(path string) uint64 {
// Ensure the path exists.
if _, err := os.Stat(path); err != nil {
return 0
}
var avail int64
var total int64
var free int64
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdiskfreespaceexa
ret, _, _ := getDiskFreeSpaceExW.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))),
uintptr(unsafe.Pointer(&avail)),
uintptr(unsafe.Pointer(&total)),
uintptr(unsafe.Pointer(&free)))
if ret == 0 || uint64(free) > uint64(total) {
return 0
}
return uint64(total)
}

View file

@ -0,0 +1,49 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package runtime
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func TestFsType(t *testing.T) {
var fsType string
path, err := os.Getwd()
require.NoError(t, err)
fsType = FsType(path)
require.Equal(t, "unknown", fsType)
fsType = FsType("A:\\no\\where\\to\\be\\found")
require.Equal(t, "unknown", fsType)
}
func TestFsSize(t *testing.T) {
var size uint64
size = FsSize("C:\\")
require.Positive(t, size)
size = FsSize("c:\\no\\where\\to\\be\\found")
require.Equal(t, uint64(0), size)
size = FsSize(" %% not event a real path\n\n")
require.Equal(t, uint64(0), size)
}

View file

@ -263,6 +263,7 @@ type Options struct {
TSDBRetentionDuration model.Duration
TSDBDir string
TSDBMaxBytes units.Base2Bytes
TSDBMaxPercentage uint
LocalStorage LocalStorage
Storage storage.Storage
ExemplarStorage storage.ExemplarQueryable
@ -874,6 +875,12 @@ func (h *Handler) runtimeInfo() (api_v1.RuntimeInfo, error) {
}
status.StorageRetention += h.options.TSDBMaxBytes.String()
}
if h.options.TSDBMaxPercentage != 0 {
if status.StorageRetention != "" {
status.StorageRetention += " or "
}
status.StorageRetention = status.StorageRetention + strconv.FormatUint(uint64(h.options.TSDBMaxPercentage), 10) + "%"
}
metrics, err := h.gatherer.Gather()
if err != nil {