From 8124ef82b37d0382752b2617dacde635d45c77ac Mon Sep 17 00:00:00 2001
From: Alexei Dodon <adodon@cisco.com>
Date: Mon, 30 Oct 2023 13:28:37 +0200
Subject: [PATCH] fix: more accurate storage metrics after zot restart

Signed-off-by: Alexei Dodon <adodon@cisco.com>
---
 THIRD-PARTY-LICENSES.md                      | 30 ++++----
 pkg/api/controller.go                        |  8 +++
 pkg/extensions/monitoring/common.go          |  2 +-
 pkg/extensions/monitoring/extension.go       |  2 +-
 pkg/extensions/monitoring/minimal.go         |  2 +-
 pkg/extensions/monitoring/monitoring_test.go | 69 +++++++++++++++++++
 pkg/storage/common/common.go                 | 72 ++++++++++++++++++++
 pkg/storage/imagestore/imagestore.go         | 22 +++++-
 pkg/storage/types/types.go                   |  1 +
 pkg/test/mocks/image_store_mock.go           |  7 ++
 10 files changed, 195 insertions(+), 20 deletions(-)

diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md
index c2c33d17db..779b3875da 100644
--- a/THIRD-PARTY-LICENSES.md
+++ b/THIRD-PARTY-LICENSES.md
@@ -6,7 +6,7 @@ cloud.google.com/go/internal|https://github.com/googleapis/google-cloud-go/blob/
 cloud.google.com/go/storage|https://github.com/googleapis/google-cloud-go/blob/storage/v1.31.0/storage/LICENSE|Apache-2.0
 cloud.google.com/go|https://github.com/googleapis/google-cloud-go/blob/v0.110.7/LICENSE|Apache-2.0
 filippo.io/edwards25519|https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE|BSD-3-Clause
-github.com/99designs/gqlgen|https://github.com/99designs/gqlgen/blob/v0.17.39/LICENSE|MIT
+github.com/99designs/gqlgen|https://github.com/99designs/gqlgen/blob/v0.17.40/LICENSE|MIT
 github.com/AdaLogics/go-fuzz-headers|https://github.com/AdaLogics/go-fuzz-headers/blob/ced1acdcaa24/LICENSE|Apache-2.0
 github.com/AdamKorcz/go-118-fuzz-build|https://github.com/AdamKorcz/go-118-fuzz-build/blob/8075edf89bb0/LICENSE|Apache-2.0
 github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper|https://github.com/AliyunContainerService/ack-ram-tool/blob/pkg/credentials/alibabacloudsdkgo/helper/v0.2.0/pkg/credentials/alibabacloudsdkgo/helper/LICENSE|Apache-2.0
@@ -63,13 +63,13 @@ github.com/aquasecurity/go-version/pkg/part|https://github.com/aquasecurity/go-v
 github.com/aquasecurity/table|https://github.com/aquasecurity/table/blob/v1.8.0/LICENSE|MIT
 github.com/aquasecurity/tml|https://github.com/aquasecurity/tml/blob/v0.6.1/LICENSE|Unlicense
 github.com/asaskevich/govalidator|https://github.com/asaskevich/govalidator/blob/a9d515a09cc2/LICENSE|MIT
-github.com/aws/aws-sdk-go-v2/config|https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.44/config/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go-v2/credentials|https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.42/credentials/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/config|https://github.com/aws/aws-sdk-go-v2/blob/config/v1.19.1/config/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/credentials|https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.43/credentials/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue|https://github.com/aws/aws-sdk-go-v2/blob/feature/dynamodb/attributevalue/v1.10.43/feature/dynamodb/attributevalue/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds|https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.12/feature/ec2/imds/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds|https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.13/feature/ec2/imds/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/internal/configsources|https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.43/internal/configsources/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2|https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.37/internal/endpoints/v2/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go-v2/internal/ini|https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.44/internal/ini/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/internal/ini|https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.45/internal/ini/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/internal/sync/singleflight|https://github.com/aws/aws-sdk-go-v2/blob/v1.21.2/internal/sync/singleflight/LICENSE|BSD-3-Clause
 github.com/aws/aws-sdk-go-v2/service/dynamodb/types|https://github.com/aws/aws-sdk-go-v2/blob/service/dynamodb/v1.23.0/service/dynamodb/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types|https://github.com/aws/aws-sdk-go-v2/blob/service/dynamodbstreams/v1.15.7/service/dynamodbstreams/LICENSE.txt|Apache-2.0
