package config import ( "reflect" "testing" "time" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" corev1 "k8s.io/api/core/v1" ) func boolPtr(b bool) *bool { return &b } func Test_metricsConfig_load(t *testing.T) { tests := []struct { name string configMap *corev1.ConfigMap expectedValue *metricsConfig }{ { name: "Case 1: Test defaults", configMap: &corev1.ConfigMap{ Data: map[string]string{}, }, expectedValue: &metricsConfig{ metricsRefreshInterval: 0, namespaces: namespacesConfig{IncludeNamespaces: []string{}, ExcludeNamespaces: []string{}}, bucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}, metricsExposure: map[string]metricExposureConfig{}, }, }, { name: "Case 2: All fields provided", configMap: &corev1.ConfigMap{ Data: map[string]string{ "metricsRefreshInterval": "10s", "namespaces": `{"include": ["namespace1"], "exclude": ["namespace2"]}`, "bucketBoundaries": "0.005, 0.01, 0.025, 0.05", "metricsExposure": `{"metric1": {"enabled": true, "disabledLabelDimensions": ["dim1"]}, "metric2": {"enabled": true, "disabledLabelDimensions": ["dim1","dim2"], "bucketBoundaries": [0.025, 0.05]}}`, }, }, expectedValue: &metricsConfig{ metricsRefreshInterval: 10 * time.Second, namespaces: namespacesConfig{IncludeNamespaces: []string{"namespace1"}, ExcludeNamespaces: []string{"namespace2"}}, bucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05}, metricsExposure: map[string]metricExposureConfig{ "metric1": {Enabled: boolPtr(true), DisabledLabelDimensions: []string{"dim1"}, BucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05}}, "metric2": {Enabled: boolPtr(true), DisabledLabelDimensions: []string{"dim1", "dim2"}, BucketBoundaries: []float64{0.025, 0.05}}, }, }, }, { name: "Case 3: Some of the fields provided", configMap: &corev1.ConfigMap{ Data: map[string]string{ "namespaces": `{"include": ["namespace1"], "exclude": ["namespace2"]}`, "metricsExposure": `{"metric1": {"enabled": true, "disabledLabelDimensions": ["dim1"]}, "metric2": {"enabled": true, "disabledLabelDimensions": ["dim1","dim2"], "bucketBoundaries": [0.025, 0.05]}}`, }, }, expectedValue: &metricsConfig{ metricsRefreshInterval: 0, namespaces: namespacesConfig{IncludeNamespaces: []string{"namespace1"}, ExcludeNamespaces: []string{"namespace2"}}, bucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}, metricsExposure: map[string]metricExposureConfig{ "metric1": {Enabled: boolPtr(true), DisabledLabelDimensions: []string{"dim1"}, BucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}}, "metric2": {Enabled: boolPtr(true), DisabledLabelDimensions: []string{"dim1", "dim2"}, BucketBoundaries: []float64{0.025, 0.05}}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cd := NewDefaultMetricsConfiguration() cd.load(tt.configMap) if !reflect.DeepEqual(cd.metricsRefreshInterval, tt.expectedValue.metricsRefreshInterval) { t.Errorf("Expected %+v, but got %+v", tt.expectedValue.metricsRefreshInterval, cd.metricsRefreshInterval) } if !reflect.DeepEqual(cd.namespaces, tt.expectedValue.namespaces) { t.Errorf("Expected %+v, but got %+v", tt.expectedValue.namespaces, cd.namespaces) } if !reflect.DeepEqual(cd.bucketBoundaries, tt.expectedValue.bucketBoundaries) { t.Errorf("Expected %+v, but got %+v", tt.expectedValue.bucketBoundaries, cd.bucketBoundaries) } if !reflect.DeepEqual(cd.metricsExposure, tt.expectedValue.metricsExposure) { t.Errorf("Expected %+v, but got %+v", tt.expectedValue.metricsExposure, cd.metricsRefreshInterval) } }) } } func Test_metricsConfig_BuildMeterProviderViews(t *testing.T) { tests := []struct { name string metricsExposure map[string]metricExposureConfig expectedSize int validateFunc func([]sdkmetric.View) bool }{ { name: "Case 1: defaults", metricsExposure: map[string]metricExposureConfig{}, expectedSize: 0, }, { name: "Case 2: there is no matching entry on the exposure config", metricsExposure: map[string]metricExposureConfig{ "metric1": {Enabled: boolPtr(true), DisabledLabelDimensions: []string{"dim1"}, BucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}}, }, expectedSize: 1, validateFunc: func(views []sdkmetric.View) bool { stream, _ := views[0](sdkmetric.Instrument{Name: "metric2"}) assert := stream.AttributeFilter == nil assert = assert && stream.Aggregation == nil return assert }, }, { name: "Case 3: metrics enabled, no transformation configured", metricsExposure: map[string]metricExposureConfig{ "metric1": {Enabled: boolPtr(true)}, }, expectedSize: 1, validateFunc: func(views []sdkmetric.View) bool { stream, _ := views[0](sdkmetric.Instrument{Name: "metric1"}) assert := stream.AttributeFilter == nil assert = assert && stream.Aggregation == nil return assert }, }, { name: "Case 4: metrics enabled, histogram metric", metricsExposure: map[string]metricExposureConfig{ "metric1": {Enabled: boolPtr(true), DisabledLabelDimensions: []string{"dim1"}, BucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}}, }, expectedSize: 1, validateFunc: func(views []sdkmetric.View) bool { stream, _ := views[0](sdkmetric.Instrument{Name: "metric1", Kind: sdkmetric.InstrumentKindHistogram}) assert := stream.AttributeFilter(attribute.String("policy_validation_mode", "")) assert = assert && !stream.AttributeFilter(attribute.String("dim1", "")) assert = assert && reflect.DeepEqual(stream.Aggregation, sdkmetric.AggregationExplicitBucketHistogram{ Boundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}, NoMinMax: false, }) return assert }, }, { name: "Case 5: metrics enabled, non histogram metric", metricsExposure: map[string]metricExposureConfig{ "metric1": {Enabled: boolPtr(true), DisabledLabelDimensions: []string{"dim1"}, BucketBoundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}}, }, expectedSize: 1, validateFunc: func(views []sdkmetric.View) bool { stream, _ := views[0](sdkmetric.Instrument{Name: "metric1", Kind: sdkmetric.InstrumentKindCounter}) assert := stream.AttributeFilter(attribute.String("policy_validation_mode", "")) assert = assert && !stream.AttributeFilter(attribute.String("dim1", "")) assert = assert && stream.Aggregation == nil return assert }, }, { name: "Case 6: metrics disabled", metricsExposure: map[string]metricExposureConfig{ "metric1": {Enabled: boolPtr(false)}, }, expectedSize: 1, validateFunc: func(views []sdkmetric.View) bool { stream, _ := views[0](sdkmetric.Instrument{Name: "metric1"}) return reflect.DeepEqual(stream.Aggregation, sdkmetric.AggregationDrop{}) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mcd := NewDefaultMetricsConfiguration() mcd.metricsExposure = tt.metricsExposure got := mcd.BuildMeterProviderViews() if len(got) != tt.expectedSize { t.Errorf("Expected result size to be %v, but got %v", tt.expectedSize, len(got)) } if tt.validateFunc != nil { if !tt.validateFunc(got) { t.Errorf("The validation function did not return true!") } } }) } } func Test_metricsConfig_GetBucketBoundaries(t *testing.T) { tests := []struct { name string provided []float64 want []float64 }{ { name: "Case 1: Test defaults", provided: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}, want: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 25, 30}, }, { name: "Case 2: Custom", provided: []float64{0.005, 0.01, 0.025, 0.05}, want: []float64{0.005, 0.01, 0.025, 0.05}, }, { name: "Case 3: Empty", provided: []float64{}, want: []float64{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mcd := NewDefaultMetricsConfiguration() mcd.bucketBoundaries = tt.provided if got := mcd.GetBucketBoundaries(); !reflect.DeepEqual(got, tt.want) { t.Errorf("GetBucketBoundaries() = %v, want %v", got, tt.want) } }) } }