Переглянути джерело

Sth/kcm 3420 (#3158)

Signed-off-by: Sean Holcomb <seanholcomb@gmail.com>
Sean Holcomb 11 місяців тому
батько
коміт
1d083bd98c

+ 60 - 15
modules/collector-source/go.mod

@@ -6,39 +6,74 @@ go 1.24.2
 
 require (
 	github.com/julienschmidt/httprouter v1.3.0
-	github.com/opencost/opencost/core v0.0.0-00010101000000-000000000000
+	github.com/opencost/opencost/core v0.0.0-20250521155634-81d2b597d1bc
 	golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
 	k8s.io/api v0.33.0
 	k8s.io/apimachinery v0.33.0
-	k8s.io/client-go v0.33.0
 	k8s.io/kubelet v0.33.0
 )
 
 require (
+	cloud.google.com/go v0.112.0 // indirect
+	cloud.google.com/go/compute/metadata v0.5.0 // indirect
+	cloud.google.com/go/iam v1.1.6 // indirect
+	cloud.google.com/go/storage v1.36.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.29.10 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.63 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
+	github.com/aws/smithy-go v1.22.2 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
-	github.com/go-openapi/jsonpointer v0.21.0 // indirect
-	github.com/go-openapi/jsonreference v0.20.2 // indirect
-	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/google/gnostic-models v0.6.9 // indirect
-	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
-	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/minio/crc64nvme v1.0.1 // indirect
+	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/minio/minio-go/v7 v7.0.88 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
-	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pelletier/go-toml v1.9.3 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
+	github.com/rs/xid v1.6.0 // indirect
 	github.com/rs/zerolog v1.26.1 // indirect
 	github.com/spf13/afero v1.6.0 // indirect
 	github.com/spf13/cast v1.3.1 // indirect
@@ -47,20 +82,30 @@ require (
 	github.com/spf13/viper v1.8.1 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
+	go.opentelemetry.io/otel v1.33.0 // indirect
+	go.opentelemetry.io/otel/metric v1.33.0 // indirect
+	go.opentelemetry.io/otel/trace v1.33.0 // indirect
+	golang.org/x/crypto v0.36.0 // indirect
 	golang.org/x/net v0.38.0 // indirect
 	golang.org/x/oauth2 v0.27.0 // indirect
+	golang.org/x/sync v0.13.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
-	golang.org/x/term v0.30.0 // indirect
 	golang.org/x/text v0.23.0 // indirect
 	golang.org/x/time v0.9.0 // indirect
+	google.golang.org/api v0.162.0 // indirect
+	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
+	google.golang.org/grpc v1.68.1 // indirect
 	google.golang.org/protobuf v1.36.5 // indirect
-	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
-	k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
 	k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
 	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/randfill v1.0.0 // indirect

+ 145 - 40
modules/collector-source/go.sum

@@ -18,15 +18,21 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
 cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
 cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
 cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
+cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
+cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -36,16 +42,62 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
+cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.10 h1:yNjgjiGBp4GgaJrGythyBXg2wAs+Im9fSWIUwvi1CAc=
+github.com/aws/aws-sdk-go-v2/config v1.29.10/go.mod h1:A0mbLXSdtob/2t59n1X0iMkPQ5d+YzYZB4rwu7SZ7aA=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63 h1:rv1V3kIJ14pdmTu01hwcMJ0WAERensSiD9rEWEBb1Tk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63/go.mod h1:EJj+yDf0txT26Ulo0VWTavBl31hOsaeuMxIHu2m0suY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 h1:wK8O+j2dOolmpNVY1EWIbLgxrGCHJKVPm08Hv/u80M8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -53,15 +105,18 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
+github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
-github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -69,7 +124,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
+github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
@@ -79,27 +138,26 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
-github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
-github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
-github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
-github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
-github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
-github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -125,10 +183,10 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
-github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -144,9 +202,12 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -158,23 +219,30 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
-github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
 github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -191,8 +259,6 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -201,24 +267,36 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
+github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs=
+github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -236,15 +314,13 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
-github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
-github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
-github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
-github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -253,11 +329,19 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
+github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
+github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
 github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -277,8 +361,6 @@ github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
-github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -310,6 +392,22 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
+go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
+go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
+go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
+go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
+go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
@@ -321,6 +419,8 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -422,6 +522,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -466,11 +568,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -538,12 +639,12 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
-golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
-golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -566,6 +667,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR
 google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
 google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
 google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps=
+google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -614,6 +717,12 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -634,6 +743,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
+google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -653,8 +764,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
-gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -680,12 +789,8 @@ k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
 k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
 k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
 k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
-k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
-k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
-k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
 k8s.io/kubelet v0.33.0 h1:4pJA2Ge6Rp0kDNV76KH7pTBiaV2T1a1874QHMcubuSU=
 k8s.io/kubelet v0.33.0/go.mod h1:iDnxbJQMy9DUNaML5L/WUlt3uJtNLWh7ZAe0JSp4Yi0=
 k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=

+ 10 - 10
modules/collector-source/pkg/collector/config.go

@@ -1,18 +1,18 @@
 package collector
 
 import (
-	"time"
+	"fmt"
 
 	"github.com/opencost/opencost/modules/collector-source/pkg/env"
 	"github.com/opencost/opencost/modules/collector-source/pkg/util"
 )
 
 type CollectorConfig struct {
-	Resolutions    []util.ResolutionConfiguration `json:"resolutions"`
-	ScrapeInterval time.Duration                  `json:"scrape_interval"`
-	ClusterID      string                         `json:"cluster_id"`
-	ReleaseName    string                         `json:"release_name"`
-	NetworkPort    int                            `json:"network_port"`
+	Resolutions      []util.ResolutionConfiguration `json:"resolutions"`
+	ScrapeInterval   string                         `json:"scrape_interval"`
+	ClusterID        string                         `json:"cluster_id"`
+	NetworkPort      int                            `json:"network_port"`
+	BucketConfigFile string                         `json:"bucket_config_file"`
 }
 
 func NewOpenCostCollectorConfigFromEnv() CollectorConfig {
@@ -31,9 +31,9 @@ func NewOpenCostCollectorConfigFromEnv() CollectorConfig {
 				Retention: env.GetCollection1dResolutionRetention(),
 			},
 		},
-		ScrapeInterval: time.Second * time.Duration(env.GetCollectorScrapeIntervalSeconds()),
-		ClusterID:      env.GetClusterID(),
-		ReleaseName:    env.GetReleaseName(),
-		NetworkPort:    env.GetNetworkPort(),
+		ScrapeInterval:   fmt.Sprintf("%ds", env.GetCollectorScrapeIntervalSeconds()),
+		ClusterID:        env.GetClusterID(),
+		NetworkPort:      env.GetNetworkPort(),
+		BucketConfigFile: env.GetExportBucketConfigFile(),
 	}
 }

+ 25 - 13
modules/collector-source/pkg/collector/datasource.go

@@ -1,17 +1,19 @@
 package collector
 
 import (
+	"os"
 	"time"
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/clusters"
 	"github.com/opencost/opencost/core/pkg/diagnostics"
+	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/source"
+	"github.com/opencost/opencost/core/pkg/storage"
 	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/scrape"
 	"github.com/opencost/opencost/modules/collector-source/pkg/util"
-	"k8s.io/client-go/kubernetes"
 )
 
 type collectorDataSource struct {
@@ -24,7 +26,6 @@ type collectorDataSource struct {
 func NewDefaultCollectorDataSource(
 	clusterInfoProvider clusters.ClusterInfoProvider,
 	clusterCache clustercache.ClusterCache,
-	k8s kubernetes.Interface,
 	statSummaryClient util.StatSummaryClient,
 ) source.OpenCostDataSource {
 	config := NewOpenCostCollectorConfigFromEnv()
@@ -32,7 +33,6 @@ func NewDefaultCollectorDataSource(
 		config,
 		clusterInfoProvider,
 		clusterCache,
-		k8s,
 		statSummaryClient,
 	)
 }
@@ -41,24 +41,33 @@ func NewCollectorDataSource(
 	config CollectorConfig,
 	clusterInfoProvider clusters.ClusterInfoProvider,
 	clusterCache clustercache.ClusterCache,
-	k8s kubernetes.Interface,
 	statSummaryClient util.StatSummaryClient,
 ) source.OpenCostDataSource {
+	var store storage.Storage
+	if config.BucketConfigFile != "" {
+		bucketConfig, err := os.ReadFile(config.BucketConfigFile)
+		if err != nil {
+			log.Errorf("Failed to initialize bucket output storage, please check your configuration and bucket security settings: %s", err)
+		} else {
+			store, err = storage.NewBucketStorage(bucketConfig)
+			if err != nil {
+				log.Errorf("Failed to create bucket storage, please check your configuration and bucket security settings: %s", err)
+			}
+		}
+	}
 
-	var storeFactory metric.MetricStoreFactory
-	storeFactory = NewOpenCostMetricStore
-
-	repo := metric.NewMetricRepository(metric.RepositoryConfig{
-		Resolutions: config.Resolutions,
-	}, storeFactory)
+	repo := metric.NewMetricRepository(
+		config.ClusterID,
+		config.Resolutions,
+		store,
+		NewOpenCostMetricStore,
+	)
 
 	scrapeController := scrape.NewScrapeController(
 		config.ScrapeInterval,
-		config.ReleaseName,
 		config.NetworkPort,
 		repo,
 		clusterCache,
-		k8s,
 		statSummaryClient,
 	)
 	scrapeController.Start()
@@ -104,5 +113,8 @@ func (c *collectorDataSource) BatchDuration() time.Duration {
 }
 
 func (c *collectorDataSource) Resolution() time.Duration {
-	return c.config.ScrapeInterval
+	interval, _ := util.NewInterval(c.config.ScrapeInterval)
+	current := interval.Truncate(time.Now().UTC())
+	next := interval.Add(current, 1)
+	return next.Sub(current)
 }

+ 5 - 5
modules/collector-source/pkg/env/collectorenv.go

@@ -6,22 +6,18 @@ import (
 
 const (
 	ClusterIDEnvVar                 = "CLUSTER_ID"
-	ReleaseNameEnvVar               = "RELEASE_NAME"
 	NetworkPortEnvVar               = "NETWORK_PORT"
 	Collector10mResolutionRetention = "COLLECTOR_10M_RESOLUTION_RETENTION"
 	Collector1hResolutionRetention  = "COLLECTOR_1H_RESOLUTION_RETENTION"
 	Collection1dResolutionRetention = "COLLECTOR_1D_RESOLUTION_RETENTION"
 	CollectorScrapeIntervalSeconds  = "COLLECTOR_SCRAPE_INTERVAL_SECONDS"
+	ExportBucketConfigFileEnvVar    = "EXPORT_BUCKET_CONFIG_FILE"
 )
 
 func GetClusterID() string {
 	return env.Get(ClusterIDEnvVar, "")
 }
 
-func GetReleaseName() string {
-	return env.Get(ReleaseNameEnvVar, "kubecost")
-}
-
 func GetNetworkPort() int {
 	return env.GetInt(NetworkPortEnvVar, 3001)
 }
@@ -41,3 +37,7 @@ func GetCollection1dResolutionRetention() int {
 func GetCollectorScrapeIntervalSeconds() int {
 	return env.GetInt(CollectorScrapeIntervalSeconds, 30)
 }
+
+func GetExportBucketConfigFile() string {
+	return env.Get(ExportBucketConfigFileEnvVar, "")
+}

+ 115 - 19
modules/collector-source/pkg/metric/repository.go

@@ -2,15 +2,23 @@ package metric
 
 import (
 	"fmt"
+	"path"
+	"sort"
+	"strings"
 	"sync"
 	"time"
 
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/exporter/pathing"
 	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/json"
 	"github.com/opencost/opencost/modules/collector-source/pkg/util"
 )
 
+const ControllerEventName = "controller"
+
 type RepositoryConfig struct {
-	Resolutions []util.ResolutionConfiguration
 }
 
 // MetricRepository is an MetricUpdater which applies calls to update to all resolutions being tracked. It holds the
@@ -18,24 +26,98 @@ type RepositoryConfig struct {
 type MetricRepository struct {
 	lock             sync.Mutex
 	resolutionStores map[string]*resolutionStores
+	exporter         exporter.EventExporter[UpdateSet]
 }
 
-func NewMetricRepository(config RepositoryConfig, factory MetricStoreFactory) *MetricRepository {
+func NewMetricRepository(
+	clusterID string,
+	resolutions []util.ResolutionConfiguration,
+	store storage.Storage,
+	storeFactory MetricStoreFactory,
+) *MetricRepository {
 	resoluationCollectors := make(map[string]*resolutionStores)
-
-	for _, resConf := range config.Resolutions {
-		resCollector, err := newResolutionStores(resConf, factory)
+	for _, resconf := range resolutions {
+		resolution, err := util.NewResolution(resconf)
+		if err != nil {
+			log.Errorf("failed to create resolution %s", err.Error())
+		}
+		resCollector, err := newResolutionStores(resolution, storeFactory)
 		if err != nil {
 			log.Errorf("NewMetricRepository: failed to init resolution metric: %s", err.Error())
 			continue
 		}
-		resoluationCollectors[resConf.Interval] = resCollector
+		resoluationCollectors[resolution.Interval()] = resCollector
 	}
 
 	repo := &MetricRepository{
 		resolutionStores: resoluationCollectors,
 	}
 
+	if store != nil {
+		pathFormatter, err := pathing.NewEventStoragePathFormatter("", clusterID, ControllerEventName)
+		if err != nil {
+			log.Errorf("filed to create path formatter for scrape controller: %s", err.Error())
+			return repo
+		}
+		encoder := exporter.NewJSONEncoder[UpdateSet]()
+		repo.exporter = exporter.NewEventStorageExporter(
+			pathFormatter,
+			encoder,
+			store,
+		)
+		// attempt to restore state from files
+		// get path of saved files
+		dirPath := path.Dir(pathFormatter.ToFullPath("", time.Time{}, ""))
+		files, err := store.List(dirPath)
+		if err != nil {
+			log.Errorf("failed to list files in scrape controller: %s", err.Error())
+		}
+		// find oldest limit
+		limit := time.Now().UTC()
+		for _, resStore := range repo.resolutionStores {
+			if limit.After(resStore.resolution.Limit()) {
+				limit = resStore.resolution.Limit()
+			}
+		}
+
+		// find files that are within limit
+		var filesToRun []string
+		for _, file := range files {
+			fileName := path.Base(file.Name)
+			timeString := strings.TrimSuffix(fileName, "."+encoder.FileExt())
+			timestamp, err := time.Parse(pathing.EventStorageTimeFormat, timeString)
+			if err != nil {
+				log.Errorf("failed to parse fileName %s: %s", fileName, err.Error())
+				continue
+			}
+			if timestamp.After(limit) {
+				filesToRun = append(filesToRun, pathFormatter.ToFullPath("", timestamp, encoder.FileExt()))
+			}
+		}
+
+		// sort files
+		sort.Strings(filesToRun)
+
+		// open files and run updates
+		for _, fileName := range filesToRun {
+			b, err := store.Read(fileName)
+			if err != nil {
+				log.Errorf("failed to load file contents for '%s': %s", fileName, err.Error())
+				continue
+			}
+			updateSet := UpdateSet{}
+			err = json.Unmarshal(b, &updateSet)
+			if err != nil {
+				log.Errorf("failed to unmarshal file %s: %s", fileName, err.Error())
+				continue
+			}
+			filePrefix := path.Base(fileName)
+			timeString := strings.TrimSuffix(filePrefix, "."+encoder.FileExt())
+			timestamp, err := time.Parse(pathing.EventStorageTimeFormat, timeString)
+			repo.Update(updateSet.Updates, timestamp)
+		}
+	}
+
 	return repo
 }
 
@@ -53,21 +135,40 @@ func (r *MetricRepository) GetCollector(interval string, t time.Time) (MetricSto
 
 // Update calls Update on the collectors for each resolution
 func (r *MetricRepository) Update(
-	metricName string,
-	labels map[string]string,
-	value float64,
+	updates []Update,
 	timestamp time.Time,
-	additionalInformation map[string]string,
 ) {
 	r.lock.Lock()
 	defer r.lock.Unlock()
 
-	// Call update on the collectors for each resolution
-	for _, resCollector := range r.resolutionStores {
-		resCollector.update(metricName, labels, value, timestamp, additionalInformation)
+	for _, update := range updates {
+		// Call update on the collectors for each resolution
+		for _, resCollector := range r.resolutionStores {
+			resCollector.update(update.Name, update.Labels, update.Value, timestamp, update.AdditionalInfo)
+		}
+	}
+
+	if r.exporter != nil {
+		err := r.exporter.Export(timestamp, &UpdateSet{
+			Updates: updates,
+		})
+		if err != nil {
+			log.Errorf("failed to export update results: %s", err.Error())
+		}
 	}
 }
 
+type UpdateSet struct {
+	Updates []Update `json:"updates"`
+}
+
+type Update struct {
+	Name           string            `json:"name"`
+	Labels         map[string]string `json:"labels"`
+	Value          float64           `json:"value"`
+	AdditionalInfo map[string]string `json:"additionalInfo"`
+}
+
 func (r *MetricRepository) Coverage() map[string][]time.Time {
 	r.lock.Lock()
 	defer r.lock.Unlock()
@@ -90,12 +191,7 @@ type resolutionStores struct {
 	factory    func() MetricStore
 }
 
-func newResolutionStores(resConf util.ResolutionConfiguration, factory MetricStoreFactory) (*resolutionStores, error) {
-	resolution, err := util.NewResolution(resConf)
-	if err != nil {
-		return nil, fmt.Errorf("NewResolutionCollectors: %w", err)
-	}
-
+func newResolutionStores(resolution *util.Resolution, factory MetricStoreFactory) (*resolutionStores, error) {
 	resCol := &resolutionStores{
 		resolution: resolution,
 		collectors: map[int64]MetricStore{},

+ 150 - 0
modules/collector-source/pkg/metric/repository_test.go

@@ -0,0 +1,150 @@
+package metric
+
+import (
+	"os"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
+	"github.com/opencost/opencost/modules/collector-source/pkg/metric/aggregator"
+	"github.com/opencost/opencost/modules/collector-source/pkg/util"
+)
+
+const TestActiveMinutesID = "TestActiveMinutes"
+const TestAverageID = "TestAverage"
+const TestMetric = "test_metric"
+
+func testMetricCollector() MetricStore {
+	memStore := NewInMemoryMetricStore()
+
+	memStore.Register(NewMetricCollector(
+		TestActiveMinutesID,
+		TestMetric,
+		[]string{
+			"test",
+		},
+		aggregator.ActiveMinutes,
+		nil,
+	))
+
+	memStore.Register(NewMetricCollector(
+		TestAverageID,
+		TestMetric,
+		[]string{
+			"test",
+		},
+		aggregator.AverageOverTime,
+		nil,
+	))
+
+	return memStore
+}
+
+func TestNewMetricRepository_DisasterRecovery(t *testing.T) {
+	time3 := time.Now().UTC().Truncate(timeutil.Day)
+	time2 := time3.Add(-12 * time.Hour)
+	time1 := time3.Add(-timeutil.Day)
+	dirPath := os.TempDir()
+	defer os.RemoveAll(dirPath)
+	store := storage.NewFileStorage(dirPath)
+	repo := NewMetricRepository(
+		"test",
+		[]util.ResolutionConfiguration{
+			{
+				Interval:  "1d",
+				Retention: 3,
+			},
+		},
+		store,
+		testMetricCollector,
+	)
+	inputUpdateSet1 := UpdateSet{
+		Updates: []Update{
+			{
+				Name: TestMetric,
+				Labels: map[string]string{
+					"test": "test",
+				},
+				Value:          1,
+				AdditionalInfo: nil,
+			},
+		},
+	}
+
+	inputUpdateSet2 := UpdateSet{
+		Updates: []Update{
+			{
+				Name: TestMetric,
+				Labels: map[string]string{
+					"test": "test",
+				},
+				Value:          2,
+				AdditionalInfo: nil,
+			},
+		},
+	}
+
+	inputUpdateSet3 := UpdateSet{
+		Updates: []Update{
+			{
+				Name: TestMetric,
+				Labels: map[string]string{
+					"test": "test",
+				},
+				Value:          3,
+				AdditionalInfo: nil,
+			},
+		},
+	}
+
+	repo.Update(inputUpdateSet1.Updates, time1)
+	repo.Update(inputUpdateSet2.Updates, time2)
+	repo.Update(inputUpdateSet3.Updates, time3)
+
+	repo2 := NewMetricRepository(
+		"test",
+		[]util.ResolutionConfiguration{
+			{
+				Interval:  "1d",
+				Retention: 3,
+			},
+		},
+		store,
+		testMetricCollector,
+	)
+
+	collector1, err := repo.GetCollector("1d", time3)
+	if err != nil {
+		t.Fatalf("failed to get collector from repo1: %s", err.Error())
+	}
+	activeMinutesRes1, err := collector1.Query(TestActiveMinutesID)
+	if err != nil {
+		t.Fatalf("failed to query %s from repo1: %s", TestActiveMinutesID, err.Error())
+	}
+	averageRes1, err := collector1.Query(TestAverageID)
+	if err != nil {
+		t.Fatalf("failed to query %s from repo1: %s", TestAverageID, err.Error())
+	}
+
+	collector2, err := repo2.GetCollector("1d", time3)
+	if err != nil {
+		t.Fatalf("failed to get collector from repo2: %s", err.Error())
+	}
+	activeMinutesRes2, err := collector2.Query(TestActiveMinutesID)
+	if err != nil {
+		t.Fatalf("failed to query %s from repo2: %s", TestActiveMinutesID, err.Error())
+	}
+	averageRes2, err := collector2.Query(TestAverageID)
+	if err != nil {
+		t.Fatalf("failed to query %s from repo2: %s", TestAverageID, err.Error())
+	}
+
+	if !reflect.DeepEqual(activeMinutesRes1, activeMinutesRes2) {
+		t.Errorf("active minute query results did not match 1: %v, 2: %v", activeMinutesRes1, activeMinutesRes2)
+	}
+	if !reflect.DeepEqual(averageRes1, averageRes2) {
+		t.Errorf("average query results did not match 1: %v, 2: %v", averageRes1, averageRes2)
+	}
+}

+ 4 - 1
modules/collector-source/pkg/metric/store.go

@@ -23,7 +23,10 @@ type MetricStore interface {
 	// Query accepts a `MetricCollectorID` and returns a slice of `MetricResult` instances for that metric.
 	Query(collectorID MetricCollectorID) ([]*aggregator.MetricResult, error)
 
-	MetricUpdater
+	// Update accepts the name of a metric, the label set and values to update the metric, the updated Value, and a Timestamp.
+	// This method does not accept a `MetricCollectorID` because it provides updates across many potential MetricCollector instances
+	// which utilize the same metric.
+	Update(metricName string, labels map[string]string, value float64, timestamp time.Time, additionalInformation map[string]string)
 }
 
 type MetricStoreFactory func() MetricStore

+ 0 - 71
modules/collector-source/pkg/metric/updater.go

@@ -1,71 +0,0 @@
-package metric
-
-import (
-	"fmt"
-	"time"
-
-	"golang.org/x/exp/maps"
-)
-
-type MetricUpdater interface {
-	// Update accepts the name of a metric, the label set and values to update the metric, the updated Value, and a Timestamp.
-	// This method does not accept a `MetricCollectorID` because it provides updates across many potential MetricCollector instances
-	// which utilize the same metric.
-	Update(metricName string, labels map[string]string, value float64, timestamp time.Time, additionalInformation map[string]string)
-}
-
-// ArgRecordUpdater is a mock MetricStore which records the arguments passed to the update function in an array
-type ArgRecordUpdater struct {
-	UpdateArgs []UpdateArgs
-}
-
-func (u *ArgRecordUpdater) Update(metricName string, labels map[string]string, value float64, timestamp time.Time, additionalInformation map[string]string) {
-	u.UpdateArgs = append(u.UpdateArgs, UpdateArgs{
-		MetricName:            metricName,
-		Labels:                labels,
-		Value:                 value,
-		Timestamp:             timestamp,
-		AdditionalInformation: additionalInformation,
-	})
-}
-
-type UpdateArgs struct {
-	MetricName            string
-	Labels                map[string]string
-	Value                 float64
-	Timestamp             time.Time
-	AdditionalInformation map[string]string
-}
-
-func (u UpdateArgs) Equals(that UpdateArgs) error {
-	err := u.ValueEquals(that)
-	if err != nil {
-		return err
-	}
-
-	if !u.Timestamp.Equal(that.Timestamp) {
-		return fmt.Errorf("expected Timestamp %s, got %s", u.Timestamp, that.Timestamp)
-	}
-
-	return nil
-}
-
-func (u UpdateArgs) ValueEquals(that UpdateArgs) error {
-	if u.MetricName != that.MetricName {
-		return fmt.Errorf("expected metric name %s, got %s", u.MetricName, that.MetricName)
-	}
-
-	if !maps.Equal(u.Labels, that.Labels) {
-		return fmt.Errorf("expected Labels %s, got %s", u.Labels, that.Labels)
-	}
-
-	if u.Value != that.Value {
-		return fmt.Errorf("expected Value %f, got %f", u.Value, that.Value)
-	}
-
-	if !maps.Equal(u.AdditionalInformation, that.AdditionalInformation) {
-		return fmt.Errorf("expected AdditionalInformation %v, got %v", u.AdditionalInformation, that.AdditionalInformation)
-	}
-
-	return nil
-}

+ 194 - 55
modules/collector-source/pkg/scrape/clustercache.go

@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"slices"
 	"strings"
-	"time"
 
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/log"
@@ -44,40 +43,37 @@ const (
 
 type ClusterCacheScraper struct {
 	clusterCache clustercache.ClusterCache
-	updater      metric.MetricUpdater
 }
 
-func newClusterCacheScraper(clusterCache clustercache.ClusterCache, updater metric.MetricUpdater) Scraper {
+func newClusterCacheScraper(clusterCache clustercache.ClusterCache) Scraper {
 	return &ClusterCacheScraper{
 		clusterCache: clusterCache,
-		updater:      updater,
 	}
 }
 
-func (ccs *ClusterCacheScraper) Scrape() {
-	timestamp := time.Now().UTC()
-	nodes := ccs.clusterCache.GetAllNodes()
-	deployments := ccs.clusterCache.GetAllDeployments()
-	namespaces := ccs.clusterCache.GetAllNamespaces()
-	pods := ccs.clusterCache.GetAllPods()
-	pvcs := ccs.clusterCache.GetAllPersistentVolumeClaims()
-	pvs := ccs.clusterCache.GetAllPersistentVolumes()
-	services := ccs.clusterCache.GetAllServices()
-	statefulSets := ccs.clusterCache.GetAllStatefulSets()
-	replicaSets := ccs.clusterCache.GetAllReplicaSets()
+func (ccs *ClusterCacheScraper) Scrape() []metric.Update {
+	scrapeFuncs := []ScrapeFunc{
+		ccs.ScrapeNodes,
+		ccs.ScrapeDeployments,
+		ccs.ScrapeNamespaces,
+		ccs.ScrapePods,
+		ccs.ScrapePVCs,
+		ccs.ScrapePVs,
+		ccs.ScrapeServices,
+		ccs.ScrapeStatefulSets,
+		ccs.ScrapeReplicaSets,
+	}
+	return concurrentScrape(scrapeFuncs...)
+}
 
-	ccs.scrapeNodes(nodes, timestamp)
-	ccs.scrapeDeployments(deployments, timestamp)
-	ccs.scrapeNamespaces(namespaces, timestamp)
-	ccs.scrapePods(pods, timestamp)
-	ccs.scrapePVCs(pvcs, timestamp)
-	ccs.scrapePVs(pvs, timestamp)
-	ccs.scrapeServices(services, timestamp)
-	ccs.scrapeStatefulSets(statefulSets, timestamp)
-	ccs.scrapeReplicaSets(replicaSets, timestamp)
+func (ccs *ClusterCacheScraper) ScrapeNodes() []metric.Update {
+	nodes := ccs.clusterCache.GetAllNodes()
+	return ccs.scrapeNodes(nodes)
 }
 
-func (ccs *ClusterCacheScraper) scrapeNodes(nodes []*clustercache.Node, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) scrapeNodes(nodes []*clustercache.Node) []metric.Update {
+	var scrapeResults []metric.Update
+
 	for _, node := range nodes {
 		nodeInfo := map[string]string{
 			source.NodeLabel:       node.Name,
@@ -88,12 +84,20 @@ func (ccs *ClusterCacheScraper) scrapeNodes(nodes []*clustercache.Node, timestam
 		if node.Status.Capacity != nil {
 			if quantity, ok := node.Status.Capacity[v1.ResourceCPU]; ok {
 				_, _, value := toResourceUnitValue(v1.ResourceCPU, quantity)
-				ccs.updater.Update(KubeNodeStatusCapacityCPUCores, nodeInfo, value, timestamp, nil)
+				scrapeResults = append(scrapeResults, metric.Update{
+					Name:   KubeNodeStatusCapacityCPUCores,
+					Labels: nodeInfo,
+					Value:  value,
+				})
 			}
 
 			if quantity, ok := node.Status.Capacity[v1.ResourceMemory]; ok {
 				_, _, value := toResourceUnitValue(v1.ResourceMemory, quantity)
-				ccs.updater.Update(KubeNodeStatusCapacityMemoryBytes, nodeInfo, value, timestamp, nil)
+				scrapeResults = append(scrapeResults, metric.Update{
+					Name:   KubeNodeStatusCapacityMemoryBytes,
+					Labels: nodeInfo,
+					Value:  value,
+				})
 			}
 		}
 
@@ -101,12 +105,20 @@ func (ccs *ClusterCacheScraper) scrapeNodes(nodes []*clustercache.Node, timestam
 		if node.Status.Allocatable != nil {
 			if quantity, ok := node.Status.Allocatable[v1.ResourceCPU]; ok {
 				_, _, value := toResourceUnitValue(v1.ResourceCPU, quantity)
-				ccs.updater.Update(KubeNodeStatusAllocatableCPUCores, nodeInfo, value, timestamp, nil)
+				scrapeResults = append(scrapeResults, metric.Update{
+					Name:   KubeNodeStatusAllocatableCPUCores,
+					Labels: nodeInfo,
+					Value:  value,
+				})
 			}
 
 			if quantity, ok := node.Status.Allocatable[v1.ResourceMemory]; ok {
 				_, _, value := toResourceUnitValue(v1.ResourceMemory, quantity)
-				ccs.updater.Update(KubeNodeStatusAllocatableMemoryBytes, nodeInfo, value, timestamp, nil)
+				scrapeResults = append(scrapeResults, metric.Update{
+					Name:   KubeNodeStatusAllocatableMemoryBytes,
+					Labels: nodeInfo,
+					Value:  value,
+				})
 			}
 		}
 
@@ -114,12 +126,24 @@ func (ccs *ClusterCacheScraper) scrapeNodes(nodes []*clustercache.Node, timestam
 		labelNames, labelValues := promutil.KubeLabelsToLabels(node.Labels)
 		nodeLabels := util.ToMap(labelNames, labelValues)
 
-		ccs.updater.Update(KubeNodeLabels, nodeInfo, 0, timestamp, nodeLabels)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           KubeNodeLabels,
+			Labels:         nodeInfo,
+			Value:          0,
+			AdditionalInfo: nodeLabels,
+		})
 
 	}
+	return scrapeResults
+}
+
+func (ccs *ClusterCacheScraper) ScrapeDeployments() []metric.Update {
+	deployments := ccs.clusterCache.GetAllDeployments()
+	return ccs.scrapeDeployments(deployments)
 }
 
-func (ccs *ClusterCacheScraper) scrapeDeployments(deployments []*clustercache.Deployment, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) scrapeDeployments(deployments []*clustercache.Deployment) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, deployment := range deployments {
 		deploymentInfo := map[string]string{
 			source.DeploymentLabel: deployment.Name,
@@ -130,12 +154,23 @@ func (ccs *ClusterCacheScraper) scrapeDeployments(deployments []*clustercache.De
 		labelNames, labelValues := promutil.KubeLabelsToLabels(deployment.MatchLabels)
 		deploymentLabels := util.ToMap(labelNames, labelValues)
 
-		ccs.updater.Update(DeploymentMatchLabels, deploymentInfo, 0, timestamp, deploymentLabels)
-
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           DeploymentMatchLabels,
+			Labels:         deploymentInfo,
+			Value:          0,
+			AdditionalInfo: deploymentLabels,
+		})
 	}
+	return scrapeResults
+}
+
+func (ccs *ClusterCacheScraper) ScrapeNamespaces() []metric.Update {
+	namespaces := ccs.clusterCache.GetAllNamespaces()
+	return ccs.scrapeNamespaces(namespaces)
 }
 
-func (ccs *ClusterCacheScraper) scrapeNamespaces(namespaces []*clustercache.Namespace, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) scrapeNamespaces(namespaces []*clustercache.Namespace) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, namespace := range namespaces {
 		namespaceInfo := map[string]string{
 			source.NamespaceLabel: namespace.Name,
@@ -144,16 +179,33 @@ func (ccs *ClusterCacheScraper) scrapeNamespaces(namespaces []*clustercache.Name
 		// namespace labels
 		labelNames, labelValues := promutil.KubeLabelsToLabels(namespace.Labels)
 		namespaceLabels := util.ToMap(labelNames, labelValues)
-		ccs.updater.Update(KubeNamespaceLabels, namespaceInfo, 0, timestamp, namespaceLabels)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           KubeNamespaceLabels,
+			Labels:         namespaceInfo,
+			Value:          0,
+			AdditionalInfo: namespaceLabels,
+		})
 
 		// namespace annotations
 		annotationNames, annotationValues := promutil.KubeAnnotationsToLabels(namespace.Annotations)
 		namespaceAnnotations := util.ToMap(annotationNames, annotationValues)
-		ccs.updater.Update(KubeNamespaceAnnotations, namespaceInfo, 0, timestamp, namespaceAnnotations)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           KubeNamespaceAnnotations,
+			Labels:         namespaceInfo,
+			Value:          0,
+			AdditionalInfo: namespaceAnnotations,
+		})
 	}
+	return scrapeResults
 }
 
-func (ccs *ClusterCacheScraper) scrapePods(pods []*clustercache.Pod, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) ScrapePods() []metric.Update {
+	pods := ccs.clusterCache.GetAllPods()
+	return ccs.scrapePods(pods)
+}
+
+func (ccs *ClusterCacheScraper) scrapePods(pods []*clustercache.Pod) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, pod := range pods {
 		podInfo := map[string]string{
 			source.PodLabel:       pod.Name,
@@ -166,19 +218,33 @@ func (ccs *ClusterCacheScraper) scrapePods(pods []*clustercache.Pod, timestamp t
 		// pod labels
 		labelNames, labelValues := promutil.KubeLabelsToLabels(pod.Labels)
 		podLabels := util.ToMap(labelNames, labelValues)
-		ccs.updater.Update(KubePodLabels, podInfo, 0, timestamp, podLabels)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           KubePodLabels,
+			Labels:         podInfo,
+			Value:          0,
+			AdditionalInfo: podLabels,
+		})
 
 		// pod annotations
 		annotationNames, annotationValues := promutil.KubeAnnotationsToLabels(pod.Annotations)
 		podAnnotations := util.ToMap(annotationNames, annotationValues)
-		ccs.updater.Update(KubePodAnnotations, podInfo, 0, timestamp, podAnnotations)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           KubePodAnnotations,
+			Labels:         podInfo,
+			Value:          0,
+			AdditionalInfo: podAnnotations,
+		})
 
 		// Pod owner metric
 		for _, owner := range pod.OwnerReferences {
 			ownerInfo := maps.Clone(podInfo)
 			ownerInfo[source.OwnerKindLabel] = owner.Kind
 			ownerInfo[source.OwnerNameLabel] = owner.Name
-			ccs.updater.Update(KubePodOwner, ownerInfo, 0, timestamp, nil)
+			scrapeResults = append(scrapeResults, metric.Update{
+				Name:   KubePodOwner,
+				Labels: ownerInfo,
+				Value:  0,
+			})
 		}
 
 		// Container Status
@@ -186,7 +252,11 @@ func (ccs *ClusterCacheScraper) scrapePods(pods []*clustercache.Pod, timestamp t
 			if status.State.Running != nil {
 				containerInfo := maps.Clone(podInfo)
 				containerInfo[source.ContainerLabel] = status.Name
-				ccs.updater.Update(KubePodContainerStatusRunning, containerInfo, 0, timestamp, nil)
+				scrapeResults = append(scrapeResults, metric.Update{
+					Name:   KubePodContainerStatusRunning,
+					Labels: containerInfo,
+					Value:  0,
+				})
 			}
 		}
 
@@ -211,14 +281,25 @@ func (ccs *ClusterCacheScraper) scrapePods(pods []*clustercache.Pod, timestamp t
 					resourceRequestInfo := maps.Clone(containerInfo)
 					resourceRequestInfo[source.ResourceLabel] = resource
 					resourceRequestInfo[source.UnitLabel] = unit
-					ccs.updater.Update(KubePodContainerResourceRequests, resourceRequestInfo, value, timestamp, nil)
+					scrapeResults = append(scrapeResults, metric.Update{
+						Name:   KubePodContainerResourceRequests,
+						Labels: resourceRequestInfo,
+						Value:  value,
+					})
 				}
 			}
 		}
 	}
+	return scrapeResults
+}
+
+func (ccs *ClusterCacheScraper) ScrapePVCs() []metric.Update {
+	pvcs := ccs.clusterCache.GetAllPersistentVolumeClaims()
+	return ccs.scrapePVCs(pvcs)
 }
 
-func (ccs *ClusterCacheScraper) scrapePVCs(pvcs []*clustercache.PersistentVolumeClaim, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) scrapePVCs(pvcs []*clustercache.PersistentVolumeClaim) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, pvc := range pvcs {
 		pvcInfo := map[string]string{
 			source.PVCLabel:          pvc.Name,
@@ -227,15 +308,30 @@ func (ccs *ClusterCacheScraper) scrapePVCs(pvcs []*clustercache.PersistentVolume
 			source.StorageClassLabel: getPersistentVolumeClaimClass(pvc),
 		}
 
-		ccs.updater.Update(KubePersistentVolumeClaimInfo, pvcInfo, 0, timestamp, nil)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:   KubePersistentVolumeClaimInfo,
+			Labels: pvcInfo,
+			Value:  0,
+		})
 
 		if storage, ok := pvc.Spec.Resources.Requests[v1.ResourceStorage]; ok {
-			ccs.updater.Update(KubePersistentVolumeClaimResourceRequestsStorageBytes, pvcInfo, float64(storage.Value()), timestamp, nil)
+			scrapeResults = append(scrapeResults, metric.Update{
+				Name:   KubePersistentVolumeClaimResourceRequestsStorageBytes,
+				Labels: pvcInfo,
+				Value:  float64(storage.Value()),
+			})
 		}
 	}
+	return scrapeResults
 }
 
-func (ccs *ClusterCacheScraper) scrapePVs(pvs []*clustercache.PersistentVolume, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) ScrapePVs() []metric.Update {
+	pvs := ccs.clusterCache.GetAllPersistentVolumes()
+	return ccs.scrapePVs(pvs)
+}
+
+func (ccs *ClusterCacheScraper) scrapePVs(pvs []*clustercache.PersistentVolume) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, pv := range pvs {
 		providerID := pv.Name
 		// if a more accurate provider ID is available, use that
@@ -248,15 +344,30 @@ func (ccs *ClusterCacheScraper) scrapePVs(pvs []*clustercache.PersistentVolume,
 			source.ProviderIDLabel:   providerID,
 		}
 
-		ccs.updater.Update(KubecostPVInfo, pvInfo, 0, timestamp, nil)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:   KubecostPVInfo,
+			Labels: pvInfo,
+			Value:  0,
+		})
 
 		if storage, ok := pv.Spec.Capacity[v1.ResourceStorage]; ok {
-			ccs.updater.Update(KubePersistentVolumeCapacityBytes, pvInfo, float64(storage.Value()), timestamp, nil)
+			scrapeResults = append(scrapeResults, metric.Update{
+				Name:   KubePersistentVolumeCapacityBytes,
+				Labels: pvInfo,
+				Value:  float64(storage.Value()),
+			})
 		}
 	}
+	return scrapeResults
 }
 
-func (ccs *ClusterCacheScraper) scrapeServices(services []*clustercache.Service, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) ScrapeServices() []metric.Update {
+	services := ccs.clusterCache.GetAllServices()
+	return ccs.scrapeServices(services)
+}
+
+func (ccs *ClusterCacheScraper) scrapeServices(services []*clustercache.Service) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, service := range services {
 		serviceInfo := map[string]string{
 			source.ServiceLabel:   service.Name,
@@ -266,12 +377,24 @@ func (ccs *ClusterCacheScraper) scrapeServices(services []*clustercache.Service,
 		// service labels
 		labelNames, labelValues := promutil.KubeLabelsToLabels(service.SpecSelector)
 		serviceLabels := util.ToMap(labelNames, labelValues)
-		ccs.updater.Update(ServiceSelectorLabels, serviceInfo, 0, timestamp, serviceLabels)
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           ServiceSelectorLabels,
+			Labels:         serviceInfo,
+			Value:          0,
+			AdditionalInfo: serviceLabels,
+		})
 
 	}
+	return scrapeResults
+}
+
+func (ccs *ClusterCacheScraper) ScrapeStatefulSets() []metric.Update {
+	statefulSets := ccs.clusterCache.GetAllStatefulSets()
+	return ccs.scrapeStatefulSets(statefulSets)
 }
 
-func (ccs *ClusterCacheScraper) scrapeStatefulSets(statefulSets []*clustercache.StatefulSet, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) scrapeStatefulSets(statefulSets []*clustercache.StatefulSet) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, statefulSet := range statefulSets {
 		statefulSetInfo := map[string]string{
 			source.StatefulSetLabel: statefulSet.Name,
@@ -281,12 +404,23 @@ func (ccs *ClusterCacheScraper) scrapeStatefulSets(statefulSets []*clustercache.
 		// statefulSet labels
 		labelNames, labelValues := promutil.KubeLabelsToLabels(statefulSet.SpecSelector.MatchLabels)
 		statefulSetLabels := util.ToMap(labelNames, labelValues)
-		ccs.updater.Update(StatefulSetMatchLabels, statefulSetInfo, 0, timestamp, statefulSetLabels)
-
+		scrapeResults = append(scrapeResults, metric.Update{
+			Name:           StatefulSetMatchLabels,
+			Labels:         statefulSetInfo,
+			Value:          0,
+			AdditionalInfo: statefulSetLabels,
+		})
 	}
+	return scrapeResults
+}
+
+func (ccs *ClusterCacheScraper) ScrapeReplicaSets() []metric.Update {
+	replicaSets := ccs.clusterCache.GetAllReplicaSets()
+	return ccs.scrapeReplicaSets(replicaSets)
 }
 
-func (ccs *ClusterCacheScraper) scrapeReplicaSets(replicaSets []*clustercache.ReplicaSet, timestamp time.Time) {
+func (ccs *ClusterCacheScraper) scrapeReplicaSets(replicaSets []*clustercache.ReplicaSet) []metric.Update {
+	var scrapeResults []metric.Update
 	for _, replicaSet := range replicaSets {
 		replicaSetInfo := map[string]string{
 			source.ReplicaSetLabel: replicaSet.Name,
@@ -297,9 +431,14 @@ func (ccs *ClusterCacheScraper) scrapeReplicaSets(replicaSets []*clustercache.Re
 			ownerInfo := maps.Clone(replicaSetInfo)
 			ownerInfo[source.OwnerKindLabel] = owner.Kind
 			ownerInfo[source.OwnerNameLabel] = owner.Name
-			ccs.updater.Update(KubeReplicasetOwner, ownerInfo, 0, timestamp, nil)
+			scrapeResults = append(scrapeResults, metric.Update{
+				Name:   KubeReplicasetOwner,
+				Labels: ownerInfo,
+				Value:  0,
+			})
 		}
 	}
+	return scrapeResults
 }
 
 // getPersistentVolumeClaimClass returns StorageClassName. If no storage class was

+ 162 - 200
modules/collector-source/pkg/scrape/clustercache_test.go

@@ -1,6 +1,7 @@
 package scrape
 
 import (
+	"reflect"
 	"testing"
 	"time"
 
@@ -26,7 +27,7 @@ func Test_kubernetesScraper_scrapeNodes(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -55,56 +56,51 @@ func Test_kubernetesScraper_scrapeNodes(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: KubeNodeStatusCapacityCPUCores,
+					Name: KubeNodeStatusCapacityCPUCores,
 					Labels: map[string]string{
 						source.NodeLabel:       "node1",
 						source.ProviderIDLabel: "i-1",
 					},
-					Value:                 2.0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          2.0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubeNodeStatusCapacityMemoryBytes,
+					Name: KubeNodeStatusCapacityMemoryBytes,
 					Labels: map[string]string{
 						source.NodeLabel:       "node1",
 						source.ProviderIDLabel: "i-1",
 					},
-					Value:                 2048.0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          2048.0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubeNodeStatusAllocatableCPUCores,
+					Name: KubeNodeStatusAllocatableCPUCores,
 					Labels: map[string]string{
 						source.NodeLabel:       "node1",
 						source.ProviderIDLabel: "i-1",
 					},
-					Value:                 1.0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          1.0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubeNodeStatusAllocatableMemoryBytes,
+					Name: KubeNodeStatusAllocatableMemoryBytes,
 					Labels: map[string]string{
 						source.NodeLabel:       "node1",
 						source.ProviderIDLabel: "i-1",
 					},
-					Value:                 1024.0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          1024.0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubeNodeLabels,
+					Name: KubeNodeLabels,
 					Labels: map[string]string{
 						source.NodeLabel:       "node1",
 						source.ProviderIDLabel: "i-1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"label_test1": "blah",
 						"label_test2": "blah2",
 					},
@@ -114,23 +110,21 @@ func Test_kubernetesScraper_scrapeNodes(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapeNodes(s.Nodes, s.Timestamp)
+				res := ks.scrapeNodes(s.Nodes)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -148,7 +142,7 @@ func Test_kubernetesScraper_scrapeDeployments(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -167,17 +161,16 @@ func Test_kubernetesScraper_scrapeDeployments(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 
 				{
-					MetricName: DeploymentMatchLabels,
+					Name: DeploymentMatchLabels,
 					Labels: map[string]string{
 						source.DeploymentLabel: "deployment1",
 						source.NamespaceLabel:  "namespace1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"label_test1": "blah",
 						"label_test2": "blah2",
 					},
@@ -187,23 +180,21 @@ func Test_kubernetesScraper_scrapeDeployments(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapeDeployments(s.Deployments, s.Timestamp)
+				res := ks.scrapeDeployments(s.Deployments)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -221,7 +212,7 @@ func Test_kubernetesScraper_scrapeNamespaces(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -243,27 +234,25 @@ func Test_kubernetesScraper_scrapeNamespaces(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: KubeNamespaceLabels,
+					Name: KubeNamespaceLabels,
 					Labels: map[string]string{
 						source.NamespaceLabel: "namespace1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"label_test1": "blah",
 						"label_test2": "blah2",
 					},
 				},
 				{
-					MetricName: KubeNamespaceAnnotations,
+					Name: KubeNamespaceAnnotations,
 					Labels: map[string]string{
 						source.NamespaceLabel: "namespace1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"annotation_test3": "blah3",
 						"annotation_test4": "blah4",
 					},
@@ -273,23 +262,21 @@ func Test_kubernetesScraper_scrapeNamespaces(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapeNamespaces(s.Namespaces, s.Timestamp)
+				res := ks.scrapeNamespaces(s.Namespaces)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -307,7 +294,7 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -362,9 +349,9 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: KubePodLabels,
+					Name: KubePodLabels,
 					Labels: map[string]string{
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
@@ -372,15 +359,14 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"label_test1": "blah",
 						"label_test2": "blah2",
 					},
 				},
 				{
-					MetricName: KubePodAnnotations,
+					Name: KubePodAnnotations,
 					Labels: map[string]string{
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
@@ -388,15 +374,14 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"annotation_test3": "blah3",
 						"annotation_test4": "blah4",
 					},
 				},
 				{
-					MetricName: KubePodOwner,
+					Name: KubePodOwner,
 					Labels: map[string]string{
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
@@ -406,12 +391,11 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 						source.OwnerKindLabel: "deployment",
 						source.OwnerNameLabel: "deployment1",
 					},
-					Value:                 0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubePodContainerStatusRunning,
+					Name: KubePodContainerStatusRunning,
 					Labels: map[string]string{
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
@@ -420,12 +404,11 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 						source.InstanceLabel:  "node1",
 						source.ContainerLabel: "container1",
 					},
-					Value:                 0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubePodContainerResourceRequests,
+					Name: KubePodContainerResourceRequests,
 					Labels: map[string]string{
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
@@ -436,12 +419,11 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 						source.ResourceLabel:  "cpu",
 						source.UnitLabel:      "core",
 					},
-					Value:                 0.5,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          0.5,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubePodContainerResourceRequests,
+					Name: KubePodContainerResourceRequests,
 					Labels: map[string]string{
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
@@ -452,32 +434,29 @@ func Test_kubernetesScraper_scrapePods(t *testing.T) {
 						source.ResourceLabel:  "memory",
 						source.UnitLabel:      "byte",
 					},
-					Value:                 512,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          512,
+					AdditionalInfo: nil,
 				},
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapePods(s.Pods, s.Timestamp)
+				res := ks.scrapePods(s.Pods)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -495,7 +474,7 @@ func Test_kubernetesScraper_scrapePVCs(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -519,53 +498,49 @@ func Test_kubernetesScraper_scrapePVCs(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: KubePersistentVolumeClaimInfo,
+					Name: KubePersistentVolumeClaimInfo,
 					Labels: map[string]string{
 						source.PVCLabel:          "pvc1",
 						source.NamespaceLabel:    "namespace1",
 						source.VolumeNameLabel:   "vol1",
 						source.StorageClassLabel: "storageClass1",
 					},
-					Value:                 0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubePersistentVolumeClaimResourceRequestsStorageBytes,
+					Name: KubePersistentVolumeClaimResourceRequestsStorageBytes,
 					Labels: map[string]string{
 						source.PVCLabel:          "pvc1",
 						source.NamespaceLabel:    "namespace1",
 						source.VolumeNameLabel:   "vol1",
 						source.StorageClassLabel: "storageClass1",
 					},
-					Value:                 4096,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          4096,
+					AdditionalInfo: nil,
 				},
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapePVCs(s.PVCs, s.Timestamp)
+				res := ks.scrapePVCs(s.PVCs)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -583,7 +558,7 @@ func Test_kubernetesScraper_scrapePVs(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -608,51 +583,47 @@ func Test_kubernetesScraper_scrapePVs(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: KubecostPVInfo,
+					Name: KubecostPVInfo,
 					Labels: map[string]string{
 						source.PVLabel:           "pv1",
 						source.ProviderIDLabel:   "vol-1",
 						source.StorageClassLabel: "storageClass1",
 					},
-					Value:                 0,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          0,
+					AdditionalInfo: nil,
 				},
 				{
-					MetricName: KubePersistentVolumeCapacityBytes,
+					Name: KubePersistentVolumeCapacityBytes,
 					Labels: map[string]string{
 						source.PVLabel:           "pv1",
 						source.ProviderIDLabel:   "vol-1",
 						source.StorageClassLabel: "storageClass1",
 					},
-					Value:                 4096,
-					Timestamp:             start1,
-					AdditionalInformation: nil,
+					Value:          4096,
+					AdditionalInfo: nil,
 				},
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapePVs(s.PVs, s.Timestamp)
+				res := ks.scrapePVs(s.PVs)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -670,7 +641,7 @@ func Test_kubernetesScraper_scrapeServices(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -689,16 +660,15 @@ func Test_kubernetesScraper_scrapeServices(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: ServiceSelectorLabels,
+					Name: ServiceSelectorLabels,
 					Labels: map[string]string{
 						"service":             "service1",
 						source.NamespaceLabel: "namespace1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"label_test1": "blah",
 						"label_test2": "blah2",
 					},
@@ -708,23 +678,21 @@ func Test_kubernetesScraper_scrapeServices(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapeServices(s.Services, s.Timestamp)
+				res := ks.scrapeServices(s.Services)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -742,7 +710,7 @@ func Test_kubernetesScraper_scrapeStatefulSets(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -763,16 +731,15 @@ func Test_kubernetesScraper_scrapeStatefulSets(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: StatefulSetMatchLabels,
+					Name: StatefulSetMatchLabels,
 					Labels: map[string]string{
 						source.StatefulSetLabel: "statefulSet1",
 						source.NamespaceLabel:   "namespace1",
 					},
-					Value:     0,
-					Timestamp: start1,
-					AdditionalInformation: map[string]string{
+					Value: 0,
+					AdditionalInfo: map[string]string{
 						"label_test1": "blah",
 						"label_test2": "blah2",
 					},
@@ -782,23 +749,21 @@ func Test_kubernetesScraper_scrapeStatefulSets(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapeStatefulSets(s.StatefulSets, s.Timestamp)
+				res := ks.scrapeStatefulSets(s.StatefulSets)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})
@@ -816,7 +781,7 @@ func Test_kubernetesScraper_scrapeReplicaSets(t *testing.T) {
 	tests := []struct {
 		name     string
 		scrapes  []scrape
-		expected []metric.UpdateArgs
+		expected []metric.Update
 	}{
 		{
 			name: "simple",
@@ -837,40 +802,37 @@ func Test_kubernetesScraper_scrapeReplicaSets(t *testing.T) {
 					Timestamp: start1,
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: KubeReplicasetOwner,
+					Name: KubeReplicasetOwner,
 					Labels: map[string]string{
 						"replicaset":          "replicaSet1",
 						source.NamespaceLabel: "namespace1",
 						source.OwnerNameLabel: "rollout1",
 						source.OwnerKindLabel: "Rollout",
 					},
-					Value:     0,
-					Timestamp: start1,
+					Value: 0,
 				},
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			ks := &ClusterCacheScraper{
-				updater: &updateRecorder,
-			}
+			ks := &ClusterCacheScraper{}
+			var scrapeResults []metric.Update
 			for _, s := range tt.scrapes {
-				ks.scrapeReplicaSets(s.ReplicaSets, s.Timestamp)
+				res := ks.scrapeReplicaSets(s.ReplicaSets)
+				scrapeResults = append(scrapeResults, res...)
 			}
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})

+ 3 - 5
modules/collector-source/pkg/scrape/dcgm.go

@@ -6,7 +6,6 @@ import (
 
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/log"
-	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/scrape/target"
 )
 
@@ -18,15 +17,14 @@ const (
 	DCGMFIDEVDECUTIL         = "DCGM_FI_DEV_DEC_UTIL"
 )
 
-func newDCGMScrapper(clusterCache clustercache.ClusterCache, updater metric.MetricUpdater) Scraper {
+func newDCGMScrapper(clusterCache clustercache.ClusterCache) Scraper {
 	tp := newDCGMTargetProvider(clusterCache)
-	return newDCGMTargetScraper(tp, updater)
+	return newDCGMTargetScraper(tp)
 }
 
-func newDCGMTargetScraper(provider target.TargetProvider, updater metric.MetricUpdater) *TargetScraper {
+func newDCGMTargetScraper(provider target.TargetProvider) *TargetScraper {
 	return newTargetScrapper(
 		provider,
-		updater,
 		[]string{
 			DCGMFIPROFGRENGINEACTIVE,
 			DCGMFIDEVDECUTIL,

+ 4 - 10
modules/collector-source/pkg/scrape/network.go

@@ -5,7 +5,6 @@ import (
 
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/log"
-	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/scrape/target"
 )
 
@@ -16,19 +15,16 @@ const (
 )
 
 func newNetworkScraper(
-	releaseName string,
 	port int,
 	clusterCache clustercache.ClusterCache,
-	updater metric.MetricUpdater,
 ) Scraper {
-	tp := NewNetworkTargetProvider(releaseName, port, clusterCache)
-	return newNetworkTargetScraper(tp, updater)
+	tp := NewNetworkTargetProvider(port, clusterCache)
+	return newNetworkTargetScraper(tp)
 }
 
-func newNetworkTargetScraper(provider target.TargetProvider, updater metric.MetricUpdater) *TargetScraper {
+func newNetworkTargetScraper(provider target.TargetProvider) *TargetScraper {
 	return newTargetScrapper(
 		provider,
-		updater,
 		[]string{
 			KubecostPodNetworkEgressBytesTotal,
 			KubecostPodNetworkIngressBytesTotal,
@@ -37,14 +33,12 @@ func newNetworkTargetScraper(provider target.TargetProvider, updater metric.Metr
 }
 
 type NetworkTargetProvider struct {
-	releaseName  string
 	port         int
 	clusterCache clustercache.ClusterCache
 }
 
-func NewNetworkTargetProvider(releaseName string, port int, clusterCache clustercache.ClusterCache) *NetworkTargetProvider {
+func NewNetworkTargetProvider(port int, clusterCache clustercache.ClusterCache) *NetworkTargetProvider {
 	return &NetworkTargetProvider{
-		releaseName:  releaseName,
 		port:         port,
 		clusterCache: clusterCache,
 	}

+ 3 - 5
modules/collector-source/pkg/scrape/opencost.go

@@ -1,7 +1,6 @@
 package scrape
 
 import (
-	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/scrape/target"
 )
 
@@ -30,14 +29,13 @@ func newOpenCostTargetProvider() target.TargetProvider {
 	return target.NewDefaultTargetProvider(target.NewUrlTarget("http://localhost:9003/metrics"))
 }
 
-func newOpenCostScraper(updater metric.MetricUpdater) Scraper {
-	return newOpencostTargetScraper(newOpenCostTargetProvider(), updater)
+func newOpenCostScraper() Scraper {
+	return newOpencostTargetScraper(newOpenCostTargetProvider())
 }
 
-func newOpencostTargetScraper(provider target.TargetProvider, updater metric.MetricUpdater) *TargetScraper {
+func newOpencostTargetScraper(provider target.TargetProvider) *TargetScraper {
 	return newTargetScrapper(
 		provider,
-		updater,
 		[]string{
 			KubecostClusterManagementCost,
 			KubecostNetworkZoneEgressCost,

+ 40 - 21
modules/collector-source/pkg/scrape/scrapecontroller.go

@@ -1,6 +1,7 @@
 package scrape
 
 import (
+	"fmt"
 	"time"
 
 	"github.com/opencost/opencost/core/pkg/clustercache"
@@ -8,46 +9,51 @@ import (
 	"github.com/opencost/opencost/core/pkg/util/atomic"
 	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/util"
-	"k8s.io/client-go/kubernetes"
 )
 
 // ScrapeController initializes and holds the scrapers in addition to running the loop that triggers scrapes
 type ScrapeController struct {
-	scrapeInterval time.Duration
+	scrapeInterval util.Interval
 	runState       atomic.AtomicRunState
 	scrapers       []Scraper
+	repo           *metric.MetricRepository
 }
 
 func NewScrapeController(
-	scrapeInterval time.Duration,
-	releaseName string,
+	scrapeInterval string,
 	networkPort int,
-	updater metric.MetricUpdater,
+	repo *metric.MetricRepository,
 	clusterCache clustercache.ClusterCache,
-	k8s kubernetes.Interface,
 	statSummaryClient util.StatSummaryClient,
 ) *ScrapeController {
-	var scrapers []Scraper
 
-	clusterCacheScraper := newClusterCacheScraper(clusterCache, updater)
+	var scrapers []Scraper
+	clusterCacheScraper := newClusterCacheScraper(clusterCache)
 	scrapers = append(scrapers, clusterCacheScraper)
 
-	opencostScraper := newOpenCostScraper(updater)
+	opencostScraper := newOpenCostScraper()
 	scrapers = append(scrapers, opencostScraper)
 
-	statSummaryScraper := newStatSummaryScraper(statSummaryClient, updater)
+	statSummaryScraper := newStatSummaryScraper(statSummaryClient)
 	scrapers = append(scrapers, statSummaryScraper)
 
-	networkScraper := newNetworkScraper(releaseName, networkPort, clusterCache, updater)
+	networkScraper := newNetworkScraper(networkPort, clusterCache)
 	scrapers = append(scrapers, networkScraper)
 
-	dcgmScraper := newDCGMScrapper(clusterCache, updater)
+	dcgmScraper := newDCGMScrapper(clusterCache)
 	scrapers = append(scrapers, dcgmScraper)
 
+	si, err := util.NewInterval(scrapeInterval)
+	if err != nil {
+		panic(fmt.Errorf("scrapecontroller failed to create scrape interval: %w", err))
+	}
+
 	sc := &ScrapeController{
-		scrapeInterval: scrapeInterval,
+		scrapeInterval: si,
 		scrapers:       scrapers,
+		repo:           repo,
 	}
+
 	return sc
 }
 
@@ -62,24 +68,37 @@ func (sc *ScrapeController) Start() {
 		return
 	}
 	go func() {
-		ticker := time.NewTicker(sc.scrapeInterval)
+		nextScrape := time.Now().UTC()
+		timer := time.NewTimer(time.Duration(0))
 		for {
-			for _, scraper := range sc.scrapers {
-				scraper.Scrape()
-			}
 			select {
 			case <-sc.runState.OnStop():
 				sc.runState.Reset()
-				ticker.Stop()
+				timer.Stop()
 				return // exit go routine
-			case <-ticker.C:
+			case <-timer.C:
+				sc.Scrape(nextScrape)
+				nextScrape = sc.scrapeInterval.Add(sc.scrapeInterval.Truncate(time.Now().UTC()), 1)
+				timer.Reset(time.Until(nextScrape))
 			}
-
 		}
-
 	}()
 }
 
 func (sc *ScrapeController) Stop() {
 	sc.runState.Stop()
 }
+
+func (sc *ScrapeController) Scrape(timestamp time.Time) {
+
+	// Run scrapes concurrently to minimize time from call to data collection
+	var scrapeFuncs []ScrapeFunc
+	for i := range sc.scrapers {
+		scraper := sc.scrapers[i]
+		scrapeFuncs = append(scrapeFuncs, scraper.Scrape)
+	}
+	scrapeResults := concurrentScrape(scrapeFuncs...)
+
+	// once all results are returned run updates all at once with the same timestamp
+	sc.repo.Update(scrapeResults, timestamp)
+}

+ 25 - 1
modules/collector-source/pkg/scrape/scraper.go

@@ -1,5 +1,29 @@
 package scrape
 
+import (
+	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
+)
+
 type Scraper interface {
-	Scrape()
+	Scrape() []metric.Update
+}
+
+type ScrapeFunc func() []metric.Update
+
+func concurrentScrape(scrapeFuncs ...ScrapeFunc) []metric.Update {
+	resultCh := make(chan []metric.Update)
+	defer close(resultCh)
+	for _, scrapeFunc := range scrapeFuncs {
+		go func() {
+			scrapeResults := scrapeFunc()
+			resultCh <- scrapeResults
+		}()
+	}
+
+	var scrapeResults []metric.Update
+	for range scrapeFuncs {
+		targetResults := <-resultCh
+		scrapeResults = append(scrapeResults, targetResults...)
+	}
+	return scrapeResults
 }

+ 47 - 63
modules/collector-source/pkg/scrape/statsummary.go

@@ -21,22 +21,21 @@ const (
 )
 
 type StatSummaryScraper struct {
-	client  util.StatSummaryClient
-	updater metric.MetricUpdater
+	client util.StatSummaryClient
 }
 
-func newStatSummaryScraper(client util.StatSummaryClient, updater metric.MetricUpdater) Scraper {
+func newStatSummaryScraper(client util.StatSummaryClient) Scraper {
 	return &StatSummaryScraper{
-		client:  client,
-		updater: updater,
+		client: client,
 	}
 }
 
-func (s *StatSummaryScraper) Scrape() {
+func (s *StatSummaryScraper) Scrape() []metric.Update {
+	var scrapeResults []metric.Update
 	nodeStats, err := s.client.GetNodeData()
 	if err != nil {
 		log.Errorf("error retrieving node stat data: %s", err.Error())
-		return
+		return scrapeResults
 	}
 
 	// track if a pvc has already been seen when updating KubeletVolumeStatsUsedBytes
@@ -45,29 +44,25 @@ func (s *StatSummaryScraper) Scrape() {
 	for _, stat := range nodeStats {
 		nodeName := stat.Node.NodeName
 		if stat.Node.CPU != nil && stat.Node.CPU.UsageCoreNanoSeconds != nil {
-			s.updater.Update(
-				NodeCPUSecondsTotal,
-				map[string]string{
+			scrapeResults = append(scrapeResults, metric.Update{
+				Name: NodeCPUSecondsTotal,
+				Labels: map[string]string{
 					source.KubernetesNodeLabel: nodeName,
 					source.ModeLabel:           "", // TODO
 				},
-				float64(*stat.Node.CPU.UsageCoreNanoSeconds)*1e-9,
-				stat.Node.CPU.Time.Time,
-				nil,
-			)
+				Value: float64(*stat.Node.CPU.UsageCoreNanoSeconds) * 1e-9,
+			})
 		}
 
 		if stat.Node.Fs != nil && stat.Node.Fs.CapacityBytes != nil {
-			s.updater.Update(
-				NodeFSCapacityBytes,
-				map[string]string{
+			scrapeResults = append(scrapeResults, metric.Update{
+				Name: NodeFSCapacityBytes,
+				Labels: map[string]string{
 					source.InstanceLabel: nodeName,
 					source.DeviceLabel:   "local", // This value has to be populated but isn't important here
 				},
-				float64(*stat.Node.Fs.CapacityBytes),
-				stat.Node.Fs.Time.Time,
-				nil,
-			)
+				Value: float64(*stat.Node.Fs.CapacityBytes),
+			})
 		}
 
 		for _, pod := range stat.Pods {
@@ -77,31 +72,27 @@ func (s *StatSummaryScraper) Scrape() {
 
 			if pod.Network != nil {
 				if pod.Network.RxBytes != nil {
-					s.updater.Update(
-						ContainerNetworkReceiveBytesTotal,
-						map[string]string{
+					scrapeResults = append(scrapeResults, metric.Update{
+						Name: ContainerNetworkReceiveBytesTotal,
+						Labels: map[string]string{
 							source.UIDLabel:       podUID,
 							source.PodLabel:       podName,
 							source.NamespaceLabel: namespace,
 						},
-						float64(*pod.Network.RxBytes),
-						pod.Network.Time.Time,
-						nil,
-					)
+						Value: float64(*pod.Network.RxBytes),
+					})
 				}
 
 				if pod.Network.TxBytes != nil {
-					s.updater.Update(
-						ContainerNetworkTransmitBytesTotal,
-						map[string]string{
+					scrapeResults = append(scrapeResults, metric.Update{
+						Name: ContainerNetworkTransmitBytesTotal,
+						Labels: map[string]string{
 							source.UIDLabel:       podUID,
 							source.PodLabel:       podName,
 							source.NamespaceLabel: namespace,
 						},
-						float64(*pod.Network.TxBytes),
-						pod.Network.Time.Time,
-						nil,
-					)
+						Value: float64(*pod.Network.TxBytes),
+					})
 				}
 			}
 
@@ -112,64 +103,57 @@ func (s *StatSummaryScraper) Scrape() {
 				if _, ok := seenPVC[*volumeStats.PVCRef]; ok {
 					continue
 				}
-				s.updater.Update(
-					KubeletVolumeStatsUsedBytes,
-					map[string]string{
+				scrapeResults = append(scrapeResults, metric.Update{
+					Name: KubeletVolumeStatsUsedBytes,
+					Labels: map[string]string{
 						source.PVCLabel:       volumeStats.PVCRef.Name,
 						source.NamespaceLabel: volumeStats.PVCRef.Namespace,
 					},
-					float64(*volumeStats.UsedBytes),
-					volumeStats.Time.Time,
-					nil,
-				)
+					Value: float64(*volumeStats.UsedBytes),
+				})
 				seenPVC[*volumeStats.PVCRef] = struct{}{}
 			}
 
 			for _, container := range pod.Containers {
 				if container.CPU != nil && container.CPU.UsageCoreNanoSeconds != nil {
-					s.updater.Update(
-						ContainerCPUUsageSecondsTotal,
-						map[string]string{
+					scrapeResults = append(scrapeResults, metric.Update{
+						Name: ContainerCPUUsageSecondsTotal,
+						Labels: map[string]string{
 							source.ContainerLabel: container.Name,
 							source.PodLabel:       podName,
 							source.NamespaceLabel: namespace,
 							source.NodeLabel:      nodeName,
 							source.InstanceLabel:  nodeName,
 						},
-						float64(*container.CPU.UsageCoreNanoSeconds)*1e-9,
-						container.CPU.Time.Time,
-						nil,
-					)
+						Value: float64(*container.CPU.UsageCoreNanoSeconds) * 1e-9,
+					})
 				}
 				if container.Memory != nil && container.Memory.WorkingSetBytes != nil {
-					s.updater.Update(
-						ContainerMemoryWorkingSetBytes,
-						map[string]string{
+					scrapeResults = append(scrapeResults, metric.Update{
+						Name: ContainerMemoryWorkingSetBytes,
+						Labels: map[string]string{
 							source.ContainerLabel: container.Name,
 							source.PodLabel:       podName,
 							source.NamespaceLabel: namespace,
 							source.NodeLabel:      nodeName,
 							source.InstanceLabel:  nodeName,
 						},
-						float64(*container.Memory.WorkingSetBytes),
-						container.Memory.Time.Time,
-						nil,
-					)
+						Value: float64(*container.Memory.WorkingSetBytes),
+					})
 				}
 
 				if container.Rootfs != nil && container.Rootfs.UsedBytes != nil {
-					s.updater.Update(
-						ContainerFSUsageBytes,
-						map[string]string{
+					scrapeResults = append(scrapeResults, metric.Update{
+						Name: ContainerFSUsageBytes,
+						Labels: map[string]string{
 							source.InstanceLabel: nodeName,
 							source.DeviceLabel:   "local",
 						},
-						float64(*container.Rootfs.UsedBytes),
-						container.Rootfs.Time.Time,
-						nil,
-					)
+						Value: float64(*container.Rootfs.UsedBytes),
+					})
 				}
 			}
 		}
 	}
+	return scrapeResults
 }

+ 31 - 42
modules/collector-source/pkg/scrape/statsummary_test.go

@@ -1,6 +1,7 @@
 package scrape
 
 import (
+	"reflect"
 	"testing"
 	"time"
 
@@ -23,7 +24,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 	start1, _ := time.Parse(time.RFC3339, Start1Str)
 	tests := map[string]struct {
 		summaries []*stats.Summary
-		expected  []metric.UpdateArgs
+		expected  []metric.Update
 	}{
 		"nil values": {
 			summaries: []*stats.Summary{
@@ -87,7 +88,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					},
 				},
 			},
-			expected: []metric.UpdateArgs{},
+			expected: []metric.Update{},
 		},
 		"nil structs": {
 			summaries: []*stats.Summary{
@@ -118,7 +119,7 @@ func TestStatScraper_Scrape(t *testing.T) {
 					},
 				},
 			},
-			expected: []metric.UpdateArgs{},
+			expected: []metric.Update{},
 		},
 		"single node": {
 			summaries: []*stats.Summary{
@@ -189,56 +190,51 @@ func TestStatScraper_Scrape(t *testing.T) {
 					},
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: NodeCPUSecondsTotal,
+					Name: NodeCPUSecondsTotal,
 					Labels: map[string]string{
 						source.KubernetesNodeLabel: "node1",
 						source.ModeLabel:           "",
 					},
-					Value:     2,
-					Timestamp: start1,
+					Value: 2,
 				},
 				{
-					MetricName: NodeFSCapacityBytes,
+					Name: NodeFSCapacityBytes,
 					Labels: map[string]string{
 						source.InstanceLabel: "node1",
 						source.DeviceLabel:   "local",
 					},
-					Value:     float64(2 * util.GB),
-					Timestamp: start1,
+					Value: float64(2 * util.GB),
 				},
 				{
-					MetricName: ContainerNetworkReceiveBytesTotal,
+					Name: ContainerNetworkReceiveBytesTotal,
 					Labels: map[string]string{
 						source.UIDLabel:       "uid1",
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
 					},
-					Value:     float64(1 * util.MB),
-					Timestamp: start1,
+					Value: float64(1 * util.MB),
 				},
 				{
-					MetricName: ContainerNetworkTransmitBytesTotal,
+					Name: ContainerNetworkTransmitBytesTotal,
 					Labels: map[string]string{
 						source.UIDLabel:       "uid1",
 						source.PodLabel:       "pod1",
 						source.NamespaceLabel: "namespace1",
 					},
-					Value:     float64(2 * util.MB),
-					Timestamp: start1,
+					Value: float64(2 * util.MB),
 				},
 				{
-					MetricName: KubeletVolumeStatsUsedBytes,
+					Name: KubeletVolumeStatsUsedBytes,
 					Labels: map[string]string{
 						source.PVCLabel:       "pvc1",
 						source.NamespaceLabel: "namespace1",
 					},
-					Value:     float64(1 * util.GB),
-					Timestamp: start1,
+					Value: float64(1 * util.GB),
 				},
 				{
-					MetricName: ContainerCPUUsageSecondsTotal,
+					Name: ContainerCPUUsageSecondsTotal,
 					Labels: map[string]string{
 						source.ContainerLabel: "container1",
 						source.PodLabel:       "pod1",
@@ -246,11 +242,10 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 					},
-					Value:     1,
-					Timestamp: start1,
+					Value: 1,
 				},
 				{
-					MetricName: ContainerMemoryWorkingSetBytes,
+					Name: ContainerMemoryWorkingSetBytes,
 					Labels: map[string]string{
 						source.ContainerLabel: "container1",
 						source.PodLabel:       "pod1",
@@ -258,17 +253,15 @@ func TestStatScraper_Scrape(t *testing.T) {
 						source.NodeLabel:      "node1",
 						source.InstanceLabel:  "node1",
 					},
-					Value:     float64(5 * util.MB),
-					Timestamp: start1,
+					Value: float64(5 * util.MB),
 				},
 				{
-					MetricName: ContainerFSUsageBytes,
+					Name: ContainerFSUsageBytes,
 					Labels: map[string]string{
 						source.InstanceLabel: "node1",
 						source.DeviceLabel:   "local",
 					},
-					Value:     float64(1 * util.GB),
-					Timestamp: start1,
+					Value: float64(1 * util.GB),
 				},
 			},
 		},
@@ -322,37 +315,33 @@ func TestStatScraper_Scrape(t *testing.T) {
 					},
 				},
 			},
-			expected: []metric.UpdateArgs{
+			expected: []metric.Update{
 				{
-					MetricName: KubeletVolumeStatsUsedBytes,
+					Name: KubeletVolumeStatsUsedBytes,
 					Labels: map[string]string{
 						source.PVCLabel:       "pvc1",
 						source.NamespaceLabel: "namespace1",
 					},
-					Value:     float64(1 * util.GB),
-					Timestamp: start1,
+					Value: float64(1 * util.GB),
 				},
 			},
 		},
 	}
 	for name, tt := range tests {
 		t.Run(name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
 			s := &StatSummaryScraper{
-				client:  &mockStatSummaryClient{results: tt.summaries},
-				updater: &updateRecorder,
+				client: &mockStatSummaryClient{results: tt.summaries},
 			}
-			s.Scrape()
+			scrapeResults := s.Scrape()
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.Equals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})

+ 30 - 27
modules/collector-source/pkg/scrape/targetscraper.go

@@ -1,8 +1,6 @@
 package scrape
 
 import (
-	"time"
-
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/scrape/parser"
@@ -11,49 +9,54 @@ import (
 
 type TargetScraper struct {
 	targetProvider target.TargetProvider
-	metricUpdater  metric.MetricUpdater
 	metricNames    map[string]struct{} // filter for which metrics will be processed
 	includeMetrics bool                // toggle to make metrics an include or exclude list
 }
 
-func newTargetScrapper(provider target.TargetProvider, updater metric.MetricUpdater, metricNames []string, includeMetrics bool) *TargetScraper {
+func newTargetScrapper(provider target.TargetProvider, metricNames []string, includeMetrics bool) *TargetScraper {
 	metricSet := make(map[string]struct{})
 	for _, metricName := range metricNames {
 		metricSet[metricName] = struct{}{}
 	}
 	return &TargetScraper{
 		targetProvider: provider,
-		metricUpdater:  updater,
 		metricNames:    metricSet,
 		includeMetrics: includeMetrics,
 	}
 }
 
-func (s *TargetScraper) Scrape() {
+func (s *TargetScraper) Scrape() []metric.Update {
 	targets := s.targetProvider.GetTargets()
-	for _, target := range targets {
-		now := time.Now().UTC()
-		f, err := target.Load()
-		if err != nil {
-			log.Errorf("failed to scrape target: %s", err.Error())
-			continue
-		}
-		results, err := parser.Parse(f)
-		if err != nil {
-			log.Errorf("failed to parse target: %s", err.Error())
-			continue
-		}
-
-		for _, result := range results {
-			// filter metrics to be processed by name
-			if _, ok := s.metricNames[result.Name]; ok != s.includeMetrics {
-				continue
+	var scrapeFuncs []ScrapeFunc
+	for i := range targets {
+		target := targets[i]
+		fn := func() []metric.Update {
+			var scrapeResults []metric.Update
+			f, err := target.Load()
+			if err != nil {
+				log.Errorf("failed to scrape target: %s", err.Error())
+				return scrapeResults
+			}
+			results, err := parser.Parse(f)
+			if err != nil {
+				log.Errorf("failed to parse target: %s", err.Error())
+				return scrapeResults
 			}
-			timestamp := now
-			if result.Timestamp != nil {
-				timestamp = *result.Timestamp
+			for _, result := range results {
+				// filter metrics to be processed by name
+				if _, ok := s.metricNames[result.Name]; ok != s.includeMetrics {
+					continue
+				}
+				scrapeResults = append(scrapeResults, metric.Update{
+					Name:   result.Name,
+					Labels: result.Labels,
+					Value:  result.Value,
+				})
 			}
-			s.metricUpdater.Update(result.Name, result.Labels, result.Value, timestamp, nil)
+			return scrapeResults
 		}
+		scrapeFuncs = append(scrapeFuncs, fn)
 	}
+
+	return concurrentScrape(scrapeFuncs...)
 }

+ 64 - 83
modules/collector-source/pkg/scrape/targetscraper_test.go

@@ -1,8 +1,8 @@
 package scrape
 
 import (
+	"reflect"
 	"testing"
-	"time"
 
 	"github.com/opencost/opencost/modules/collector-source/pkg/metric"
 	"github.com/opencost/opencost/modules/collector-source/pkg/scrape/target"
@@ -101,23 +101,19 @@ DCGM_FI_DEV_DEC_UTIL{gpu="0",UUID="GPU-1",pci_bus_id="00000000:00:0A.0",device="
 `
 
 func TestTargetScraper_Scrape(t *testing.T) {
-	start1, _ := time.Parse(time.RFC3339, Start1Str)
 	tests := []struct {
-		name            string
-		scrapperFactory func(metric.MetricUpdater) *TargetScraper
-		expected        []metric.UpdateArgs
+		name                 string
+		scrapeText           string
+		targetScraperFactory func(provider target.TargetProvider) *TargetScraper
+		expected             []metric.Update
 	}{
 		{
-			name: "Network Scrape",
-			scrapperFactory: func(updater metric.MetricUpdater) *TargetScraper {
-				return newNetworkTargetScraper(
-					target.NewDefaultTargetProvider(target.NewStringTarget(networkScape)),
-					updater,
-				)
-			},
-			expected: []metric.UpdateArgs{
+			name:                 "Network Scrape",
+			scrapeText:           networkScape,
+			targetScraperFactory: newNetworkTargetScraper,
+			expected: []metric.Update{
 				{
-					MetricName: KubecostPodNetworkEgressBytesTotal,
+					Name: KubecostPodNetworkEgressBytesTotal,
 					Labels: map[string]string{
 						"pod_name":    "pod1",
 						"namespace":   "namespace1",
@@ -126,11 +122,10 @@ func TestTargetScraper_Scrape(t *testing.T) {
 						"same_zone":   "true",
 						"service":     "service1",
 					},
-					Value:     3127969647,
-					Timestamp: start1,
+					Value: 3127969647,
 				},
 				{
-					MetricName: KubecostPodNetworkEgressBytesTotal,
+					Name: KubecostPodNetworkEgressBytesTotal,
 					Labels: map[string]string{
 						"pod_name":    "pod2",
 						"namespace":   "namespace1",
@@ -139,11 +134,10 @@ func TestTargetScraper_Scrape(t *testing.T) {
 						"same_zone":   "false",
 						"service":     "",
 					},
-					Value:     335188219,
-					Timestamp: start1,
+					Value: 335188219,
 				},
 				{
-					MetricName: KubecostPodNetworkIngressBytesTotal,
+					Name: KubecostPodNetworkIngressBytesTotal,
 					Labels: map[string]string{
 						"pod_name":    "pod1",
 						"namespace":   "namespace1",
@@ -152,11 +146,10 @@ func TestTargetScraper_Scrape(t *testing.T) {
 						"same_zone":   "false",
 						"service":     "service1",
 					},
-					Value:     17941460,
-					Timestamp: start1,
+					Value: 17941460,
 				},
 				{
-					MetricName: KubecostPodNetworkIngressBytesTotal,
+					Name: KubecostPodNetworkIngressBytesTotal,
 					Labels: map[string]string{
 						"pod_name":    "pod2",
 						"namespace":   "namespace1",
@@ -165,43 +158,36 @@ func TestTargetScraper_Scrape(t *testing.T) {
 						"same_zone":   "false",
 						"service":     "",
 					},
-					Value:     13948766,
-					Timestamp: start1,
+					Value: 13948766,
 				},
 			},
 		},
 		{
-			name: "Opencost Metric",
-			scrapperFactory: func(updater metric.MetricUpdater) *TargetScraper {
-				return newOpencostTargetScraper(target.NewDefaultTargetProvider(target.NewStringTarget(opencostScrape)),
-					updater,
-				)
-			},
-			expected: []metric.UpdateArgs{
+			name:                 "Opencost Metric",
+			scrapeText:           opencostScrape,
+			targetScraperFactory: newOpencostTargetScraper,
+			expected: []metric.Update{
 				{
-					MetricName: KubecostClusterManagementCost,
+					Name: KubecostClusterManagementCost,
 					Labels: map[string]string{
 						"provisioner_name": "GKE",
 					},
 					Value: 0.1,
 				},
 				{
-					MetricName: KubecostNetworkZoneEgressCost,
-					Labels:     map[string]string{},
-					Value:      0.01,
+					Name:  KubecostNetworkZoneEgressCost,
+					Value: 0.01,
 				},
 				{
-					MetricName: KubecostNetworkRegionEgressCost,
-					Labels:     map[string]string{},
-					Value:      0.01,
+					Name:  KubecostNetworkRegionEgressCost,
+					Value: 0.01,
 				},
 				{
-					MetricName: KubecostNetworkInternetEgressCost,
-					Labels:     map[string]string{},
-					Value:      0.12,
+					Name:  KubecostNetworkInternetEgressCost,
+					Value: 0.12,
 				},
 				{
-					MetricName: PVHourlyCost,
+					Name: PVHourlyCost,
 					Labels: map[string]string{
 						"persistentvolume": "pvc-1",
 						"provider_id":      "pvc-1",
@@ -210,7 +196,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 5.479452054794521e-05,
 				},
 				{
-					MetricName: PVHourlyCost,
+					Name: PVHourlyCost,
 					Labels: map[string]string{
 						"persistentvolume": "pvc-2",
 						"provider_id":      "pvc-2",
@@ -219,7 +205,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 5.479452054794521e-05,
 				},
 				{
-					MetricName: KubecostLoadBalancerCost,
+					Name: KubecostLoadBalancerCost,
 					Labels: map[string]string{
 						"ingress_ip":   "127.0.0.1",
 						"namespace":    "namespace1",
@@ -228,7 +214,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.025,
 				},
 				{
-					MetricName: NodeTotalHourlyCost,
+					Name: NodeTotalHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node1",
@@ -240,7 +226,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.06631302438846588,
 				},
 				{
-					MetricName: NodeTotalHourlyCost,
+					Name: NodeTotalHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node2",
@@ -252,7 +238,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.06631302438846588,
 				},
 				{
-					MetricName: NodeCPUHourlyCost,
+					Name: NodeCPUHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node1",
@@ -264,7 +250,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.021811590000000002,
 				},
 				{
-					MetricName: NodeCPUHourlyCost,
+					Name: NodeCPUHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node2",
@@ -276,7 +262,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.021811590000000002,
 				},
 				{
-					MetricName: NodeRAMHourlyCost,
+					Name: NodeRAMHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node1",
@@ -288,7 +274,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.00292353,
 				},
 				{
-					MetricName: NodeRAMHourlyCost,
+					Name: NodeRAMHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node2",
@@ -300,7 +286,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.00292353,
 				},
 				{
-					MetricName: NodeGPUHourlyCost,
+					Name: NodeGPUHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node1",
@@ -312,7 +298,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: NodeGPUHourlyCost,
+					Name: NodeGPUHourlyCost,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node2",
@@ -324,7 +310,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: NodeGPUCount,
+					Name: NodeGPUCount,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node1",
@@ -336,7 +322,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: NodeGPUCount,
+					Name: NodeGPUCount,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node2",
@@ -348,7 +334,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: KubecostNodeIsSpot,
+					Name: KubecostNodeIsSpot,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node1",
@@ -360,7 +346,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: KubecostNodeIsSpot,
+					Name: KubecostNodeIsSpot,
 					Labels: map[string]string{
 						"arch":          "amd64",
 						"instance":      "node2",
@@ -372,7 +358,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: ContainerCPUAllocation,
+					Name: ContainerCPUAllocation,
 					Labels: map[string]string{
 						"container": "container1",
 						"instance":  "node1",
@@ -383,7 +369,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.02,
 				},
 				{
-					MetricName: ContainerCPUAllocation,
+					Name: ContainerCPUAllocation,
 					Labels: map[string]string{
 						"container": "container2",
 						"instance":  "node2",
@@ -394,7 +380,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.01,
 				},
 				{
-					MetricName: ContainerMemoryAllocationBytes,
+					Name: ContainerMemoryAllocationBytes,
 					Labels: map[string]string{
 						"container": "container1",
 						"instance":  "node1",
@@ -405,7 +391,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 1.1528192e+07,
 				},
 				{
-					MetricName: ContainerMemoryAllocationBytes,
+					Name: ContainerMemoryAllocationBytes,
 					Labels: map[string]string{
 						"container": "container2",
 						"instance":  "node2",
@@ -416,7 +402,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 1e+07,
 				},
 				{
-					MetricName: ContainerGPUAllocation,
+					Name: ContainerGPUAllocation,
 					Labels: map[string]string{
 						"container": "container1",
 						"instance":  "node1",
@@ -427,7 +413,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: ContainerGPUAllocation,
+					Name: ContainerGPUAllocation,
 					Labels: map[string]string{
 						"container": "container2",
 						"instance":  "node2",
@@ -438,7 +424,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0,
 				},
 				{
-					MetricName: PodPVCAllocation,
+					Name: PodPVCAllocation,
 					Labels: map[string]string{
 						"namespace":             "namespace1",
 						"persistentvolume":      "pvc-1",
@@ -448,7 +434,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 3.4359738368e+10,
 				},
 				{
-					MetricName: PodPVCAllocation,
+					Name: PodPVCAllocation,
 					Labels: map[string]string{
 						"namespace":             "namespace1",
 						"persistentvolume":      "pvc-2",
@@ -460,15 +446,12 @@ func TestTargetScraper_Scrape(t *testing.T) {
 			},
 		},
 		{
-			name: "GPU Metric",
-			scrapperFactory: func(updater metric.MetricUpdater) *TargetScraper {
-				return newDCGMTargetScraper(target.NewDefaultTargetProvider(target.NewStringTarget(dcgmScrape)),
-					updater,
-				)
-			},
-			expected: []metric.UpdateArgs{
+			name:                 "GPU Metric",
+			scrapeText:           dcgmScrape,
+			targetScraperFactory: newDCGMTargetScraper,
+			expected: []metric.Update{
 				{
-					MetricName: DCGMFIPROFGRENGINEACTIVE,
+					Name: DCGMFIPROFGRENGINEACTIVE,
 					Labels: map[string]string{
 						"gpu":        "0",
 						"UUID":       "GPU-1",
@@ -480,7 +463,7 @@ func TestTargetScraper_Scrape(t *testing.T) {
 					Value: 0.999999,
 				},
 				{
-					MetricName: DCGMFIDEVDECUTIL,
+					Name: DCGMFIDEVDECUTIL,
 					Labels: map[string]string{
 						"gpu":        "0",
 						"UUID":       "GPU-1",
@@ -496,19 +479,17 @@ func TestTargetScraper_Scrape(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			updateRecorder := metric.ArgRecordUpdater{}
-			scrapper := tt.scrapperFactory(&updateRecorder)
-			scrapper.Scrape()
+			scraper := tt.targetScraperFactory(target.NewDefaultTargetProvider(target.NewStringTarget(tt.scrapeText)))
+			scrapeResults := scraper.Scrape()
 
-			if len(updateRecorder.UpdateArgs) != len(tt.expected) {
-				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(updateRecorder.UpdateArgs))
+			if len(scrapeResults) != len(tt.expected) {
+				t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
 			}
 
 			for i, expected := range tt.expected {
-				updateArg := updateRecorder.UpdateArgs[i]
-				err := expected.ValueEquals(updateArg)
-				if err != nil {
-					t.Errorf("Result did not match expected at index %d: %s", i, err.Error())
+				got := scrapeResults[i]
+				if !reflect.DeepEqual(expected, got) {
+					t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
 				}
 			}
 		})

+ 3 - 1
modules/collector-source/pkg/util/interval.go

@@ -9,7 +9,7 @@ import (
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 )
 
-var intervalRegex = regexp.MustCompile(`^(\d+)(m|h|d|w)$`)
+var intervalRegex = regexp.MustCompile(`^(\d+)(s|m|h|d|w)$`)
 
 // Interval is a time period defined by a string with a integer followed by a letter (ex: 5d = 5 days)
 type Interval interface {
@@ -34,6 +34,8 @@ func NewInterval(def string) (Interval, error) {
 	}
 
 	switch match[2] {
+	case "s":
+		return &durationInterval{time.Duration(num) * time.Second}, nil
 	case "m":
 		return &durationInterval{time.Duration(num) * time.Minute}, nil
 	case "h":

+ 3 - 2
modules/collector-source/pkg/util/resolution.go

@@ -24,8 +24,9 @@ func NewResolution(configuration ResolutionConfiguration) (*Resolution, error) {
 		return nil, fmt.Errorf("failed to create resolution: %w", err)
 	}
 	return &Resolution{
-		interval:  interval,
-		retention: configuration.Retention,
+		interval:    interval,
+		intervalDef: configuration.Interval,
+		retention:   configuration.Retention,
 	}, nil
 }