@@ -81,14 +81,14 @@ github.com/aws/aws-sdk-go-v2/service/ecrpublic|https://github.com/aws/aws-sdk-go
 github.com/aws/aws-sdk-go-v2/service/ecr|https://github.com/aws/aws-sdk-go-v2/blob/service/ecr/v1.17.18/service/ecr/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding|https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.9.15/service/internal/accept-encoding/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery|https://github.com/aws/aws-sdk-go-v2/blob/service/internal/endpoint-discovery/v1.7.37/service/internal/endpoint-discovery/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url|https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.36/service/internal/presigned-url/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url|https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.37/service/internal/presigned-url/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2/service/secretsmanager|https://github.com/aws/aws-sdk-go-v2/blob/service/secretsmanager/v1.21.6/service/secretsmanager/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go-v2/service/ssooidc|https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.17.2/service/ssooidc/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go-v2/service/sso|https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.15.1/service/sso/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go-v2/service/sts|https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.23.1/service/sts/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/service/ssooidc|https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.17.3/service/ssooidc/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/service/sso|https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.15.2/service/sso/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go-v2/service/sts|https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.23.2/service/sts/LICENSE.txt|Apache-2.0
 github.com/aws/aws-sdk-go-v2|https://github.com/aws/aws-sdk-go-v2/blob/v1.21.2/LICENSE.txt|Apache-2.0
-github.com/aws/aws-sdk-go/internal/sync/singleflight|https://github.com/aws/aws-sdk-go/blob/v1.46.1/internal/sync/singleflight/LICENSE|BSD-3-Clause
-github.com/aws/aws-sdk-go|https://github.com/aws/aws-sdk-go/blob/v1.46.1/LICENSE.txt|Apache-2.0
+github.com/aws/aws-sdk-go/internal/sync/singleflight|https://github.com/aws/aws-sdk-go/blob/v1.46.7/internal/sync/singleflight/LICENSE|BSD-3-Clause
+github.com/aws/aws-sdk-go|https://github.com/aws/aws-sdk-go/blob/v1.46.7/LICENSE.txt|Apache-2.0
 github.com/aws/smithy-go/internal/sync/singleflight|https://github.com/aws/smithy-go/blob/v1.15.0/internal/sync/singleflight/LICENSE|BSD-3-Clause
 github.com/aws/smithy-go|https://github.com/aws/smithy-go/blob/v1.15.0/LICENSE|Apache-2.0
 github.com/awslabs/amazon-ecr-credential-helper/ecr-login|https://github.com/awslabs/amazon-ecr-credential-helper/blob/396b2034c795/ecr-login/LICENSE|Apache-2.0
@@ -198,7 +198,7 @@ github.com/google/gofuzz|https://github.com/google/gofuzz/blob/v1.2.0/LICENSE|Ap
 github.com/google/licenseclassifier/v2|https://github.com/google/licenseclassifier/blob/v2.0.0/v2/LICENSE|Apache-2.0
 github.com/google/s2a-go|https://github.com/google/s2a-go/blob/v0.1.7/LICENSE.md|Apache-2.0
 github.com/google/shlex|https://github.com/google/shlex/blob/e7afc7fbc510/COPYING|Apache-2.0
