diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index 409b72f5f9..fcb655fb1e 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -824,7 +824,7 @@ func main() { 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 { + if storagePathFsSize(localStoragePath) == 0 { fmt.Fprintln(os.Stderr, fmt.Errorf("unable to detect total capacity of metric storage at %s, please disable retention percentage (%g%%)", localStoragePath, cfg.tsdb.MaxPercentage)) os.Exit(2) } @@ -1756,6 +1756,25 @@ func computeExternalURL(u, listenAddr string) (*url.URL, error) { return eu, nil } +// storagePathFsSize returns the filesystem size for path or its closest existing parent. +func storagePathFsSize(path string) uint64 { + for { + if size := prom_runtime.FsSize(path); size > 0 { + return size + } + + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { + return 0 + } + + parent := filepath.Dir(path) + if parent == path { + return 0 + } + path = parent + } +} + // readyStorage implements the Storage interface while allowing to set the actual // storage at a later point in time. type readyStorage struct { diff --git a/cmd/prometheus/main_test.go b/cmd/prometheus/main_test.go index 5e57dd9352..228898054a 100644 --- a/cmd/prometheus/main_test.go +++ b/cmd/prometheus/main_test.go @@ -148,6 +148,47 @@ func TestFailedStartupExitCode(t *testing.T) { require.Equal(t, expectedExitStatus, status.ExitStatus()) } +func TestRetentionPercentageStartsWithMissingStoragePath(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + t.Parallel() + + tmpDir := t.TempDir() + if storagePathFsSize(tmpDir) == 0 { + t.Skip("skipping test because filesystem size detection is unavailable.") + } + + configFile := filepath.Join(tmpDir, "prometheus.yml") + storagePath := filepath.Join(tmpDir, "missing", "data") + + require.NoError(t, os.WriteFile(configFile, []byte(` +storage: + tsdb: + retention: + percentage: 1.5 +`), 0o777)) + + port := testutil.RandomUnprivilegedPort(t) + prom := prometheusCommandWithLogging( + t, + configFile, + port, + "--storage.tsdb.path="+storagePath, + ) + require.NoError(t, prom.Start()) + + require.Eventually(t, func() bool { + r, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port)) + if err != nil { + return false + } + defer r.Body.Close() + return r.StatusCode == http.StatusOK + }, startupTime, 100*time.Millisecond) + require.DirExists(t, storagePath) +} + type senderFunc func(alerts ...*notifier.Alert) func (s senderFunc) Send(alerts ...*notifier.Alert) {