diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 965ddcedc..bb8aa3a2c 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -128,6 +128,7 @@ type HelmChartReconciler struct { Cache *cache.Cache TTL time.Duration *cache.CacheRecorder + RepoRecorder *repository.Recorder } func (r *HelmChartReconciler) SetupWithManager(mgr ctrl.Manager) error { @@ -585,7 +586,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * } } default: - httpChartRepo, err := repository.NewChartRepository(normalizedURL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, tlsConfig, clientOpts, + httpChartRepo, err := repository.NewChartRepository(normalizedURL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, tlsConfig, clientOpts, repo.Namespace, r.RepoRecorder, repository.WithMemoryCache(r.Storage.LocalPath(*repo.GetArtifact()), r.Cache, r.TTL, func(event string) { r.IncCacheEvents(event, obj.Name, obj.Namespace) })) @@ -1038,7 +1039,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont chartRepo = ociChartRepo } else { - httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, tlsConfig, clientOpts) + httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, tlsConfig, clientOpts, namespace, r.RepoRecorder) if err != nil { return nil, err } diff --git a/controllers/helmrepository_controller.go b/controllers/helmrepository_controller.go index ea72a51b6..c1f2ef52a 100644 --- a/controllers/helmrepository_controller.go +++ b/controllers/helmrepository_controller.go @@ -109,6 +109,7 @@ type HelmRepositoryReconciler struct { Cache *cache.Cache TTL time.Duration *cache.CacheRecorder + RepoRecorder *repository.Recorder } type HelmRepositoryReconcilerOptions struct { @@ -398,7 +399,7 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, obj *sou } // Construct Helm chart repository with options and download index - newChartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", r.Getters, tlsConfig, clientOpts) + newChartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", r.Getters, tlsConfig, clientOpts, obj.Namespace, r.RepoRecorder) if err != nil { switch err.(type) { case *url.Error: diff --git a/controllers/helmrepository_controller_test.go b/controllers/helmrepository_controller_test.go index 2e8df4873..5c17c2cdc 100644 --- a/controllers/helmrepository_controller_test.go +++ b/controllers/helmrepository_controller_test.go @@ -600,9 +600,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) { if serr != nil { validSecret = false } - newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tOpts, clientOpts) + newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tOpts, clientOpts, obj.Namespace, testRepoRecorder) } else { - newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil, nil) + newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil, nil, obj.Namespace, testRepoRecorder) } g.Expect(err).ToNot(HaveOccurred()) @@ -736,7 +736,7 @@ func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(cacheFile.Close()).ToNot(HaveOccurred()) - chartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil, nil) + chartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil, nil, obj.Namespace, testRepoRecorder) g.Expect(err).ToNot(HaveOccurred()) chartRepo.CachePath = cachePath diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 8654f06f4..7784e1983 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -53,6 +53,7 @@ import ( "github.com/fluxcd/source-controller/internal/cache" "github.com/fluxcd/source-controller/internal/features" "github.com/fluxcd/source-controller/internal/helm/registry" + "github.com/fluxcd/source-controller/internal/helm/repository" "github.com/fluxcd/source-controller/pkg/git/libgit2/managed" // +kubebuilder:scaffold:imports ) @@ -106,6 +107,7 @@ var ( var ( testRegistryServer *registryClientTestServer testCache *cache.Cache + testRepoRecorder *repository.Recorder ) func init() { @@ -263,6 +265,7 @@ func TestMain(m *testing.M) { testCache = cache.New(5, 1*time.Second) cacheRecorder := cache.MustMakeMetrics() + testRepoRecorder = repository.MustMakeMetrics() if err := (&OCIRepositoryReconciler{ Client: testEnv, @@ -282,6 +285,7 @@ func TestMain(m *testing.M) { Cache: testCache, TTL: 1 * time.Second, CacheRecorder: cacheRecorder, + RepoRecorder: testRepoRecorder, }).SetupWithManager(testEnv); err != nil { panic(fmt.Sprintf("Failed to start HelmRepositoryReconciler: %v", err)) } diff --git a/internal/helm/repository/chart_repository.go b/internal/helm/repository/chart_repository.go index 282d49a5d..e15f3dc13 100644 --- a/internal/helm/repository/chart_repository.go +++ b/internal/helm/repository/chart_repository.go @@ -76,6 +76,11 @@ type ChartRepository struct { *sync.RWMutex cacheInfo + + // namespace of the ChartRepository of the helm/oci chart repository object + Namespace string + + *Recorder } type cacheInfo struct { @@ -119,7 +124,7 @@ func WithMemoryCache(key string, c *cache.Cache, ttl time.Duration, rec RecordMe // the ChartRepository.Client configured to the getter.Getter for the // repository URL scheme. It returns an error on URL parsing failures, // or if there is no getter available for the scheme. -func NewChartRepository(repositoryURL, cachePath string, providers getter.Providers, tlsConfig *tls.Config, getterOpts []getter.Option, chartRepoOpts ...ChartRepositoryOption) (*ChartRepository, error) { +func NewChartRepository(repositoryURL, cachePath string, providers getter.Providers, tlsConfig *tls.Config, getterOpts []getter.Option, namespace string, repoRecorder *Recorder, chartRepoOpts ...ChartRepositoryOption) (*ChartRepository, error) { u, err := url.Parse(repositoryURL) if err != nil { return nil, err @@ -135,6 +140,8 @@ func NewChartRepository(repositoryURL, cachePath string, providers getter.Provid r.Client = c r.Options = getterOpts r.tlsConfig = tlsConfig + r.Namespace = namespace + r.Recorder = repoRecorder for _, opt := range chartRepoOpts { if err := opt(r); err != nil { @@ -275,8 +282,17 @@ func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer t := transport.NewOrIdle(r.tlsConfig) clientOpts := append(r.Options, getter.WithTransport(t)) defer transport.Release(t) - - return r.Client.Get(u.String(), clientOpts...) + start := time.Now() + buffer, err := r.Client.Get(u.String(), clientOpts...) + if r.Recorder != nil { + r.Recorder.RecordChartRepoEventDuration( + ChartRepoTypeHelm, + ChartRepoEventDownloadChart, + r.Namespace, + r.URL, + start) + } + return buffer, err } // LoadIndexFromBytes loads Index from the given bytes. @@ -428,6 +444,7 @@ func (r *ChartRepository) LoadFromCache() error { // the Client and set Options, and writes the index to the given io.Writer. // It returns an url.Error if the URL failed to parse. func (r *ChartRepository) DownloadIndex(w io.Writer) (err error) { + start := time.Now() u, err := url.Parse(r.URL) if err != nil { return err @@ -444,6 +461,15 @@ func (r *ChartRepository) DownloadIndex(w io.Writer) (err error) { if err != nil { return err } + if r.Recorder != nil { + r.Recorder.RecordChartRepoEventDuration( + ChartRepoTypeHelm, + ChartRepoEventDownloadIndex, + r.Namespace, + r.URL, + start) + } + if _, err = io.Copy(w, res); err != nil { return err } diff --git a/internal/helm/repository/chart_repository_test.go b/internal/helm/repository/chart_repository_test.go index 4023345bd..047b48de0 100644 --- a/internal/helm/repository/chart_repository_test.go +++ b/internal/helm/repository/chart_repository_test.go @@ -68,7 +68,7 @@ func TestNewChartRepository(t *testing.T) { t.Run("should construct chart repository", func(t *testing.T) { g := NewWithT(t) - r, err := NewChartRepository(repositoryURL, "", providers, nil, options) + r, err := NewChartRepository(repositoryURL, "", providers, nil, options, "fake-namespace", nil) g.Expect(err).ToNot(HaveOccurred()) g.Expect(r).ToNot(BeNil()) g.Expect(r.URL).To(Equal(repositoryURL)) @@ -78,7 +78,7 @@ func TestNewChartRepository(t *testing.T) { t.Run("should error on URL parsing failure", func(t *testing.T) { g := NewWithT(t) - r, err := NewChartRepository("https://ex ample.com", "", nil, nil, nil) + r, err := NewChartRepository("https://ex ample.com", "", nil, nil, nil, "fake-namespace", nil) g.Expect(err).To(HaveOccurred()) g.Expect(err).To(BeAssignableToTypeOf(&url.Error{})) g.Expect(r).To(BeNil()) @@ -88,7 +88,7 @@ func TestNewChartRepository(t *testing.T) { t.Run("should error on unsupported scheme", func(t *testing.T) { g := NewWithT(t) - r, err := NewChartRepository("http://example.com", "", providers, nil, nil) + r, err := NewChartRepository("http://example.com", "", providers, nil, nil, "fake-namespace", nil) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(Equal("scheme \"http\" not supported")) g.Expect(r).To(BeNil()) @@ -235,9 +235,12 @@ func TestChartRepository_DownloadChart(t *testing.T) { t.Parallel() mg := mockGetter{} + r := &ChartRepository{ - URL: tt.url, - Client: &mg, + URL: tt.url, + Namespace: "dummy", + Client: &mg, + Recorder: nil, } res, err := r.DownloadChart(tt.chartVersion) if tt.wantErr { diff --git a/internal/helm/repository/metrics.go b/internal/helm/repository/metrics.go new file mode 100644 index 000000000..539b36ee2 --- /dev/null +++ b/internal/helm/repository/metrics.go @@ -0,0 +1,90 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repository + +import ( + "github.com/prometheus/client_golang/prometheus" + "sigs.k8s.io/controller-runtime/pkg/metrics" + "time" +) + +const ( + ChartRepoTypeHelm = "helm" + //ChartRepoTypeOCI = "oci" + ChartRepoEventDownloadIndex = "chart_repository_download" + ChartRepoEventDownloadChart = "chart_download" +) + +// Recorder is a recorder for chart repository events. +type Recorder struct { + // TODO: type up the metrics and talk to aryan9600 + // TODO: split this counter?? + chartRepoEventsCounter *prometheus.CounterVec + durationHistogram *prometheus.HistogramVec +} + +// NewRepositoryRecorder returns a new Recorder. +// The configured labels are: event_type, name, namespace. +// The event_type is one of: +// - "chart_repository_download" +// - "chart_download" +// The url is the url of the helm chart repository +func NewRepositoryRecorder() *Recorder { + return &Recorder{ + chartRepoEventsCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "gotk_chart_repository_events_total", + Help: "Total number of events for a Helm Chart Repository.", + }, + []string{"name", "repo_type", "namespace", "url", "checksum"}, + ), + durationHistogram: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "gotk_chart_repository_event_duration_seconds", + Help: "The duration in seconds of an event for a Helm Chart Repository.", + Buckets: prometheus.ExponentialBuckets(10e-9, 10, 10), + }, + []string{"name", "repo_type", "namespace", "url"}, + ), + } +} + +// ChartRepoCollectors returns the metrics.Collector objects for the Recorder. +func (r *Recorder) ChartRepoCollectors() []prometheus.Collector { + return []prometheus.Collector{ + r.chartRepoEventsCounter, + r.durationHistogram, + } +} + +// IncChartRepoEvents increment by 1 the chart repo event count for the given event type, url and checksum. +func (r *Recorder) IncChartRepoEvents(event, repoType, url, checksum, namespace string) { + r.chartRepoEventsCounter.WithLabelValues(event, repoType, url, checksum, namespace).Inc() +} + +// RecordChartRepoEventDuration records the duration since start for the given ref. +func (r *Recorder) RecordChartRepoEventDuration(event, repoType, namespace, url string, start time.Time) { + r.durationHistogram.WithLabelValues(event, repoType, namespace, url).Observe(time.Since(start).Seconds()) +} + +// MustMakeMetrics creates a new Recorder, and registers the metrics collectors in the controller-runtime metrics registry. +func MustMakeMetrics() *Recorder { + r := NewRepositoryRecorder() + metrics.Registry.MustRegister(r.ChartRepoCollectors()...) + + return r +} diff --git a/main.go b/main.go index 621cea36c..e8c1f10b9 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "github.com/fluxcd/source-controller/internal/helm/repository" "net" "net/http" "os" @@ -225,6 +226,8 @@ func main() { os.Exit(1) } + repoRecorder := repository.MustMakeMetrics() + if err = (&controllers.HelmRepositoryOCIReconciler{ Client: mgr.GetClient(), EventRecorder: eventRecorder, @@ -270,6 +273,7 @@ func main() { Cache: c, TTL: ttl, CacheRecorder: cacheRecorder, + RepoRecorder: repoRecorder, }).SetupWithManagerAndOptions(mgr, controllers.HelmRepositoryReconcilerOptions{ MaxConcurrentReconciles: concurrent, RateLimiter: helper.GetRateLimiter(rateLimiterOptions),