-github.com/google/uuid|https://github.com/google/uuid/blob/v1.3.1/LICENSE|BSD-3-Clause
+github.com/google/uuid|https://github.com/google/uuid/blob/v1.4.0/LICENSE|BSD-3-Clause
 github.com/google/wire|https://github.com/google/wire/blob/v0.5.0/LICENSE|Apache-2.0
 github.com/googleapis/enterprise-certificate-proxy/client|https://github.com/googleapis/enterprise-certificate-proxy/blob/v0.3.1/LICENSE|Apache-2.0
 github.com/googleapis/gax-go/v2|https://github.com/googleapis/gax-go/blob/v2.12.0/v2/LICENSE|BSD-3-Clause
@@ -295,7 +295,7 @@ github.com/mpvl/unique|https://github.com/mpvl/unique/blob/cbe035fff7de/LICENSE|
 github.com/munnerz/goautoneg|https://github.com/munnerz/goautoneg/blob/a7dc8b61c822/LICENSE|BSD-3-Clause
 github.com/nmcclain/asn1-ber|https://github.com/nmcclain/asn1-ber/blob/2661553a0484/LICENSE|BSD-3-Clause
 github.com/nmcclain/ldap|https://github.com/nmcclain/ldap/blob/7f8d1e44eeba/LICENSE|BSD-3-Clause
-github.com/notaryproject/notation-core-go|https://github.com/notaryproject/notation-core-go/blob/v1.0.0/LICENSE|Apache-2.0
+github.com/notaryproject/notation-core-go|https://github.com/notaryproject/notation-core-go/blob/v1.0.1/LICENSE|Apache-2.0
 github.com/notaryproject/notation-go|https://github.com/notaryproject/notation-go/blob/v1.0.0/LICENSE|Apache-2.0
 github.com/nozzle/throttler|https://github.com/nozzle/throttler/blob/2ea982251481/LICENSE|Apache-2.0
 github.com/oklog/ulid|https://github.com/oklog/ulid/blob/v1.3.1/LICENSE|Apache-2.0
@@ -392,7 +392,7 @@ github.com/zclconf/go-cty-yaml|https://github.com/zclconf/go-cty-yaml/blob/v1.0.
 github.com/zclconf/go-cty/cty|https://github.com/zclconf/go-cty/blob/v1.13.0/LICENSE|MIT
 github.com/zeebo/errs|https://github.com/zeebo/errs/blob/v1.3.0/LICENSE|MIT
 github.com/zitadel/oidc|https://github.com/zitadel/oidc/blob/v1.13.5/LICENSE|Apache-2.0
-go.etcd.io/bbolt|https://github.com/etcd-io/bbolt/blob/v1.3.7/LICENSE|MIT
+go.etcd.io/bbolt|https://github.com/etcd-io/bbolt/blob/v1.3.8/LICENSE|MIT
 go.mongodb.org/mongo-driver|https://github.com/mongodb/mongo-go-driver/blob/v1.11.3/LICENSE|Apache-2.0
 go.mozilla.org/pkcs7|https://github.com/mozilla-services/pkcs7/blob/33d05740a352/LICENSE|MIT
 go.opencensus.io|https://github.com/census-instrumentation/opencensus-go/blob/v0.24.0/LICENSE|Apache-2.0
@@ -441,7 +441,7 @@ google.golang.org/genproto/googleapis/rpc|https://github.com/googleapis/go-genpr
 google.golang.org/genproto/googleapis/type/expr|https://github.com/googleapis/go-genproto/blob/007df8e322eb/LICENSE|Apache-2.0
 google.golang.org/genproto/googleapis/type|https://github.com/googleapis/go-genproto/blob/007df8e322eb/LICENSE|Apache-2.0
 google.golang.org/genproto/protobuf/field_mask|https://github.com/googleapis/go-genproto/blob/007df8e322eb/LICENSE|Apache-2.0
