From 66473ca9c8d5ed870038634f41bcb413f53c70e2 Mon Sep 17 00:00:00 2001 From: Khurram Baig Date: Thu, 22 Jun 2023 17:13:11 +0530 Subject: [PATCH] Add Metrics for TektonResult Added Metrics `results_reconciled` which tells us what type of logging is being used for results installation. This metric can be use for telemetry. --- pkg/reconciler/common/initcontroller.go | 3 + .../kubernetes/tektonresult/controller.go | 27 +- .../kubernetes/tektonresult/metrics.go | 108 ++++++ .../kubernetes/tektonresult/metrics_test.go | 47 +++ .../kubernetes/tektonresult/tektonresult.go | 5 +- .../pkg/metrics/metricstest/metricstest.go | 262 +++++++++++++ .../metrics/metricstest/resource_metrics.go | 350 ++++++++++++++++++ .../knative.dev/pkg/metrics/testing/config.go | 28 ++ vendor/modules.txt | 2 + 9 files changed, 818 insertions(+), 14 deletions(-) create mode 100644 pkg/reconciler/kubernetes/tektonresult/metrics.go create mode 100644 pkg/reconciler/kubernetes/tektonresult/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/reconciler/common/initcontroller.go b/pkg/reconciler/common/initcontroller.go index 2505742a6e..2b5aaa6789 100644 --- a/pkg/reconciler/common/initcontroller.go +++ b/pkg/reconciler/common/initcontroller.go @@ -112,6 +112,9 @@ func (ctrl Controller) fetchSourceManifests(ctx context.Context, opts PayloadOpt case "chains": var chain v1alpha1.TektonChain return AppendTarget(ctx, ctrl.Manifest, &chain) + case "results": + var results v1alpha1.TektonResult + return AppendTarget(ctx, ctrl.Manifest, &results) case "pipelines-as-code": pacLocation := filepath.Join(os.Getenv(KoEnvKey), "tekton-addon", "pipelines-as-code") return AppendManifest(ctrl.Manifest, pacLocation) diff --git a/pkg/reconciler/kubernetes/tektonresult/controller.go b/pkg/reconciler/kubernetes/tektonresult/controller.go index 9a528c4649..fc444c1ab0 100644 --- a/pkg/reconciler/kubernetes/tektonresult/controller.go +++ b/pkg/reconciler/kubernetes/tektonresult/controller.go @@ -19,9 +19,6 @@ package tektonresult import ( "context" - "github.com/go-logr/zapr" - mfc "github.com/manifestival/client-go-client" - mf "github.com/manifestival/manifestival" "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" operatorclient "github.com/tektoncd/operator/pkg/client/injection/client" tektonInstallerinformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektoninstallerset" @@ -29,7 +26,6 @@ import ( tektonResultInformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonresult" tektonResultReconciler "github.com/tektoncd/operator/pkg/client/injection/reconciler/operator/v1alpha1/tektonresult" "github.com/tektoncd/operator/pkg/reconciler/common" - "go.uber.org/zap" "k8s.io/client-go/tools/cache" kubeclient "knative.dev/pkg/client/injection/kube/client" "knative.dev/pkg/configmap" @@ -38,6 +34,8 @@ import ( "knative.dev/pkg/logging" ) +const versionConfigMap = "results-info" + // NewController initializes the controller and is called by the generated code // Registers event handlers to enqueue events func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { @@ -49,14 +47,14 @@ func NewExtendedController(generator common.ExtensionGenerator) injection.Contro return func(ctx context.Context, cmw configmap.Watcher) *controller.Impl { logger := logging.FromContext(ctx) - mfclient, err := mfc.NewClient(injection.GetConfig(ctx)) - if err != nil { - logger.Fatalw("Error creating client from injected config", zap.Error(err)) + ctrl := common.Controller{ + Logger: logger, + VersionConfigMap: versionConfigMap, } - mflogger := zapr.NewLogger(logger.Named("manifestival").Desugar()) - manifest, err := mf.ManifestFrom(mf.Slice{}, mf.UseClient(mfclient), mf.UseLogger(mflogger)) - if err != nil { - logger.Fatalw("Error creating initial manifest", zap.Error(err)) + + manifest, resultsVer := ctrl.InitController(ctx, common.PayloadOptions{}) + if resultsVer == common.ReleaseVersionUnknown { + resultsVer = "devel" } operatorVer, err := common.OperatorVersion(ctx) @@ -64,8 +62,9 @@ func NewExtendedController(generator common.ExtensionGenerator) injection.Contro logger.Fatal(err) } - if err := common.AppendTarget(ctx, &manifest, &v1alpha1.TektonResult{}); err != nil { - logger.Fatalw("Error fetching manifests", zap.Error(err)) + recorder, err := NewRecorder() + if err != nil { + logger.Fatalw("Error starting Results metrics") } c := &Reconciler{ @@ -75,6 +74,8 @@ func NewExtendedController(generator common.ExtensionGenerator) injection.Contro manifest: manifest, pipelineInformer: tektonPipelineInformer.Get(ctx), operatorVersion: operatorVer, + resultsVersion: resultsVer, + recorder: recorder, } impl := tektonResultReconciler.NewImpl(ctx, c) diff --git a/pkg/reconciler/kubernetes/tektonresult/metrics.go b/pkg/reconciler/kubernetes/tektonresult/metrics.go new file mode 100644 index 0000000000..4468577746 --- /dev/null +++ b/pkg/reconciler/kubernetes/tektonresult/metrics.go @@ -0,0 +1,108 @@ +/* +Copyright 2023 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 tektonresult + +import ( + "context" + "fmt" + + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + "go.uber.org/zap" + "knative.dev/pkg/metrics" +) + +var ( + rReconcileCount = stats.Float64("results_reconciled", + "results reconciled with their log type", + stats.UnitDimensionless) + rReconcilerCountView *view.View + + errUninitializedRecorder = fmt.Errorf("ignoring the metrics recording for result failed to initialize the metrics recorder") +) + +// Recorder holds keys for Tekton metrics +type Recorder struct { + initialized bool + version tag.Key + logType tag.Key +} + +// NewRecorder creates a new metrics recorder instance +// to log the PipelineRun related metrics +func NewRecorder() (*Recorder, error) { + r := &Recorder{ + initialized: true, + } + + version, err := tag.NewKey("version") + if err != nil { + return nil, err + } + r.version = version + + logType, err := tag.NewKey("log_type") + if err != nil { + return nil, err + } + r.logType = logType + + rReconcilerCountView = &view.View{ + Description: rReconcileCount.Description(), + Measure: rReconcileCount, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{r.version, r.logType}, + } + + err = view.Register(rReconcilerCountView) + + if err != nil { + r.initialized = false + return r, err + } + + return r, nil +} + +// Record the Results reconciled with their log type +func (r *Recorder) Count(version, logType string) error { + if !r.initialized { + return errUninitializedRecorder + } + + ctx, err := tag.New( + context.Background(), + tag.Insert(r.version, version), + tag.Insert(r.logType, logType), + ) + + if err != nil { + return err + } + + metrics.Record(ctx, rReconcileCount.M(float64(1))) + return nil +} + +func (m *Recorder) LogMetrics(version string, spec v1alpha1.TektonResultSpec, logger *zap.SugaredLogger) { + err := m.Count(version, spec.LogsType) + if err != nil { + logger.Warnf("%v: Failed to log the metrics : %v", v1alpha1.KindTektonResult, err) + } +} diff --git a/pkg/reconciler/kubernetes/tektonresult/metrics_test.go b/pkg/reconciler/kubernetes/tektonresult/metrics_test.go new file mode 100644 index 0000000000..0d7c18b129 --- /dev/null +++ b/pkg/reconciler/kubernetes/tektonresult/metrics_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2023 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 tektonresult + +import ( + "testing" + + "knative.dev/pkg/metrics/metricstest" // Required to setup metrics env for testing + _ "knative.dev/pkg/metrics/testing" +) + +func TestUninitializedMetrics(t *testing.T) { + recorder := Recorder{} + if err := recorder.Count("v0.1", "GCS"); err != errUninitializedRecorder { + t.Errorf("recorder.Count recording expected to return error %s but got %s", errUninitializedRecorder.Error(), err.Error()) + } +} + +func TestMetricsCount(t *testing.T) { + + metricstest.Unregister("results_reconciled") + + recorder, err := NewRecorder() + if err != nil { + t.Errorf("failed to initilized recorder, got %s", err.Error()) + + } + if err := recorder.Count("v0.1", "GCS"); err != nil { + t.Errorf("recorder.Count recording failed got %s", err.Error()) + } + metricstest.CheckStatsReported(t, "results_reconciled") + metricstest.CheckLastValueData(t, "results_reconciled", map[string]string{"version": "v0.1", "log_type": "GCS"}, float64(1)) +} diff --git a/pkg/reconciler/kubernetes/tektonresult/tektonresult.go b/pkg/reconciler/kubernetes/tektonresult/tektonresult.go index 8a91ae9393..6f1a8124f0 100644 --- a/pkg/reconciler/kubernetes/tektonresult/tektonresult.go +++ b/pkg/reconciler/kubernetes/tektonresult/tektonresult.go @@ -60,6 +60,8 @@ type Reconciler struct { pipelineInformer pipelineInformer.TektonPipelineInformer operatorVersion string + resultsVersion string + recorder *Recorder } // Check that our Reconciler implements controller.Reconciler @@ -103,6 +105,8 @@ func (r *Reconciler) FinalizeKind(ctx context.Context, original *v1alpha1.Tekton // converge the two. func (r *Reconciler) ReconcileKind(ctx context.Context, tr *v1alpha1.TektonResult) pkgreconciler.Event { logger := logging.FromContext(ctx) + defer r.recorder.LogMetrics(r.resultsVersion, tr.Spec, logger) + tr.Status.InitializeConditions() tr.Status.ObservedGeneration = tr.Generation @@ -157,7 +161,6 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tr *v1alpha1.TektonResul if err != nil { return err } - return r.updateTektonResultsStatus(ctx, tr, createdIs) } 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 091d2308bc..4cdc957e29 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2170,6 +2170,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