pkg/metrics/stackdriver_exporter_test.go

519 lines
19 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 (
"path"
"testing"
"time"
"contrib.go.opencensus.io/exporter/stackdriver"
"go.opencensus.io/metric/metricdata"
"go.opencensus.io/stats/view"
. "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 (
revisionTestTags = map[string]string{
metricskey.LabelNamespaceName: testNS,
metricskey.LabelServiceName: testService,
metricskey.LabelRouteName: testRoute, // Not a label key for knative_revision resource
metricskey.LabelRevisionName: testRevision,
}
brokerTestTags = map[string]string{
metricskey.LabelNamespaceName: testNS,
metricskey.LabelBrokerName: testBroker,
metricskey.LabelEventType: testEventType, // Not a label key for knative_broker resource
}
triggerTestTags = map[string]string{
metricskey.LabelNamespaceName: testNS,
metricskey.LabelTriggerName: testTrigger,
metricskey.LabelBrokerName: testBroker,
metricskey.LabelFilterType: testFilterType, // Not a label key for knative_trigger resource
}
sourceTestTags = map[string]string{
metricskey.LabelNamespaceName: testNS,
metricskey.LabelName: testSource,
metricskey.LabelResourceGroup: testSourceResourceGroup,
metricskey.LabelEventType: testEventType, // Not a label key for knative_source resource
metricskey.LabelEventSource: testEventSource, // Not a label key for knative_source resource
}
testGcpMetadata = gcpMetadata{
project: "test-project",
location: "test-location",
cluster: "test-cluster",
}
supportedServingMetricsTestCases = []struct {
name string
domain string
component string
metricName string
}{{
name: "activator metric",
domain: internalServingDomain,
component: "activator",
metricName: "request_count",
}, {
name: "autoscaler metric",
domain: servingDomain,
component: "autoscaler",
metricName: "desired_pods",
}}
supportedEventingBrokerMetricsTestCases = []struct {
name string
domain string
component string
metricName string
}{{
name: "broker metric",
domain: internalEventingDomain,
component: "broker",
metricName: "event_count",
}}
supportedEventingTriggerMetricsTestCases = []struct {
name string
domain string
component string
metricName string
}{{
name: "trigger metric",
domain: internalEventingDomain,
component: "trigger",
metricName: "event_count",
}, {
name: "trigger metric",
domain: internalEventingDomain,
component: "trigger",
metricName: "event_processing_latencies",
}, {
name: "trigger metric",
domain: internalEventingDomain,
component: "trigger",
metricName: "event_dispatch_latencies",
}}
supportedEventingSourceMetricsTestCases = []struct {
name string
domain string
component string
metricName string
}{{
name: "source metric",
domain: eventingDomain,
component: "source",
metricName: "event_count",
}}
unsupportedMetricsTestCases = []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",
}}
)
func fakeGcpMetadataFunc() *gcpMetadata {
return &testGcpMetadata
}
type fakeExporter struct{}
func (fe *fakeExporter) ExportView(vd *view.Data) {}
func (fe *fakeExporter) Flush() {}
func newFakeExporter(o stackdriver.Options) (view.Exporter, error) {
return &fakeExporter{}, nil
}
func TestGetResourceByDescriptorFunc_UseKnativeRevision(t *testing.T) {
for _, testCase := range supportedServingMetricsTestCases {
testDescriptor := &metricdata.Descriptor{
Name: testCase.metricName,
Description: "Test View",
Type: metricdata.TypeGaugeInt64,
Unit: metricdata.UnitDimensionless,
}
rbd := getResourceByDescriptorFunc(path.Join(testCase.domain, testCase.component), &testGcpMetadata)
metricLabels, monitoredResource := rbd(testDescriptor, revisionTestTags)
gotResType, resourceLabels := monitoredResource.MonitoredResource()
wantedResType := "knative_revision"
if gotResType != wantedResType {
t.Fatalf("MonitoredResource=%v, want %v", gotResType, wantedResType)
}
// revisionTestTags includes route_name, which is not a key for knative_revision resource.
if got := metricLabels[metricskey.LabelRouteName]; got != testRoute {
t.Errorf("expected metrics label: %v, got: %v", testRoute, got)
}
if got := resourceLabels[metricskey.LabelNamespaceName]; got != testNS {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelNamespaceName, testNS, got)
}
// configuration_name is a key required by knative_revision but missed in revisionTestTags
if got := resourceLabels[metricskey.LabelConfigurationName]; got != metricskey.ValueUnknown {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelConfigurationName, metricskey.ValueUnknown, got)
}
}
}
func TestGetResourceByDescriptorFunc_UseKnativeBroker(t *testing.T) {
for _, testCase := range supportedEventingBrokerMetricsTestCases {
testDescriptor := &metricdata.Descriptor{
Name: testCase.metricName,
Description: "Test View",
Type: metricdata.TypeGaugeInt64,
Unit: metricdata.UnitDimensionless,
}
rbd := getResourceByDescriptorFunc(path.Join(testCase.domain, testCase.component), &testGcpMetadata)
metricLabels, monitoredResource := rbd(testDescriptor, brokerTestTags)
gotResType, resourceLabels := monitoredResource.MonitoredResource()
wantedResType := "knative_broker"
if gotResType != wantedResType {
t.Fatalf("MonitoredResource=%v, want %v", gotResType, wantedResType)
}
// brokerTestTags includes event_type, which is not a key for knative_broker resource.
if got := metricLabels[metricskey.LabelEventType]; got != testEventType {
t.Errorf("expected metrics label: %v, got: %v", testEventType, got)
}
if got := resourceLabels[metricskey.LabelNamespaceName]; got != testNS {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelNamespaceName, testNS, got)
}
if got := resourceLabels[metricskey.LabelBrokerName]; got != testBroker {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelBrokerName, testBroker, got)
}
}
}
func TestGetResourceByDescriptorFunc_UseKnativeTrigger(t *testing.T) {
for _, testCase := range supportedEventingTriggerMetricsTestCases {
testDescriptor := &metricdata.Descriptor{
Name: testCase.metricName,
Description: "Test View",
Type: metricdata.TypeGaugeInt64,
Unit: metricdata.UnitDimensionless,
}
rbd := getResourceByDescriptorFunc(path.Join(testCase.domain, testCase.component), &testGcpMetadata)
metricLabels, monitoredResource := rbd(testDescriptor, triggerTestTags)
gotResType, resourceLabels := monitoredResource.MonitoredResource()
wantedResType := "knative_trigger"
if gotResType != wantedResType {
t.Fatalf("MonitoredResource=%v, want %v", gotResType, wantedResType)
}
// triggerTestTags includes filter_type, which is not a key for knative_trigger resource.
if got := metricLabels[metricskey.LabelFilterType]; got != testFilterType {
t.Errorf("expected metrics label: %v, got: %v", testFilterType, got)
}
if got := resourceLabels[metricskey.LabelNamespaceName]; got != testNS {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelNamespaceName, testNS, got)
}
if got := resourceLabels[metricskey.LabelBrokerName]; got != testBroker {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelBrokerName, testBroker, got)
}
}
}
func TestGetResourceByDescriptorFunc_UseKnativeSource(t *testing.T) {
for _, testCase := range supportedEventingSourceMetricsTestCases {
testDescriptor := &metricdata.Descriptor{
Name: testCase.metricName,
Description: "Test View",
Type: metricdata.TypeGaugeInt64,
Unit: metricdata.UnitDimensionless,
}
rbd := getResourceByDescriptorFunc(path.Join(testCase.domain, testCase.component), &testGcpMetadata)
metricLabels, monitoredResource := rbd(testDescriptor, sourceTestTags)
gotResType, resourceLabels := monitoredResource.MonitoredResource()
wantedResType := "knative_source"
if gotResType != wantedResType {
t.Fatalf("MonitoredResource=%v, want %v", gotResType, wantedResType)
}
// sourceTestTags includes event_type, which is not a key for knative_trigger resource.
if got := metricLabels[metricskey.LabelEventType]; got != testEventType {
t.Errorf("expected metrics label: %v, got: %v", testEventType, got)
}
// sourceTestTags includes event_source, which is not a key for knative_trigger resource.
if got := metricLabels[metricskey.LabelEventSource]; got != testEventSource {
t.Errorf("expected metrics label: %v, got: %v", testEventSource, got)
}
if got := resourceLabels[metricskey.LabelNamespaceName]; got != testNS {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelNamespaceName, testNS, got)
}
if got := resourceLabels[metricskey.LabelName]; got != testSource {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelName, testSource, got)
}
if got := resourceLabels[metricskey.LabelResourceGroup]; got != testSourceResourceGroup {
t.Errorf("expected resource label %v with value %v, got: %v", metricskey.LabelResourceGroup, testSourceResourceGroup, got)
}
}
}
func TestGetResourceByDescriptorFunc_UseGlobal(t *testing.T) {
for _, testCase := range unsupportedMetricsTestCases {
testDescriptor := &metricdata.Descriptor{
Name: testCase.metricName,
Description: "Test View",
Type: metricdata.TypeGaugeInt64,
Unit: metricdata.UnitDimensionless,
}
mrf := getResourceByDescriptorFunc(path.Join(testCase.domain, testCase.component), &testGcpMetadata)
metricLabels, monitoredResource := mrf(testDescriptor, revisionTestTags)
gotResType, resourceLabels := monitoredResource.MonitoredResource()
wantedResType := "global"
if gotResType != wantedResType {
t.Fatalf("MonitoredResource=%v, want: %v", gotResType, wantedResType)
}
if got := metricLabels[metricskey.LabelNamespaceName]; got != testNS {
t.Errorf("expected new tag %v with value %v, got: %v", metricskey.LabelNamespaceName, testNS, got)
}
if len(resourceLabels) != 0 {
t.Errorf("expected no label, got: %v", resourceLabels)
}
}
}
func TestGetMetricPrefixFunc_UseKnativeDomain(t *testing.T) {
for _, testCase := range supportedServingMetricsTestCases {
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) {
for _, testCase := range unsupportedMetricsTestCases {
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) {
// Reset global state after test
defer func() {
secretName = StackdriverSecretNameDefault
secretNamespace = StackdriverSecretNamespaceDefault
}()
sdConfig := &StackdriverClientConfig{
ProjectID: "project",
GCPLocation: "us-west2",
ClusterName: "cluster",
UseSecret: false,
}
// Sanity checks
assertStringsEqual(t, "DefaultSecretName", secretName, StackdriverSecretNameDefault)
assertStringsEqual(t, "DefaultSecretNamespace", secretNamespace, StackdriverSecretNamespaceDefault)
if _, err := getStackdriverExporterClientOptions(sdConfig); err != nil {
t.Errorf("Got unexpected error when creating exporter client options: [%v]", err)
}
// Check configuration's UseSecret value is ignored until the consuming package calls SetStackdriverSecretLocation
// If an attempt to read a Secret was made, there should be an error because there's no valid in-cluster kubeclient.
sdConfig.UseSecret = true
if _, e1 := getStackdriverExporterClientOptions(sdConfig); e1 != nil {
t.Errorf("Got unexpected error when creating exporter client options: [%v]", e1)
}
testName, testNamespace := "test-name", "test-namespace"
// SetStackdriverSecretLocation has been called & config's UseSecret value is set
// There should be an attempt to read the Secret, and an error because there's no valid in-cluster kubeclient.
SetStackdriverSecretLocation("test-name", "test-namespace")
if _, e1 := getStackdriverExporterClientOptions(sdConfig); e1 == nil {
t.Errorf("Expected a known error when getting exporter options with Secrets enabled (cannot create valid kubeclient in tests). Did the function run as expected?")
}
assertStringsEqual(t, "secretName", secretName, testName)
assertStringsEqual(t, "secretNamespace", secretNamespace, testNamespace)
randomName, randomNamespace := "random-name", "random-namespace"
SetStackdriverSecretLocation(randomName, randomNamespace)
if _, e1 := getStackdriverExporterClientOptions(sdConfig); e1 == nil {
t.Errorf("Expected a known error when getting exporter options with Secrets enabled (cannot create valid kubeclient in tests). Did the function run as expected?")
}
assertStringsEqual(t, "secretName", secretName, randomName)
assertStringsEqual(t, "secretNamespace", secretNamespace, randomNamespace)
}