-google.golang.org/grpc|https://github.com/grpc/grpc-go/blob/v1.58.2/LICENSE|Apache-2.0
+google.golang.org/grpc|https://github.com/grpc/grpc-go/blob/v1.58.3/LICENSE|Apache-2.0
 google.golang.org/protobuf|https://github.com/protocolbuffers/protobuf-go/blob/v1.31.0/LICENSE|BSD-3-Clause
 gopkg.in/cheggaaa/pb.v1|https://github.com/cheggaaa/pb/blob/v1.0.28/LICENSE|BSD-3-Clause
 gopkg.in/go-jose/go-jose.v2/json|https://github.com/go-jose/go-jose/blob/v2.6.1/json/LICENSE|BSD-3-Clause
diff --git a/pkg/api/controller.go b/pkg/api/controller.go
index e958048d77..3bc8a18e7d 100644
--- a/pkg/api/controller.go
+++ b/pkg/api/controller.go
@@ -372,6 +372,10 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
 		ext.EnableMetricsExtension(c.Config, c.Log, c.Config.Storage.RootDirectory)
 		ext.EnableSearchExtension(c.Config, c.StoreController, c.MetaDB, taskScheduler, c.CveScanner, c.Log)
 	}
+	// runs once if metrics are enabled & imagestore is local
+	if c.Config.IsMetricsEnabled() && c.Config.Storage.StorageDriver == nil {
+		c.StoreController.DefaultStore.PopulateStorageMetrics(time.Duration(0), taskScheduler)
+	}
 
 	if c.Config.Storage.SubPaths != nil {
 		for route, storageConfig := range c.Config.Storage.SubPaths {
@@ -396,6 +400,10 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
 			substore := c.StoreController.SubStore[route]
 			if substore != nil {
 				substore.RunDedupeBlobs(time.Duration(0), taskScheduler)
+
+				if c.Config.IsMetricsEnabled() && c.Config.Storage.StorageDriver == nil {
+					substore.PopulateStorageMetrics(time.Duration(0), taskScheduler)
+				}
 			}
 		}
 	}
diff --git a/pkg/extensions/monitoring/common.go b/pkg/extensions/monitoring/common.go
index a04610315d..917e322869 100644
--- a/pkg/extensions/monitoring/common.go
+++ b/pkg/extensions/monitoring/common.go
@@ -16,7 +16,7 @@ type MetricServer interface {
 	IsEnabled() bool
 }
 
