From d5a565997f99a809426b53b3d5d40c487d4e2b30 Mon Sep 17 00:00:00 2001 From: Sudhish Nair <38028716+sudhishmk@users.noreply.github.com> Date: Tue, 30 Jan 2024 23:57:05 +0530 Subject: [PATCH] Add metrics for count chains attestation for issue#1028 (#1034) * Add metrics for chains to fix issue#1028 * Add metrics for chains to fix issue#1028 --- pkg/chains/constants.go | 37 ++ pkg/chains/signing.go | 21 +- pkg/pipelinerunmetrics/fake/fake.go | 30 ++ pkg/pipelinerunmetrics/injection.go | 50 +++ pkg/pipelinerunmetrics/metrics.go | 144 +++++++ pkg/pipelinerunmetrics/metrics_test.go | 68 ++++ pkg/reconciler/pipelinerun/controller.go | 2 + .../pipelinerun/pipelinerun_test.go | 1 + pkg/reconciler/taskrun/controller.go | 2 + pkg/reconciler/taskrun/taskrun_test.go | 1 + pkg/taskrunmetrics/fake/fake.go | 30 ++ pkg/taskrunmetrics/injection.go | 50 +++ pkg/taskrunmetrics/metrics.go | 145 ++++++++ pkg/taskrunmetrics/metrics_test.go | 68 ++++ .../pkg/metrics/metricstest/metricstest.go | 262 +++++++++++++ .../metrics/metricstest/resource_metrics.go | 350 ++++++++++++++++++ .../knative.dev/pkg/metrics/testing/config.go | 28 ++ vendor/modules.txt | 2 + 18 files changed, 1288 insertions(+), 3 deletions(-) create mode 100644 pkg/chains/constants.go create mode 100644 pkg/pipelinerunmetrics/fake/fake.go create mode 100644 pkg/pipelinerunmetrics/injection.go create mode 100644 pkg/pipelinerunmetrics/metrics.go create mode 100644 pkg/pipelinerunmetrics/metrics_test.go create mode 100644 pkg/taskrunmetrics/fake/fake.go create mode 100644 pkg/taskrunmetrics/injection.go create mode 100644 pkg/taskrunmetrics/metrics.go create mode 100644 pkg/taskrunmetrics/metrics_test.go create mode 100644 vendor/knative.dev/pkg/metrics/metricstest/metricstest.go create mode 100644 vendor/knative.dev/pkg/metrics/metricstest/resource_metrics.go create mode 100644 vendor/knative.dev/pkg/metrics/testing/config.go diff --git a/pkg/chains/constants.go b/pkg/chains/constants.go new file mode 100644 index 0000000000..870b441072 --- /dev/null +++ b/pkg/chains/constants.go @@ -0,0 +1,37 @@ +/* +Copyright 2024 The Tekton 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 chains + +const ( + SignedMessagesCount = "sgcount" + SignsStoredCount = "stcount" + PayloadUploadeCount = "plcount" + MarkedAsSignedCount = "mrcount" + PipelineRunSignedName = "pipelinerun_sign_created_total" + PipelineRunSignedDesc = "Total number of signed messages for pipelineruns" + PipelineRunUploadedName = "pipelinerun_payload_uploaded_total" + PipelineRunUploadedDesc = "Total number of uploaded payloads for pipelineruns" + PipelineRunStoredName = "pipelinerun_payload_stored_total" + PipelineRunStoredDesc = "Total number of stored payloads for pipelineruns" + PipelineRunMarkedName = "pipelinerun_marked_signed_total" + PipelineRunMarkedDesc = "Total number of objects marked as signed for pipelineruns" + TaskRunSignedName = "taskrun_sign_created_total" + TaskRunSignedDesc = "Total number of signed messages for taskruns" + TaskRunUploadedName = "taskrun_payload_uploaded_total" + TaskRunUploadedDesc = "Total number of uploaded payloads for taskruns" + TaskRunStoredName = "taskrun_payload_stored_total" + TaskRunStoredDesc = "Total number of stored payloads for taskruns" + TaskRunMarkedName = "taskrun_marked_signed_total" + TaskRunMarkedDesc = "Total number of objects marked as signed for taskruns" +) diff --git a/pkg/chains/signing.go b/pkg/chains/signing.go index 6a28b5e349..453bad9820 100644 --- a/pkg/chains/signing.go +++ b/pkg/chains/signing.go @@ -37,6 +37,10 @@ type Signer interface { Sign(ctx context.Context, obj objects.TektonObject) error } +type MetricsRecorder interface { + RecordCountMetrics(ctx context.Context, MetricType string) +} + type ObjectSigner struct { // Backends: store payload and signature // The keys are different storage option's name. {docdb, gcs, grafeas, oci, tekton} @@ -44,6 +48,8 @@ type ObjectSigner struct { Backends map[string]storage.Backend SecretPath string Pipelineclientset versioned.Interface + // Metrics Recorder config + Recorder MetricsRecorder } func allSigners(ctx context.Context, sp string, cfg config.Config) map[string]signing.Signer { @@ -135,7 +141,6 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) // Extract all the "things" to be signed. // We might have a few of each type (several binaries, or images) objects := signableType.ExtractObjects(ctx, tektonObj) - // Go through each object one at a time. for _, obj := range objects { @@ -175,6 +180,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) logger.Error(err) continue } + measureMetrics(ctx, SignedMessagesCount, o.Recorder) // Now store those! for _, backend := range sets.List[string](signableType.StorageBackend(cfg)) { @@ -189,6 +195,8 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) if err := b.StorePayload(ctx, tektonObj, rawPayload, string(signature), storageOpts); err != nil { logger.Error(err) merr = multierror.Append(merr, err) + } else { + measureMetrics(ctx, SignsStoredCount, o.Recorder) } } @@ -204,8 +212,8 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) merr = multierror.Append(merr, err) } else { logger.Infof("Uploaded entry to %s with index %d", cfg.Transparency.URL, *entry.LogIndex) - extraAnnotations[ChainsTransparencyAnnotation] = fmt.Sprintf("%s/api/v1/log/entries?logIndex=%d", cfg.Transparency.URL, *entry.LogIndex) + measureMetrics(ctx, PayloadUploadeCount, o.Recorder) } } @@ -223,10 +231,17 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) if err := MarkSigned(ctx, tektonObj, o.Pipelineclientset, extraAnnotations); err != nil { return err } - + measureMetrics(ctx, MarkedAsSignedCount, o.Recorder) return nil } +func measureMetrics(ctx context.Context, metrictype string, mtr MetricsRecorder) { + if mtr != nil { + mtr.RecordCountMetrics(ctx, metrictype) + } + +} + func HandleRetry(ctx context.Context, obj objects.TektonObject, ps versioned.Interface, annotations map[string]string) error { if RetryAvailable(obj) { return AddRetry(ctx, obj, ps, annotations) diff --git a/pkg/pipelinerunmetrics/fake/fake.go b/pkg/pipelinerunmetrics/fake/fake.go new file mode 100644 index 0000000000..523fe90e27 --- /dev/null +++ b/pkg/pipelinerunmetrics/fake/fake.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 The Tekton 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 fake + +import ( + "context" + + "github.com/tektoncd/chains/pkg/pipelinerunmetrics" + _ "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1/pipelinerun/fake" // Make sure the fake pipelinerun informer is setup + "k8s.io/client-go/rest" + "knative.dev/pkg/injection" +) + +func init() { + injection.Fake.RegisterClient(func(ctx context.Context, _ *rest.Config) context.Context { return pipelinerunmetrics.WithClient(ctx) }) +} diff --git a/pkg/pipelinerunmetrics/injection.go b/pkg/pipelinerunmetrics/injection.go new file mode 100644 index 0000000000..d6a758babf --- /dev/null +++ b/pkg/pipelinerunmetrics/injection.go @@ -0,0 +1,50 @@ +/* +Copyright 2024 The Tekton 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 pipelinerunmetrics + +import ( + "context" + + "k8s.io/client-go/rest" + "knative.dev/pkg/injection" + "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterClient(func(ctx context.Context, _ *rest.Config) context.Context { return WithClient(ctx) }) +} + +// RecorderKey is used for associating the Recorder inside the context.Context. +type RecorderKey struct{} + +// WithClient adds a metrics recorder to the given context +func WithClient(ctx context.Context) context.Context { + rec, err := NewRecorder(ctx) + if err != nil { + logging.FromContext(ctx).Errorf("Failed to create pipelinerun metrics recorder %v", err) + } + return context.WithValue(ctx, RecorderKey{}, rec) +} + +// Get extracts the pipelinerunmetrics.Recorder from the context. +func Get(ctx context.Context) *Recorder { + untyped := ctx.Value(RecorderKey{}) + if untyped == nil { + logging.FromContext(ctx).Errorf("Unable to fetch *pipelinerunmetrics.Recorder from context.") + } + return untyped.(*Recorder) +} diff --git a/pkg/pipelinerunmetrics/metrics.go b/pkg/pipelinerunmetrics/metrics.go new file mode 100644 index 0000000000..cdc8bfa36c --- /dev/null +++ b/pkg/pipelinerunmetrics/metrics.go @@ -0,0 +1,144 @@ +/* +Copyright 2024 The Tekton 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 pipelinerunmetrics + +import ( + "context" + "sync" + + "github.com/tektoncd/chains/pkg/chains" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "knative.dev/pkg/logging" + "knative.dev/pkg/metrics" +) + +var ( + sgCount = stats.Float64(chains.PipelineRunSignedName, + chains.PipelineRunSignedDesc, + stats.UnitDimensionless) + + sgCountView *view.View + + plCount = stats.Float64(chains.PipelineRunUploadedName, + chains.PipelineRunUploadedDesc, + stats.UnitDimensionless) + + plCountView *view.View + + stCount = stats.Float64(chains.PipelineRunStoredName, + chains.PipelineRunStoredDesc, + stats.UnitDimensionless) + + stCountView *view.View + + mrCount = stats.Float64(chains.PipelineRunMarkedName, + chains.PipelineRunMarkedDesc, + stats.UnitDimensionless) + + mrCountView *view.View +) + +// Recorder holds keys for Tekton metrics +type Recorder struct { + initialized bool +} + +// We cannot register the view multiple times, so NewRecorder lazily +// initializes this singleton and returns the same recorder across any +// subsequent invocations. +var ( + once sync.Once + r *Recorder +) + +// NewRecorder creates a new metrics recorder instance +// to log the PipelineRun related metrics +func NewRecorder(ctx context.Context) (*Recorder, error) { + var errRegistering error + logger := logging.FromContext(ctx) + once.Do(func() { + r = &Recorder{ + initialized: true, + } + errRegistering = viewRegister() + if errRegistering != nil { + r.initialized = false + logger.Errorf("View Register Failed ", r.initialized) + return + } + }) + + return r, errRegistering +} + +func viewRegister() error { + sgCountView = &view.View{ + Description: sgCount.Description(), + Measure: sgCount, + Aggregation: view.Count(), + } + + plCountView = &view.View{ + Description: plCount.Description(), + Measure: plCount, + Aggregation: view.Count(), + } + + stCountView = &view.View{ + Description: stCount.Description(), + Measure: stCount, + Aggregation: view.Count(), + } + + mrCountView = &view.View{ + Description: mrCount.Description(), + Measure: mrCount, + Aggregation: view.Count(), + } + return view.Register( + sgCountView, + plCountView, + stCountView, + mrCountView, + ) +} + +func (r *Recorder) RecordCountMetrics(ctx context.Context, metricType string) { + logger := logging.FromContext(ctx) + if !r.initialized { + logger.Errorf("Ignoring the metrics recording as recorder not initialized ") + return + } + switch mt := metricType; mt { + case chains.SignedMessagesCount: + r.countMetrics(ctx, sgCount) + case chains.PayloadUploadeCount: + r.countMetrics(ctx, plCount) + case chains.SignsStoredCount: + r.countMetrics(ctx, stCount) + case chains.MarkedAsSignedCount: + r.countMetrics(ctx, mrCount) + default: + logger.Errorf("Ignoring the metrics recording as valid Metric type matching %v was not found", mt) + } + +} + +func (r *Recorder) countMetrics(ctx context.Context, measure *stats.Float64Measure) { + metrics.Record(ctx, measure.M(1)) +} diff --git a/pkg/pipelinerunmetrics/metrics_test.go b/pkg/pipelinerunmetrics/metrics_test.go new file mode 100644 index 0000000000..09737f8c7b --- /dev/null +++ b/pkg/pipelinerunmetrics/metrics_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2024 The Tekton 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 pipelinerunmetrics + +import ( + "context" + "sync" + "testing" + + "github.com/tektoncd/chains/pkg/chains" + "knative.dev/pkg/metrics/metricstest" + _ "knative.dev/pkg/metrics/testing" +) + +func TestUninitializedMetrics(t *testing.T) { + metrics := &Recorder{} + ctx := context.Background() + + metrics.RecordCountMetrics(ctx, chains.SignedMessagesCount) + metricstest.CheckStatsNotReported(t, chains.PipelineRunSignedName) + + metrics.RecordCountMetrics(ctx, chains.PayloadUploadeCount) + metricstest.CheckStatsNotReported(t, chains.PipelineRunUploadedName) + + metrics.RecordCountMetrics(ctx, chains.SignsStoredCount) + metricstest.CheckStatsNotReported(t, chains.PipelineRunStoredName) + + metrics.RecordCountMetrics(ctx, chains.MarkedAsSignedCount) + metricstest.CheckStatsNotReported(t, chains.PipelineRunMarkedName) +} + +func TestCountMetrics(t *testing.T) { + unregisterMetrics() + ctx := context.Background() + ctx = WithClient(ctx) + + rec := Get(ctx) + + rec.RecordCountMetrics(ctx, chains.SignedMessagesCount) + metricstest.CheckCountData(t, chains.PipelineRunSignedName, map[string]string{}, 1) + rec.RecordCountMetrics(ctx, chains.PayloadUploadeCount) + metricstest.CheckCountData(t, chains.PipelineRunUploadedName, map[string]string{}, 1) + rec.RecordCountMetrics(ctx, chains.SignsStoredCount) + metricstest.CheckCountData(t, chains.PipelineRunStoredName, map[string]string{}, 1) + rec.RecordCountMetrics(ctx, chains.MarkedAsSignedCount) + metricstest.CheckCountData(t, chains.PipelineRunMarkedName, map[string]string{}, 1) +} + +func unregisterMetrics() { + metricstest.Unregister(chains.PipelineRunSignedName, chains.PipelineRunUploadedName, chains.PipelineRunStoredName, chains.PipelineRunMarkedName) + // Allow the recorder singleton to be recreated. + once = sync.Once{} + r = nil +} diff --git a/pkg/reconciler/pipelinerun/controller.go b/pkg/reconciler/pipelinerun/controller.go index 8f5ff637ba..21beabbc47 100644 --- a/pkg/reconciler/pipelinerun/controller.go +++ b/pkg/reconciler/pipelinerun/controller.go @@ -19,6 +19,7 @@ import ( "github.com/tektoncd/chains/pkg/chains" "github.com/tektoncd/chains/pkg/chains/storage" "github.com/tektoncd/chains/pkg/config" + "github.com/tektoncd/chains/pkg/pipelinerunmetrics" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" pipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client" pipelineruninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1/pipelinerun" @@ -44,6 +45,7 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl psSigner := &chains.ObjectSigner{ SecretPath: SecretPath, Pipelineclientset: pipelineClient, + Recorder: pipelinerunmetrics.Get(ctx), } c := &Reconciler{ diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index 3d7235172c..2d7cf60a05 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -22,6 +22,7 @@ import ( "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" "github.com/tektoncd/chains/pkg/internal/mocksigner" + _ "github.com/tektoncd/chains/pkg/pipelinerunmetrics/fake" "github.com/tektoncd/chains/pkg/test/tekton" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" informers "github.com/tektoncd/pipeline/pkg/client/informers/externalversions/pipeline/v1" diff --git a/pkg/reconciler/taskrun/controller.go b/pkg/reconciler/taskrun/controller.go index 6d2eea061e..8695dcfdb3 100644 --- a/pkg/reconciler/taskrun/controller.go +++ b/pkg/reconciler/taskrun/controller.go @@ -19,6 +19,7 @@ import ( "github.com/tektoncd/chains/pkg/chains" "github.com/tektoncd/chains/pkg/chains/storage" "github.com/tektoncd/chains/pkg/config" + "github.com/tektoncd/chains/pkg/taskrunmetrics" pipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client" taskruninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1/taskrun" taskrunreconciler "github.com/tektoncd/pipeline/pkg/client/injection/reconciler/pipeline/v1/taskrun" @@ -40,6 +41,7 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl tsSigner := &chains.ObjectSigner{ SecretPath: SecretPath, Pipelineclientset: pipelineClient, + Recorder: taskrunmetrics.Get(ctx), } c := &Reconciler{ diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index 5335b44f85..20ba45023f 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -21,6 +21,7 @@ import ( "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" "github.com/tektoncd/chains/pkg/internal/mocksigner" + _ "github.com/tektoncd/chains/pkg/taskrunmetrics/fake" "github.com/tektoncd/chains/pkg/test/tekton" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" informers "github.com/tektoncd/pipeline/pkg/client/informers/externalversions/pipeline/v1" diff --git a/pkg/taskrunmetrics/fake/fake.go b/pkg/taskrunmetrics/fake/fake.go new file mode 100644 index 0000000000..a9683ec983 --- /dev/null +++ b/pkg/taskrunmetrics/fake/fake.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 The Tekton 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 fake + +import ( + "context" + + "github.com/tektoncd/chains/pkg/taskrunmetrics" + _ "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1/taskrun/fake" // Make sure the fake taskrun informer is setup + "k8s.io/client-go/rest" + "knative.dev/pkg/injection" +) + +func init() { + injection.Fake.RegisterClient(func(ctx context.Context, _ *rest.Config) context.Context { return taskrunmetrics.WithClient(ctx) }) +} diff --git a/pkg/taskrunmetrics/injection.go b/pkg/taskrunmetrics/injection.go new file mode 100644 index 0000000000..529f63cf78 --- /dev/null +++ b/pkg/taskrunmetrics/injection.go @@ -0,0 +1,50 @@ +/* +Copyright 2024 The Tekton 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 taskrunmetrics + +import ( + "context" + + "k8s.io/client-go/rest" + "knative.dev/pkg/injection" + "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterClient(func(ctx context.Context, _ *rest.Config) context.Context { return WithClient(ctx) }) +} + +// RecorderKey is used for associating the Recorder inside the context.Context. +type RecorderKey struct{} + +// WithClient adds a metrics recorder to the given context +func WithClient(ctx context.Context) context.Context { + rec, err := NewRecorder(ctx) + if err != nil { + logging.FromContext(ctx).Errorf("Failed to create taskrun metrics recorder %v", err) + } + return context.WithValue(ctx, RecorderKey{}, rec) +} + +// Get extracts the taskrunmetrics.Recorder from the context. +func Get(ctx context.Context) *Recorder { + untyped := ctx.Value(RecorderKey{}) + if untyped == nil { + logging.FromContext(ctx).Panic("Unable to fetch *taskrunmetrics.Recorder from context.") + } + return untyped.(*Recorder) +} diff --git a/pkg/taskrunmetrics/metrics.go b/pkg/taskrunmetrics/metrics.go new file mode 100644 index 0000000000..79156792f5 --- /dev/null +++ b/pkg/taskrunmetrics/metrics.go @@ -0,0 +1,145 @@ +/* +Copyright 2024 The Tekton 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 taskrunmetrics + +import ( + "context" + "sync" + + "github.com/tektoncd/chains/pkg/chains" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "knative.dev/pkg/logging" + "knative.dev/pkg/metrics" +) + +var ( + sgCountView *view.View + + sgCount = stats.Float64(chains.TaskRunSignedName, + chains.TaskRunSignedDesc, + stats.UnitDimensionless) + + plCount = stats.Float64(chains.TaskRunUploadedName, + chains.TaskRunUploadedDesc, + stats.UnitDimensionless) + + plCountView *view.View + + stCount = stats.Float64(chains.TaskRunStoredName, + chains.TaskRunStoredDesc, + stats.UnitDimensionless) + + stCountView *view.View + + mrCount = stats.Float64(chains.TaskRunMarkedName, + chains.TaskRunMarkedDesc, + stats.UnitDimensionless) + + mrCountView *view.View +) + +// Recorder is used to actually record TaskRun metrics +type Recorder struct { + initialized bool +} + +// We cannot register the view multiple times, so NewRecorder lazily +// initializes this singleton and returns the same recorder across any +// subsequent invocations. +var ( + once sync.Once + r *Recorder +) + +// NewRecorder creates a new metrics recorder instance +// to log the TaskRun related metrics +func NewRecorder(ctx context.Context) (*Recorder, error) { + var errRegistering error + logger := logging.FromContext(ctx) + once.Do(func() { + r = &Recorder{ + initialized: true, + } + errRegistering = viewRegister() + if errRegistering != nil { + r.initialized = false + logger.Errorf("View Register Failed ", r.initialized) + return + } + }) + + return r, errRegistering +} + +func viewRegister() error { + + sgCountView = &view.View{ + Description: sgCount.Description(), + Measure: sgCount, + Aggregation: view.Count(), + } + + plCountView = &view.View{ + Description: plCount.Description(), + Measure: plCount, + Aggregation: view.Count(), + } + + stCountView = &view.View{ + Description: stCount.Description(), + Measure: stCount, + Aggregation: view.Count(), + } + + mrCountView = &view.View{ + Description: mrCount.Description(), + Measure: mrCount, + Aggregation: view.Count(), + } + return view.Register( + sgCountView, + plCountView, + stCountView, + mrCountView, + ) +} + +func (r *Recorder) RecordCountMetrics(ctx context.Context, metricType string) { + logger := logging.FromContext(ctx) + + if !r.initialized { + logger.Errorf("ignoring the metrics recording as recorder not initialized ") + } + switch mt := metricType; mt { + case chains.SignedMessagesCount: + r.countMetrics(ctx, sgCount) + case chains.PayloadUploadeCount: + r.countMetrics(ctx, plCount) + case chains.SignsStoredCount: + r.countMetrics(ctx, stCount) + case chains.MarkedAsSignedCount: + r.countMetrics(ctx, mrCount) + default: + logger.Errorf("Ignoring the metrics recording as valid Metric type matching %v was not found", mt) + } + +} + +func (r *Recorder) countMetrics(ctx context.Context, measure *stats.Float64Measure) { + metrics.Record(ctx, measure.M(1)) +} diff --git a/pkg/taskrunmetrics/metrics_test.go b/pkg/taskrunmetrics/metrics_test.go new file mode 100644 index 0000000000..f644f296b0 --- /dev/null +++ b/pkg/taskrunmetrics/metrics_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2024 The Tekton 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 taskrunmetrics + +import ( + "context" + "sync" + "testing" + + "github.com/tektoncd/chains/pkg/chains" + "knative.dev/pkg/metrics/metricstest" + _ "knative.dev/pkg/metrics/testing" +) + +func TestUninitializedMetrics(t *testing.T) { + metrics := &Recorder{} + ctx := context.Background() + + metrics.RecordCountMetrics(ctx, chains.SignedMessagesCount) + metricstest.CheckStatsNotReported(t, chains.TaskRunSignedName) + + metrics.RecordCountMetrics(ctx, chains.PayloadUploadeCount) + metricstest.CheckStatsNotReported(t, chains.TaskRunUploadedName) + + metrics.RecordCountMetrics(ctx, chains.SignsStoredCount) + metricstest.CheckStatsNotReported(t, chains.TaskRunStoredName) + + metrics.RecordCountMetrics(ctx, chains.MarkedAsSignedCount) + metricstest.CheckStatsNotReported(t, chains.TaskRunMarkedName) +} + +func TestCountMetrics(t *testing.T) { + unregisterMetrics() + ctx := context.Background() + ctx = WithClient(ctx) + + rec := Get(ctx) + + rec.RecordCountMetrics(ctx, chains.SignedMessagesCount) + metricstest.CheckCountData(t, chains.TaskRunSignedName, map[string]string{}, 1) + rec.RecordCountMetrics(ctx, chains.PayloadUploadeCount) + metricstest.CheckCountData(t, chains.TaskRunUploadedName, map[string]string{}, 1) + rec.RecordCountMetrics(ctx, chains.SignsStoredCount) + metricstest.CheckCountData(t, chains.TaskRunStoredName, map[string]string{}, 1) + rec.RecordCountMetrics(ctx, chains.MarkedAsSignedCount) + metricstest.CheckCountData(t, chains.TaskRunMarkedName, map[string]string{}, 1) +} + +func unregisterMetrics() { + metricstest.Unregister(chains.TaskRunSignedName, chains.TaskRunUploadedName, chains.TaskRunStoredName, chains.TaskRunMarkedName) + // Allow the recorder singleton to be recreated. + once = sync.Once{} + r = nil +} diff --git a/vendor/knative.dev/pkg/metrics/metricstest/metricstest.go b/vendor/knative.dev/pkg/metrics/metricstest/metricstest.go new file mode 100644 index 0000000000..3d054a1830 --- /dev/null +++ b/vendor/knative.dev/pkg/metrics/metricstest/metricstest.go @@ -0,0 +1,262 @@ +/* +Copyright 2019 The Knative 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 metricstest + +import ( + "fmt" + "reflect" + + "go.opencensus.io/metric/metricproducer" + "go.opencensus.io/stats/view" +) + +type ti interface { + Helper() + Error(args ...interface{}) +} + +// CheckStatsReported checks that there is a view registered with the given name for each string in names, +// and that each view has at least one record. +func CheckStatsReported(t ti, names ...string) { + t.Helper() + for _, name := range names { + d, err := readRowsFromAllMeters(name) + if err != nil { + t.Error("For metric, Reporter.Report() error", "metric", name, "error", err) + } + if len(d) < 1 { + t.Error("For metric, no data reported when data was expected, view data is empty.", "metric", name) + } + } +} + +// CheckStatsNotReported checks that there are no records for any views that a name matching a string in names. +// Names that do not match registered views are considered not reported. +func CheckStatsNotReported(t ti, names ...string) { + t.Helper() + for _, name := range names { + d, err := readRowsFromAllMeters(name) + // err == nil means a valid stat exists matching "name" + // len(d) > 0 means a component recorded metrics for that stat + if err == nil && len(d) > 0 { + t.Error("For metric, unexpected data reported when no data was expected.", "metric", name, "Reporter len(d)", len(d)) + } + } +} + +// CheckCountData checks the view with a name matching string name to verify that the CountData stats +// reported are tagged with the tags in wantTags and that wantValue matches reported count. +func CheckCountData(t ti, name string, wantTags map[string]string, wantValue int64) { + t.Helper() + row, err := checkExactlyOneRow(t, name) + if err != nil { + t.Error(err) + return + } + checkRowTags(t, row, name, wantTags) + + if s, ok := row.Data.(*view.CountData); !ok { + t.Error("want CountData", "metric", name, "got", reflect.TypeOf(row.Data)) + } else if s.Value != wantValue { + t.Error("Wrong value", "metric", name, "value", s.Value, "want", wantValue) + } +} + +// CheckDistributionData checks the view with a name matching string name to verify that the DistributionData stats reported +// are tagged with the tags in wantTags and that expectedCount number of records were reported. +// It also checks that expectedMin and expectedMax match the minimum and maximum reported values, respectively. +func CheckDistributionData(t ti, name string, wantTags map[string]string, expectedCount int64, expectedMin float64, expectedMax float64) { + t.Helper() + row, err := checkExactlyOneRow(t, name) + if err != nil { + t.Error(err) + return + } + checkRowTags(t, row, name, wantTags) + + if s, ok := row.Data.(*view.DistributionData); !ok { + t.Error("want DistributionData", "metric", name, "got", reflect.TypeOf(row.Data)) + } else { + if s.Count != expectedCount { + t.Error("reporter count wrong", "metric", name, "got", s.Count, "want", expectedCount) + } + if s.Min != expectedMin { + t.Error("reporter min wrong", "metric", name, "got", s.Min, "want", expectedMin) + } + if s.Max != expectedMax { + t.Error("reporter max wrong", "metric", name, "got", s.Max, "want", expectedMax) + } + } +} + +// CheckDistributionCount checks the view with a name matching string name to verify that the DistributionData stats reported +// are tagged with the tags in wantTags and that expectedCount number of records were reported. +func CheckDistributionCount(t ti, name string, wantTags map[string]string, expectedCount int64) { + t.Helper() + row, err := checkExactlyOneRow(t, name) + if err != nil { + t.Error(err) + return + } + checkRowTags(t, row, name, wantTags) + + if s, ok := row.Data.(*view.DistributionData); !ok { + t.Error("want DistributionData", "metric", name, "got", reflect.TypeOf(row.Data)) + } else if s.Count != expectedCount { + t.Error("reporter count wrong", "metric", name, "got", s.Count, "want", expectedCount) + } + +} + +// GetLastValueData returns the last value for the given metric, verifying tags. +func GetLastValueData(t ti, name string, tags map[string]string) float64 { + t.Helper() + return GetLastValueDataWithMeter(t, name, tags, nil) +} + +// GetLastValueDataWithMeter returns the last value of the given metric using meter, verifying tags. +func GetLastValueDataWithMeter(t ti, name string, tags map[string]string, meter view.Meter) float64 { + t.Helper() + if row := lastRow(t, name, meter); row != nil { + checkRowTags(t, row, name, tags) + + s, ok := row.Data.(*view.LastValueData) + if !ok { + t.Error("want LastValueData", "metric", name, "got", reflect.TypeOf(row.Data)) + } + return s.Value + } + return 0 +} + +// CheckLastValueData checks the view with a name matching string name to verify that the LastValueData stats +// reported are tagged with the tags in wantTags and that wantValue matches reported last value. +func CheckLastValueData(t ti, name string, wantTags map[string]string, wantValue float64) { + t.Helper() + CheckLastValueDataWithMeter(t, name, wantTags, wantValue, nil) +} + +// CheckLastValueDataWithMeter checks the view with a name matching the string name in the +// specified Meter (resource-specific view) to verify that the LastValueData stats are tagged with +// the tags in wantTags and that wantValue matches the last reported value. +func CheckLastValueDataWithMeter(t ti, name string, wantTags map[string]string, wantValue float64, meter view.Meter) { + t.Helper() + if v := GetLastValueDataWithMeter(t, name, wantTags, meter); v != wantValue { + t.Error("Reporter.Report() wrong value", "metric", name, "got", v, "want", wantValue) + } +} + +// CheckSumData checks the view with a name matching string name to verify that the SumData stats +// reported are tagged with the tags in wantTags and that wantValue matches the reported sum. +func CheckSumData(t ti, name string, wantTags map[string]string, wantValue float64) { + t.Helper() + row, err := checkExactlyOneRow(t, name) + if err != nil { + t.Error(err) + return + } + checkRowTags(t, row, name, wantTags) + + if s, ok := row.Data.(*view.SumData); !ok { + t.Error("Wrong type", "metric", name, "got", reflect.TypeOf(row.Data), "want", "SumData") + } else if s.Value != wantValue { + t.Error("Wrong sumdata", "metric", name, "got", s.Value, "want", wantValue) + } +} + +// Unregister unregisters the metrics that were registered. +// This is useful for testing since golang execute test iterations within the same process and +// opencensus views maintain global state. At the beginning of each test, tests should +// unregister for all metrics and then re-register for the same metrics. This effectively clears +// out any existing data and avoids a panic due to re-registering a metric. +// +// In normal process shutdown, metrics do not need to be unregistered. +func Unregister(names ...string) { + for _, producer := range metricproducer.GlobalManager().GetAll() { + meter := producer.(view.Meter) + for _, n := range names { + if v := meter.Find(n); v != nil { + meter.Unregister(v) + } + } + } +} + +func lastRow(t ti, name string, meter view.Meter) *view.Row { + t.Helper() + var d []*view.Row + var err error + if meter != nil { + d, err = meter.RetrieveData(name) + } else { + d, err = readRowsFromAllMeters(name) + } + if err != nil { + t.Error("Reporter.Report() error", "metric", name, "error", err) + return nil + } + if len(d) < 1 { + t.Error("Reporter.Report() wrong length", "metric", name, "got", len(d), "want at least", 1) + return nil + } + + return d[len(d)-1] +} + +func checkExactlyOneRow(t ti, name string) (*view.Row, error) { + rows, err := readRowsFromAllMeters(name) + if err != nil || len(rows) == 0 { + return nil, fmt.Errorf("could not find row for %q", name) + } + if len(rows) > 1 { + return nil, fmt.Errorf("expected 1 row for metric %q got %d", name, len(rows)) + } + return rows[0], nil +} + +func readRowsFromAllMeters(name string) ([]*view.Row, error) { + // view.Meter implements (and is exposed by) metricproducer.GetAll. Since + // this is a test, reach around and cast these to view.Meter. + var rows []*view.Row + for _, producer := range metricproducer.GlobalManager().GetAll() { + meter := producer.(view.Meter) + d, err := meter.RetrieveData(name) + if err != nil || len(d) == 0 { + continue + } + if rows != nil { + return nil, fmt.Errorf("got metrics for the same name from different meters: %+v, %+v", rows, d) + } + rows = d + } + return rows, nil +} + +func checkRowTags(t ti, row *view.Row, name string, wantTags map[string]string) { + t.Helper() + if wantlen, gotlen := len(wantTags), len(row.Tags); gotlen != wantlen { + t.Error("Reporter got wrong number of tags", "metric", name, "got", gotlen, "want", wantlen) + } + for _, got := range row.Tags { + n := got.Key.Name() + if want, ok := wantTags[n]; !ok { + t.Error("Reporter got an extra tag", "metric", name, "gotName", n, "gotValue", got.Value) + } else if got.Value != want { + t.Error("Reporter expected a different tag value for key", "metric", name, "key", n, "got", got.Value, "want", want) + } + } +} diff --git a/vendor/knative.dev/pkg/metrics/metricstest/resource_metrics.go b/vendor/knative.dev/pkg/metrics/metricstest/resource_metrics.go new file mode 100644 index 0000000000..10e2e4c94a --- /dev/null +++ b/vendor/knative.dev/pkg/metrics/metricstest/resource_metrics.go @@ -0,0 +1,350 @@ +/* +Copyright 2020 The Knative 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 metricstest simplifies some of the common boilerplate around testing +// metrics exports. It should work with or without the code in metrics, but this +// code particularly knows how to deal with metrics which are exported for +// multiple Resources in the same process. +package metricstest + +import ( + "fmt" + "sort" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "go.opencensus.io/metric/metricdata" + "go.opencensus.io/metric/metricproducer" + "go.opencensus.io/resource" + "go.opencensus.io/stats/view" +) + +// Value provides a simplified implementation of a metric Value suitable for +// easy testing. +type Value struct { + Tags map[string]string + // union interface, only one of these will be set + Int64 *int64 + Float64 *float64 + Distribution *metricdata.Distribution + // VerifyDistributionCountOnly makes Equal compare the Distribution with the + // field Count only, and ignore all other fields of Distribution. + // This is ignored when the value is not a Distribution. + VerifyDistributionCountOnly bool +} + +// Metric provides a simplified (for testing) implementation of a metric report +// for a given metric name in a given Resource. +type Metric struct { + // Name is the exported name of the metric, probably from the View's name. + Name string + // Unit is the units of measure of the metric. This is only checked for + // equality if Unit is non-empty or VerifyMetadata is true on both Metrics. + Unit metricdata.Unit + // Type is the type of measurement represented by the metric. This is only + // checked for equality if VerifyMetadata is true on both Metrics. + Type metricdata.Type + + // Resource is the reported Resource (if any) for this metric. This is only + // checked for equality if Resource is non-nil or VerifyResource is true on + // both Metrics. + Resource *resource.Resource + + // Values contains the values recorded for different Key=Value Tag + // combinations. Value is checked for equality if present. + Values []Value + + // Equality testing/validation settings on the Metric. These are used to + // allow simple construction and usage with github.com/google/go-cmp/cmp + + // VerifyMetadata makes Equal compare Unit and Type if it is true on both + // Metrics. + VerifyMetadata bool + // VerifyResource makes Equal compare Resource if it is true on Metrics with + // nil Resource. Metrics with non-nil Resource are always compared. + VerifyResource bool +} + +// NewMetric creates a Metric from a metricdata.Metric, which is designed for +// compact wire representation. +func NewMetric(metric *metricdata.Metric) Metric { + value := Metric{ + Name: metric.Descriptor.Name, + Unit: metric.Descriptor.Unit, + Type: metric.Descriptor.Type, + Resource: metric.Resource, + + VerifyMetadata: true, + VerifyResource: true, + + Values: make([]Value, 0, len(metric.TimeSeries)), + } + + for _, ts := range metric.TimeSeries { + tags := make(map[string]string, len(metric.Descriptor.LabelKeys)) + for i, k := range metric.Descriptor.LabelKeys { + if ts.LabelValues[i].Present { + tags[k.Key] = ts.LabelValues[i].Value + } + } + v := Value{Tags: tags} + ts.Points[0].ReadValue(&v) + value.Values = append(value.Values, v) + } + + return value +} + +// EnsureRecorded makes sure that all stats metrics are actually flushed and recorded. +func EnsureRecorded() { + // stats.Record queues the actual record to a channel to be accounted for by + // a background goroutine (nonblocking). Call a method which does a + // round-trip to that goroutine to ensure that records have been flushed. + for _, producer := range metricproducer.GlobalManager().GetAll() { + if meter, ok := producer.(view.Meter); ok { + meter.Find("nonexistent") + } + } +} + +// GetMetric returns all values for the named metric. +func GetMetric(name string) []Metric { + producers := metricproducer.GlobalManager().GetAll() + retval := make([]Metric, 0, len(producers)) + for _, p := range producers { + for _, m := range p.Read() { + if m.Descriptor.Name == name && len(m.TimeSeries) > 0 { + retval = append(retval, NewMetric(m)) + } + } + } + return retval +} + +// GetOneMetric is like GetMetric, but it panics if more than a single Metric is +// found. +func GetOneMetric(name string) Metric { + m := GetMetric(name) + if len(m) != 1 { + panic(fmt.Sprint("Got wrong number of metrics:", m)) + } + return m[0] +} + +// IntMetric creates an Int64 metric. +func IntMetric(name string, value int64, tags map[string]string) Metric { + return Metric{ + Name: name, + Values: []Value{{Int64: &value, Tags: tags}}, + } +} + +// FloatMetric creates a Float64 metric +func FloatMetric(name string, value float64, tags map[string]string) Metric { + return Metric{ + Name: name, + Values: []Value{{Float64: &value, Tags: tags}}, + } +} + +// DistributionCountOnlyMetric creates a distribution metric for test, and verifying only the count. +func DistributionCountOnlyMetric(name string, count int64, tags map[string]string) Metric { + return Metric{ + Name: name, + Values: []Value{{ + Distribution: &metricdata.Distribution{Count: count}, + Tags: tags, + VerifyDistributionCountOnly: true}}, + } +} + +// WithResource sets the resource of the metric. +func (m Metric) WithResource(r *resource.Resource) Metric { + m.Resource = r + return m +} + +// AssertMetric verifies that the metrics have the specified values. Note that +// this method will spuriously fail if there are multiple metrics with the same +// name on different Meters. Calls EnsureRecorded internally before fetching the +// batch of metrics. +func AssertMetric(t *testing.T, values ...Metric) { + t.Helper() + EnsureRecorded() + for _, v := range values { + if diff := cmp.Diff(v, GetOneMetric(v.Name)); diff != "" { + t.Error("Wrong metric (-want +got):", diff) + } + } +} + +// AssertMetricExists verifies that at least one metric values has been reported for +// each of metric names. +// Calls EnsureRecorded internally before fetching the batch of metrics. +func AssertMetricExists(t *testing.T, names ...string) { + metrics := make([]Metric, 0, len(names)) + for _, n := range names { + metrics = append(metrics, Metric{Name: n}) + } + AssertMetric(t, metrics...) +} + +// AssertNoMetric verifies that no metrics have been reported for any of the +// metric names. +// Calls EnsureRecorded internally before fetching the batch of metrics. +func AssertNoMetric(t *testing.T, names ...string) { + t.Helper() + EnsureRecorded() + for _, name := range names { + if m := GetMetric(name); len(m) != 0 { + t.Error("Found unexpected data for:", m) + } + } +} + +// VisitFloat64Value implements metricdata.ValueVisitor. +func (v *Value) VisitFloat64Value(f float64) { + v.Float64 = &f + v.Int64 = nil + v.Distribution = nil +} + +// VisitInt64Value implements metricdata.ValueVisitor. +func (v *Value) VisitInt64Value(i int64) { + v.Int64 = &i + v.Float64 = nil + v.Distribution = nil +} + +// VisitDistributionValue implements metricdata.ValueVisitor. +func (v *Value) VisitDistributionValue(d *metricdata.Distribution) { + v.Distribution = d + v.Int64 = nil + v.Float64 = nil +} + +// VisitSummaryValue implements metricdata.ValueVisitor. +func (v *Value) VisitSummaryValue(*metricdata.Summary) { + panic("Attempted to fetch summary value, which we never use!") +} + +// Equal provides a contract for use with github.com/google/go-cmp/cmp. Due to +// the reflection in cmp, it only works if the type of the two arguments to cmp +// are the same. +func (m Metric) Equal(other Metric) bool { + if m.Name != other.Name { + return false + } + if (m.Unit != "" || m.VerifyMetadata) && (other.Unit != "" || other.VerifyMetadata) { + if m.Unit != other.Unit { + return false + } + } + if m.VerifyMetadata && other.VerifyMetadata { + if m.Type != other.Type { + return false + } + } + + if (m.Resource != nil || m.VerifyResource) && (other.Resource != nil || other.VerifyResource) { + if !cmp.Equal(m.Resource, other.Resource) { + return false + } + } + + if len(m.Values) > 0 && len(other.Values) > 0 { + if len(m.Values) != len(other.Values) { + return false + } + myValues := make(map[string]Value, len(m.Values)) + for _, v := range m.Values { + myValues[tagsToString(v.Tags)] = v + } + for _, v := range other.Values { + myV, ok := myValues[tagsToString(v.Tags)] + if !ok || !myV.Equal(v) { + return false + } + } + } + + return true +} + +// Equal provides a contract for github.com/google/go-cmp/cmp. It compares two +// values, including deep comparison of Distributions. (Exemplars are +// intentional not included in the comparison, but other fields are considered). +func (v Value) Equal(other Value) bool { + if len(v.Tags) != len(other.Tags) { + return false + } + for k, v := range v.Tags { + if v != other.Tags[k] { + return false + } + } + if v.Int64 != nil { + return other.Int64 != nil && *v.Int64 == *other.Int64 + } + if v.Float64 != nil { + return other.Float64 != nil && *v.Float64 == *other.Float64 + } + + if v.Distribution != nil { + if other.Distribution == nil { + return false + } + if v.Distribution.Count != other.Distribution.Count { + return false + } + if v.VerifyDistributionCountOnly || other.VerifyDistributionCountOnly { + return true + } + if v.Distribution.Sum != other.Distribution.Sum { + return false + } + if v.Distribution.SumOfSquaredDeviation != other.Distribution.SumOfSquaredDeviation { + return false + } + if v.Distribution.BucketOptions != nil { + if other.Distribution.BucketOptions == nil { + return false + } + for i, bo := range v.Distribution.BucketOptions.Bounds { + if bo != other.Distribution.BucketOptions.Bounds[i] { + return false + } + } + } + for i, b := range v.Distribution.Buckets { + if b.Count != other.Distribution.Buckets[i].Count { + return false + } + } + } + + return true +} + +func tagsToString(tags map[string]string) string { + pairs := make([]string, 0, len(tags)) + for k, v := range tags { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(pairs) + return strings.Join(pairs, ",") +} diff --git a/vendor/knative.dev/pkg/metrics/testing/config.go b/vendor/knative.dev/pkg/metrics/testing/config.go new file mode 100644 index 0000000000..2df88b6e09 --- /dev/null +++ b/vendor/knative.dev/pkg/metrics/testing/config.go @@ -0,0 +1,28 @@ +/* +Copyright 2019 The Knative 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 + + https://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 testing + +import ( + "os" + + "knative.dev/pkg/metrics" +) + +func init() { + os.Setenv(metrics.DomainEnv, "knative.dev/testing") + metrics.InitForTesting() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 92be40d4fb..8b473097ff 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -3063,6 +3063,8 @@ knative.dev/pkg/logging/logkey knative.dev/pkg/logging/testing knative.dev/pkg/metrics knative.dev/pkg/metrics/metricskey +knative.dev/pkg/metrics/metricstest +knative.dev/pkg/metrics/testing knative.dev/pkg/network knative.dev/pkg/network/handlers knative.dev/pkg/profiling