mirror of https://github.com/knative/pkg.git
546 lines
18 KiB
Go
546 lines
18 KiB
Go
/*
|
|
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 metrics
|
|
|
|
import (
|
|
"context"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
sd "contrib.go.opencensus.io/exporter/stackdriver"
|
|
"github.com/google/go-cmp/cmp"
|
|
"go.opencensus.io/metric/metricdata"
|
|
"go.opencensus.io/metric/metricexport"
|
|
"go.opencensus.io/resource"
|
|
"go.opencensus.io/stats"
|
|
"go.opencensus.io/stats/view"
|
|
"go.opencensus.io/tag"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
. "knative.dev/pkg/logging/testing"
|
|
"knative.dev/pkg/metrics/metricskey"
|
|
)
|
|
|
|
// TODO UTs should move to eventing and serving, as appropriate.
|
|
// See https://github.com/knative/pkg/issues/608
|
|
|
|
var testGcpMetadata = gcpMetadata{
|
|
project: "test-project",
|
|
location: "test-location",
|
|
cluster: "test-cluster",
|
|
}
|
|
|
|
func fakeGcpMetadataFunc() *gcpMetadata {
|
|
// the caller of this function could modify the struct, so we need a copy if we don't want the original modified.
|
|
newTestGCPMetadata := testGcpMetadata
|
|
return &newTestGCPMetadata
|
|
}
|
|
|
|
type fakeExporter struct{}
|
|
|
|
func (fe *fakeExporter) ExportView(vd *view.Data) {}
|
|
func (fe *fakeExporter) Flush() {}
|
|
|
|
func newFakeExporter(o sd.Options) (view.Exporter, error) {
|
|
return &fakeExporter{}, nil
|
|
}
|
|
|
|
func makeResourceLabels(kv ...string) map[string]string {
|
|
retval := map[string]string{
|
|
metricskey.LabelProject: testGcpMetadata.project,
|
|
metricskey.LabelLocation: testGcpMetadata.location,
|
|
metricskey.LabelClusterName: testGcpMetadata.cluster,
|
|
metricskey.LabelNamespaceName: testNS,
|
|
}
|
|
for i := 0; i+1 < len(kv); i += 2 {
|
|
retval[kv[i]] = kv[i+1]
|
|
}
|
|
return retval
|
|
}
|
|
|
|
type metricExtractor struct {
|
|
data []*metricdata.Metric
|
|
}
|
|
|
|
func (me *metricExtractor) ExportMetrics(ctx context.Context, data []*metricdata.Metric) error {
|
|
me.data = data
|
|
return nil
|
|
}
|
|
|
|
func TestSdRecordWithResources(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
domain string
|
|
component string
|
|
metricName string
|
|
allowCustomMetrics bool
|
|
metricTags map[string]string
|
|
resource resource.Resource
|
|
expectedLabels map[string]string
|
|
expectedResource map[string]string
|
|
}{{
|
|
name: "Serving resource and metric labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
metricTags: map[string]string{
|
|
metricskey.ContainerName: testContainer,
|
|
metricskey.PodName: testPod,
|
|
metricskey.LabelResponseCodeClass: "2xx",
|
|
metricskey.LabelResponseCode: "200",
|
|
},
|
|
resource: resource.Resource{
|
|
Labels: map[string]string{
|
|
metricskey.LabelConfigurationName: testConfiguration,
|
|
metricskey.LabelNamespaceName: testNS,
|
|
metricskey.LabelServiceName: testService,
|
|
metricskey.LabelRevisionName: testRevision,
|
|
},
|
|
},
|
|
expectedLabels: map[string]string{
|
|
metricskey.ContainerName: testContainer,
|
|
metricskey.PodName: testPod,
|
|
metricskey.LabelResponseCodeClass: "2xx",
|
|
metricskey.LabelResponseCode: "200",
|
|
},
|
|
expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
|
|
metricskey.LabelConfigurationName, testConfiguration,
|
|
metricskey.LabelRevisionName, testRevision),
|
|
}, {
|
|
name: "Serving only resource labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
resource: resource.Resource{Labels: map[string]string{
|
|
metricskey.LabelConfigurationName: testConfiguration,
|
|
metricskey.LabelNamespaceName: testNS,
|
|
metricskey.LabelServiceName: testService,
|
|
metricskey.LabelRevisionName: testRevision,
|
|
}},
|
|
expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
|
|
metricskey.LabelConfigurationName, testConfiguration,
|
|
metricskey.LabelRevisionName, testRevision),
|
|
}, {
|
|
name: "Serving resource labels overwrite metric labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
metricTags: map[string]string{
|
|
metricskey.LabelNamespaceName: testNS,
|
|
metricskey.LabelServiceName: testService,
|
|
},
|
|
resource: resource.Resource{Labels: map[string]string{
|
|
metricskey.LabelNamespaceName: "foo",
|
|
metricskey.LabelServiceName: "bar",
|
|
metricskey.LabelRevisionName: testRevision,
|
|
}},
|
|
expectedResource: makeResourceLabels(metricskey.LabelNamespaceName, "foo",
|
|
metricskey.LabelServiceName, "bar",
|
|
metricskey.LabelConfigurationName, metricskey.ValueUnknown,
|
|
metricskey.LabelRevisionName, testRevision),
|
|
}, {
|
|
name: "Serving only metric labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
metricTags: map[string]string{
|
|
metricskey.LabelNamespaceName: testNS,
|
|
metricskey.LabelServiceName: testService,
|
|
metricskey.LabelRevisionName: testRevision,
|
|
metricskey.ContainerName: testContainer,
|
|
metricskey.PodName: testPod,
|
|
metricskey.LabelResponseCodeClass: "2xx",
|
|
metricskey.LabelResponseCode: "200",
|
|
},
|
|
expectedLabels: map[string]string{
|
|
metricskey.ContainerName: testContainer,
|
|
metricskey.PodName: testPod,
|
|
metricskey.LabelResponseCodeClass: "2xx",
|
|
metricskey.LabelResponseCode: "200",
|
|
},
|
|
expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
|
|
metricskey.LabelConfigurationName, metricskey.ValueUnknown,
|
|
metricskey.LabelRevisionName, testRevision),
|
|
}, {
|
|
name: "Serving only metric labels with allowCustomMetrics",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
allowCustomMetrics: true,
|
|
metricTags: map[string]string{
|
|
metricskey.LabelNamespaceName: testNS,
|
|
metricskey.LabelServiceName: testService,
|
|
metricskey.LabelRevisionName: testRevision,
|
|
metricskey.ContainerName: testContainer,
|
|
metricskey.PodName: testPod,
|
|
metricskey.LabelResponseCodeClass: "2xx",
|
|
metricskey.LabelResponseCode: "200",
|
|
},
|
|
expectedLabels: map[string]string{
|
|
metricskey.ContainerName: testContainer,
|
|
metricskey.PodName: testPod,
|
|
metricskey.LabelResponseCodeClass: "2xx",
|
|
metricskey.LabelResponseCode: "200",
|
|
},
|
|
expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
|
|
metricskey.LabelConfigurationName, metricskey.ValueUnknown,
|
|
metricskey.LabelRevisionName, testRevision),
|
|
}, {
|
|
name: "Eventing broker metrics",
|
|
domain: internalEventingDomain,
|
|
component: "broker",
|
|
metricName: "event_count",
|
|
}, {
|
|
name: "Eventing trigger metrics",
|
|
domain: internalEventingDomain,
|
|
component: "trigger",
|
|
metricName: "event_processing_latencies",
|
|
}, {
|
|
name: "Eventing source metrics",
|
|
domain: eventingDomain,
|
|
component: "source",
|
|
metricName: "event_count",
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
recordFunc := sdCustomMetricsRecorder(metricsConfig{
|
|
stackdriverMetricTypePrefix: path.Join(tc.domain, tc.component),
|
|
}, tc.allowCustomMetrics)
|
|
m := stats.Int64(tc.metricName, "", "1")
|
|
v := &view.View{
|
|
Name: "test_" + tc.metricName,
|
|
Measure: m,
|
|
|
|
Aggregation: view.Count(),
|
|
}
|
|
for k := range tc.metricTags {
|
|
v.TagKeys = append(v.TagKeys, tag.MustNewKey(k))
|
|
}
|
|
if err := RegisterResourceView(v); err != nil {
|
|
t.Error("Unable to register view:", err)
|
|
}
|
|
defer UnregisterResourceView(v)
|
|
|
|
ctx := context.Background()
|
|
ctx = metricskey.WithResource(ctx, tc.resource)
|
|
tags := make([]tag.Mutator, 0, len(tc.metricTags))
|
|
for k, v := range tc.metricTags {
|
|
tags = append(tags, tag.Upsert(tag.MustNewKey(k), v))
|
|
}
|
|
ctx, err := tag.New(ctx, tags...)
|
|
if err != nil {
|
|
t.Error("Unable to set tags:", err)
|
|
}
|
|
|
|
if err := recordFunc(ctx, []stats.Measurement{m.M(1)}); err != nil {
|
|
t.Errorf("Record %q failed: %v", tc.metricName, err)
|
|
}
|
|
|
|
// We need to sleep for a moment because stats.Record happens on a
|
|
// background thread, and ReadAndExport happens on the local thread.
|
|
// (This is probably an opencensus bug!)
|
|
time.Sleep(1 * time.Millisecond)
|
|
|
|
me := metricExtractor{}
|
|
metricexport.NewReader().ReadAndExport(&me)
|
|
|
|
if len(me.data) != 1 {
|
|
t.Fatalf("Expected exactly one metric: %+v", me.data)
|
|
}
|
|
if len(me.data[0].TimeSeries) != 1 {
|
|
t.Errorf("Expected exactly one row: %+v", me.data[0].TimeSeries)
|
|
}
|
|
|
|
if tc.expectedResource != nil {
|
|
if diff := cmp.Diff(tc.expectedResource, me.data[0].Resource.Labels); diff != "" {
|
|
t.Errorf("Wrong resource for %s (-want +got):\n%s", tc.name, diff)
|
|
}
|
|
}
|
|
|
|
if tc.expectedLabels != nil {
|
|
labels := make(map[string]string, len(me.data[0].Descriptor.LabelKeys))
|
|
for i, k := range me.data[0].Descriptor.LabelKeys {
|
|
if me.data[0].TimeSeries[0].LabelValues[i].Present {
|
|
labels[k.Key] = me.data[0].TimeSeries[0].LabelValues[i].Value
|
|
}
|
|
}
|
|
if diff := cmp.Diff(tc.expectedLabels, labels); diff != "" {
|
|
t.Errorf("Wrong labels for %s (-want + got):\n%s\n\n%+v", tc.name, diff, me.data[0].Resource.Labels)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetMetricPrefixFunc_UseKnativeDomain(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
domain string
|
|
component string
|
|
metricName string
|
|
}{{
|
|
name: "both resource and metric labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
}, {
|
|
name: "only resource labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
}, {
|
|
name: "resource labels overwrite metric labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
}, {
|
|
name: "only metric labels",
|
|
domain: internalServingDomain,
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
}}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
knativePrefix := path.Join(testCase.domain, testCase.component)
|
|
customPrefix := path.Join(defaultCustomMetricSubDomain, testCase.component)
|
|
mpf := getMetricPrefixFunc(knativePrefix, customPrefix)
|
|
|
|
if got, want := mpf(testCase.metricName), knativePrefix; got != want {
|
|
t.Fatalf("getMetricPrefixFunc=%v, want %v", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetMetricPrefixFunc_UseCustomDomain(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
domain string
|
|
component string
|
|
metricName string
|
|
}{{
|
|
name: "unsupported domain",
|
|
domain: "unsupported",
|
|
component: "activator",
|
|
metricName: "request_count",
|
|
}, {
|
|
name: "unsupported component",
|
|
domain: servingDomain,
|
|
component: "unsupported",
|
|
metricName: "request_count",
|
|
}, {
|
|
name: "unsupported metric",
|
|
domain: servingDomain,
|
|
component: "activator",
|
|
metricName: "unsupported",
|
|
}, {
|
|
name: "unsupported component",
|
|
domain: internalEventingDomain,
|
|
component: "unsupported",
|
|
metricName: "event_count",
|
|
}, {
|
|
name: "unsupported metric",
|
|
domain: internalEventingDomain,
|
|
component: "broker",
|
|
metricName: "unsupported",
|
|
}}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
knativePrefix := path.Join(testCase.domain, testCase.component)
|
|
customPrefix := path.Join(defaultCustomMetricSubDomain, testCase.component)
|
|
mpf := getMetricPrefixFunc(knativePrefix, customPrefix)
|
|
|
|
if got, want := mpf(testCase.metricName), customPrefix; got != want {
|
|
t.Fatalf("getMetricPrefixFunc=%v, want %v", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewStackdriverExporterWithMetadata(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *metricsConfig
|
|
expectSuccess bool
|
|
}{{
|
|
name: "standardCase",
|
|
config: &metricsConfig{
|
|
domain: servingDomain,
|
|
component: "autoscaler",
|
|
backendDestination: stackdriver,
|
|
stackdriverClientConfig: StackdriverClientConfig{
|
|
ProjectID: testProj,
|
|
},
|
|
},
|
|
expectSuccess: true,
|
|
}, {
|
|
name: "stackdriverClientConfigOnly",
|
|
config: &metricsConfig{
|
|
stackdriverClientConfig: StackdriverClientConfig{
|
|
ProjectID: "project",
|
|
GCPLocation: "us-west1",
|
|
ClusterName: "cluster",
|
|
UseSecret: true,
|
|
},
|
|
},
|
|
expectSuccess: true,
|
|
}, {
|
|
name: "fullValidConfig",
|
|
config: &metricsConfig{
|
|
domain: servingDomain,
|
|
component: testComponent,
|
|
backendDestination: stackdriver,
|
|
reportingPeriod: 60 * time.Second,
|
|
isStackdriverBackend: true,
|
|
stackdriverMetricTypePrefix: path.Join(servingDomain, testComponent),
|
|
stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
|
|
stackdriverClientConfig: StackdriverClientConfig{
|
|
ProjectID: "project",
|
|
GCPLocation: "us-west1",
|
|
ClusterName: "cluster",
|
|
UseSecret: true,
|
|
},
|
|
},
|
|
expectSuccess: true,
|
|
}, {
|
|
name: "invalidStackdriverGcpLocation",
|
|
config: &metricsConfig{
|
|
domain: servingDomain,
|
|
component: testComponent,
|
|
backendDestination: stackdriver,
|
|
reportingPeriod: 60 * time.Second,
|
|
isStackdriverBackend: true,
|
|
stackdriverMetricTypePrefix: path.Join(servingDomain, testComponent),
|
|
stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
|
|
stackdriverClientConfig: StackdriverClientConfig{
|
|
ProjectID: "project",
|
|
GCPLocation: "narnia",
|
|
ClusterName: "cluster",
|
|
UseSecret: true,
|
|
},
|
|
},
|
|
expectSuccess: true,
|
|
}, {
|
|
name: "missingProjectID",
|
|
config: &metricsConfig{
|
|
domain: servingDomain,
|
|
component: testComponent,
|
|
backendDestination: stackdriver,
|
|
reportingPeriod: 60 * time.Second,
|
|
isStackdriverBackend: true,
|
|
stackdriverMetricTypePrefix: path.Join(servingDomain, testComponent),
|
|
stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
|
|
stackdriverClientConfig: StackdriverClientConfig{
|
|
GCPLocation: "narnia",
|
|
ClusterName: "cluster",
|
|
UseSecret: true,
|
|
},
|
|
},
|
|
expectSuccess: true,
|
|
}, {
|
|
name: "partialStackdriverConfig",
|
|
config: &metricsConfig{
|
|
domain: servingDomain,
|
|
component: testComponent,
|
|
backendDestination: stackdriver,
|
|
reportingPeriod: 60 * time.Second,
|
|
isStackdriverBackend: true,
|
|
stackdriverMetricTypePrefix: path.Join(servingDomain, testComponent),
|
|
stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
|
|
stackdriverClientConfig: StackdriverClientConfig{
|
|
ProjectID: "project",
|
|
},
|
|
},
|
|
expectSuccess: true,
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
e, _, err := newStackdriverExporter(test.config, TestLogger(t))
|
|
|
|
succeeded := e != nil && err == nil
|
|
if test.expectSuccess != succeeded {
|
|
t.Errorf("Unexpected test result. Expected success? [%v]. Error: [%v]", test.expectSuccess, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureKubeClient(t *testing.T) {
|
|
// Even though ensureKubeclient uses sync.Once, make sure if the first run failed, it returns an error on subsequent calls.
|
|
for i := 0; i < 3; i++ {
|
|
err := ensureKubeclient()
|
|
if err == nil {
|
|
t.Error("Expected ensureKubeclient to fail due to not being in a Kubernetes cluster. Did the function run?")
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertStringsEqual(t *testing.T, description string, expected string, actual string) {
|
|
if expected != actual {
|
|
t.Errorf("Expected %v to be set correctly. Want [%v], Got [%v]", description, expected, actual)
|
|
}
|
|
}
|
|
|
|
func TestSetStackdriverSecretLocation(t *testing.T) {
|
|
// Prevent pollution from other tests
|
|
useStackdriverSecretEnabled = false
|
|
// Reset global state after test
|
|
defer func() {
|
|
secretName = StackdriverSecretNameDefault
|
|
secretNamespace = StackdriverSecretNamespaceDefault
|
|
useStackdriverSecretEnabled = false
|
|
}()
|
|
|
|
const testName, testNamespace = "test-name", "test-namespace"
|
|
secretFetcher := func(name string) (*corev1.Secret, error) {
|
|
return &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: testName,
|
|
Namespace: testNamespace,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Sanity checks
|
|
assertStringsEqual(t, "DefaultSecretName", secretName, StackdriverSecretNameDefault)
|
|
assertStringsEqual(t, "DefaultSecretNamespace", secretNamespace, StackdriverSecretNamespaceDefault)
|
|
sec, err := getStackdriverSecret(ctx, secretFetcher)
|
|
if err != nil {
|
|
t.Error("Got unexpected error when getting secret:", err)
|
|
}
|
|
if sec != nil {
|
|
t.Errorf("Stackdriver secret should not be fetched unless SetStackdriverSecretLocation has been called")
|
|
}
|
|
|
|
// Once SetStackdriverSecretLocation has been called, attempts to get the secret should complete.
|
|
SetStackdriverSecretLocation(testName, testNamespace)
|
|
sec, err = getStackdriverSecret(ctx, secretFetcher)
|
|
if err != nil {
|
|
t.Error("Got unexpected error when getting secret:", err)
|
|
}
|
|
if sec == nil {
|
|
t.Error("expected secret to be non-nil if there is no error and SetStackdriverSecretLocation has been called")
|
|
}
|
|
assertStringsEqual(t, "secretName", secretName, testName)
|
|
assertStringsEqual(t, "secretNamespace", secretNamespace, testNamespace)
|
|
}
|