-func getDirSize(path string) (int64, error) {
+func GetDirSize(path string) (int64, error) {
 	var size int64
 
 	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
diff --git a/pkg/extensions/monitoring/extension.go b/pkg/extensions/monitoring/extension.go
index a53301a89b..82f78b93ec 100644
--- a/pkg/extensions/monitoring/extension.go
+++ b/pkg/extensions/monitoring/extension.go
@@ -171,7 +171,7 @@ func IncDownloadCounter(ms MetricServer, repo string) {
 func SetStorageUsage(ms MetricServer, rootDir, repo string) {
 	ms.SendMetric(func() {
 		dir := path.Join(rootDir, repo)
-		repoSize, err := getDirSize(dir)
+		repoSize, err := GetDirSize(dir)
 
 		if err == nil {
 			repoStorageBytes.WithLabelValues(repo).Set(float64(repoSize))
diff --git a/pkg/extensions/monitoring/minimal.go b/pkg/extensions/monitoring/minimal.go
index 54efb5d67d..bd38b8c131 100644
--- a/pkg/extensions/monitoring/minimal.go
+++ b/pkg/extensions/monitoring/minimal.go
@@ -486,7 +486,7 @@ func IncUploadCounter(ms MetricServer, repo string) {
 func SetStorageUsage(ms MetricServer, rootDir, repo string) {
 	dir := path.Join(rootDir, repo)
 
-	repoSize, err := getDirSize(dir)
+	repoSize, err := GetDirSize(dir)
 	if err != nil {
 		ms.(*metricServer).log.Error().Err(err).Msg("failed to set storage usage")
 	}
diff --git a/pkg/extensions/monitoring/monitoring_test.go b/pkg/extensions/monitoring/monitoring_test.go
index 026389b6b5..9511518a8d 100644
--- a/pkg/extensions/monitoring/monitoring_test.go
+++ b/pkg/extensions/monitoring/monitoring_test.go
@@ -4,9 +4,12 @@
 package monitoring_test
 
 import (
+	"context"
+	"fmt"
 	"math/rand"
 	"net/http"
 	"os"
+	"path"
 	"testing"
 	"time"
 
@@ -17,6 +20,8 @@ import (
 	"zotregistry.io/zot/pkg/api/config"
 	extconf "zotregistry.io/zot/pkg/extensions/config"
 	"zotregistry.io/zot/pkg/extensions/monitoring"
+	"zotregistry.io/zot/pkg/scheduler"
+	common "zotregistry.io/zot/pkg/storage/common"
 	test "zotregistry.io/zot/pkg/test/common"
 	. "zotregistry.io/zot/pkg/test/image-utils"
 	ociutils "zotregistry.io/zot/pkg/test/oci-utils"
@@ -413,6 +418,70 @@ func TestMetricsAuthorization(t *testing.T) {
 	})
 }
 
+func TestPopulateStorageMetrics(t *testing.T) {
+	Convey("Start a scheduler when metrics enabled", t, func() {
+		port := test.GetFreePort()
+		baseURL := test.GetBaseURL(port)
+		conf := config.New()
+		conf.HTTP.Port = port
+
+		rootDir := t.TempDir()
+
+		conf.Storage.RootDirectory = rootDir
+		conf.Extensions = &extconf.ExtensionConfig{}
+		enabled := true
+		conf.Extensions.Metrics = &extconf.MetricsConfig{
+			BaseConfig: extconf.BaseConfig{Enable: &enabled},
+			Prometheus: &extconf.PrometheusConfig{Path: "/metrics"},
+		}
+
+		ctlr := api.NewController(conf)
+		So(ctlr, ShouldNotBeNil)
+
+		cm := test.NewControllerManager(ctlr)
+		cm.StartAndWait(port)
+		defer cm.StopServer()
+
+		// write a couple of images
+		srcStorageCtlr := ociutils.GetDefaultStoreController(rootDir, ctlr.Log)
+		err := WriteImageToFileSystem(CreateDefaultImage(), "alpine", "0.0.1", srcStorageCtlr)
+		So(err, ShouldBeNil)
+		err = WriteImageToFileSystem(CreateDefaultImage(), "busybox", "0.0.1", srcStorageCtlr)
+		So(err, ShouldBeNil)
+
+		sch := scheduler.NewScheduler(conf, ctlr.Log)
+		ctx, cancel := context.WithCancel(context.Background())
+		sch.RunScheduler(ctx)
+
+		generator := &common.StorageMetricsInitGenerator{
+			ImgStore: ctlr.StoreController.DefaultStore,
+			Metrics:  ctlr.Metrics,
+			Log:      ctlr.Log,
+			MaxDelay: 1, // maximum delay between jobs (each job computes repo's storage size)
+		}
+
+		sch.SubmitGenerator(generator, time.Duration(0), scheduler.LowPriority)
+
+		time.Sleep(5 * time.Second)
+		cancel()
+		alpineSize, err := monitoring.GetDirSize(path.Join(rootDir, "alpine"))
+		So(err, ShouldBeNil)
+		busyboxSize, err := monitoring.GetDirSize(path.Join(rootDir, "busybox"))
+		So(err, ShouldBeNil)
+
+		resp, err := resty.R().Get(baseURL + "/metrics")
+		So(err, ShouldBeNil)
+		So(resp, ShouldNotBeNil)
+		So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+		alpineMetric := fmt.Sprintf("zot_repo_storage_bytes{repo=\"alpine\"} %d", alpineSize)
+		busyboxMetric := fmt.Sprintf("zot_repo_storage_bytes{repo=\"busybox\"} %d", busyboxSize)
+		respStr := string(resp.Body())
+		So(respStr, ShouldContainSubstring, alpineMetric)
+		So(respStr, ShouldContainSubstring, busyboxMetric)
+	})
+}
+
 func generateRandomString() string {
 	//nolint: gosec
 	seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
diff --git a/pkg/storage/common/common.go b/pkg/storage/common/common.go
index 8573b4a6ba..1a3fb35ec3 100644
--- a/pkg/storage/common/common.go
+++ b/pkg/storage/common/common.go
@@ -6,8 +6,10 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"math/rand"
 	"path"
 	"strings"
+	"time"
 
 	"github.com/docker/distribution/registry/storage/driver"
 	godigest "github.com/opencontainers/go-digest"
@@ -18,6 +20,7 @@ import (
 
 	zerr "zotregistry.io/zot/errors"
 	zcommon "zotregistry.io/zot/pkg/common"
+	"zotregistry.io/zot/pkg/extensions/monitoring"
 	zlog "zotregistry.io/zot/pkg/log"
 	"zotregistry.io/zot/pkg/scheduler"
 	storageConstants "zotregistry.io/zot/pkg/storage/constants"
@@ -1052,3 +1055,72 @@ func (dt *dedupeTask) DoWork(ctx context.Context) error {
 
 	return err
 }
+
+type StorageMetricsInitGenerator struct {
+	ImgStore storageTypes.ImageStore
+	done     bool
+	Metrics  monitoring.MetricServer
+	lastRepo string
+	nextRun  time.Time
+	rand     *rand.Rand
+	Log      zlog.Logger
+	MaxDelay int
+}
+
+func (gen *StorageMetricsInitGenerator) Next() (scheduler.Task, error) {
+	if gen.lastRepo == "" && gen.nextRun.IsZero() {
+		gen.rand = rand.New(rand.NewSource(time.Now().UTC().UnixNano())) //nolint: gosec
+	}
+
+	delay := gen.rand.Intn(gen.MaxDelay)
+
+	gen.nextRun = time.Now().Add(time.Duration(delay) * time.Second)
+
+	repo, err := gen.ImgStore.GetNextRepository(gen.lastRepo)
+	if err != nil {
+		return nil, err
+	}
+
+	gen.Log.Debug().Str("repo", repo).Int("randomDelay", delay).Msg("StorageMetricsInitGenerator")
+
+	if repo == "" {
+		gen.done = true
+
+		return nil, nil
+	}
+	gen.lastRepo = repo
+
+	return NewStorageMetricsTask(gen.ImgStore, gen.Metrics, repo), nil
+}
+
+func (gen *StorageMetricsInitGenerator) IsDone() bool {
+	return gen.done
+}
+
+func (gen *StorageMetricsInitGenerator) IsReady() bool {
+	return time.Now().After(gen.nextRun)
+}
+
+func (gen *StorageMetricsInitGenerator) Reset() {
+	gen.lastRepo = ""
+	gen.done = false
+	gen.nextRun = time.Time{}
+}
+
+type smTask struct {
+	imgStore storageTypes.ImageStore
+	metrics  monitoring.MetricServer
+	repo     string
+}
+
+func NewStorageMetricsTask(imgStore storageTypes.ImageStore, metrics monitoring.MetricServer, repo string,
+) *smTask {
+	return &smTask{imgStore, metrics, repo}
+}
+
+func (smt *smTask) DoWork(ctx context.Context) error {
+	// run task
+	monitoring.SetStorageUsage(smt.metrics, smt.imgStore.RootDir(), smt.repo)
+
+	return nil
+}
diff --git a/pkg/storage/imagestore/imagestore.go b/pkg/storage/imagestore/imagestore.go
index 2508a59fd3..80cd7be71f 100644
--- a/pkg/storage/imagestore/imagestore.go
+++ b/pkg/storage/imagestore/imagestore.go
@@ -488,7 +488,10 @@ func (is *ImageStore) PutImageManifest(repo, reference, mediaType string, //noli
 		is.Unlock(&lockLatency)
 
 		if err == nil {
-			monitoring.SetStorageUsage(is.metrics, is.rootDir, repo)
+			if is.storeDriver.Name() == storageConstants.LocalStorageDriverName {
+				monitoring.SetStorageUsage(is.metrics, is.rootDir, repo)
+			}
+
 			monitoring.IncUploadCounter(is.metrics, repo)
 		}
 	}()
@@ -621,7 +624,11 @@ func (is *ImageStore) DeleteImageManifest(repo, reference string, detectCollisio
 }
 
 func (is *ImageStore) deleteImageManifest(repo, reference string, detectCollisions bool) error {
-	defer monitoring.SetStorageUsage(is.metrics, is.rootDir, repo)
+	defer func() {
+		if is.storeDriver.Name() == storageConstants.LocalStorageDriverName {
+			monitoring.SetStorageUsage(is.metrics, is.rootDir, repo)
+		}
+	}()
 
 	index, err := common.GetIndex(is, repo, is.log)
 	if err != nil {
@@ -1929,6 +1936,17 @@ func (is *ImageStore) RunDedupeBlobs(interval time.Duration, sch *scheduler.Sche
 	sch.SubmitGenerator(generator, interval, scheduler.MediumPriority)
 }
 
+func (is *ImageStore) PopulateStorageMetrics(interval time.Duration, sch *scheduler.Scheduler) {
+	generator := &common.StorageMetricsInitGenerator{
+		ImgStore: is,
+		Metrics:  is.metrics,
+		Log:      is.log,
+		MaxDelay: 15, //nolint:gomnd
+	}
+
+	sch.SubmitGenerator(generator, interval, scheduler.LowPriority)
+}
+
 type blobStream struct {
 	reader io.Reader
 	closer io.Closer
diff --git a/pkg/storage/types/types.go b/pkg/storage/types/types.go
index 4f7c364120..03f7648918 100644
--- a/pkg/storage/types/types.go
+++ b/pkg/storage/types/types.go
@@ -61,6 +61,7 @@ type ImageStore interface { //nolint:interfacebloat
 	RunDedupeForDigest(digest godigest.Digest, dedupe bool, duplicateBlobs []string) error
 	GetNextDigestWithBlobPaths(repos []string, lastDigests []godigest.Digest) (godigest.Digest, []string, error)
 	GetAllBlobs(repo string) ([]string, error)
+	PopulateStorageMetrics(interval time.Duration, sch *scheduler.Scheduler)
 }
 
 type Driver interface { //nolint:interfacebloat
diff --git a/pkg/test/mocks/image_store_mock.go b/pkg/test/mocks/image_store_mock.go
index e5eda2c5c4..5451c10bf0 100644
--- a/pkg/test/mocks/image_store_mock.go
+++ b/pkg/test/mocks/image_store_mock.go
@@ -55,6 +55,7 @@ type MockedImageStore struct {
 	GetAllBlobsFn                func(repo string) ([]string, error)
 	CleanupRepoFn                func(repo string, blobs []godigest.Digest, removeRepo bool) (int, error)
 	PutIndexContentFn            func(repo string, index ispec.Index) error
+	RunStorageMetricsFn          func(interval time.Duration, sch *scheduler.Scheduler)
 }
 
 func (is MockedImageStore) Lock(t *time.Time) {
@@ -405,3 +406,9 @@ func (is MockedImageStore) PutIndexContent(repo string, index ispec.Index) error
 
 	return nil
 }
+
+func (is MockedImageStore) PopulateStorageMetrics(interval time.Duration, sch *scheduler.Scheduler) {
+	if is.RunStorageMetricsFn != nil {
+		is.RunStorageMetricsFn(interval, sch)
+	}
+}