knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/observability/metrics/metricstest/assert.go (about)

     1  /*
     2  Copyright 2025 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package metricstest
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  
    23  	"go.opentelemetry.io/otel/attribute"
    24  	"go.opentelemetry.io/otel/sdk/metric/metricdata"
    25  	"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
    26  
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  )
    29  
    30  type metricReader interface {
    31  	Collect(ctx context.Context, rm *metricdata.ResourceMetrics) error
    32  }
    33  
    34  type testingT interface {
    35  	Error(args ...any)
    36  	Errorf(format string, args ...any)
    37  	Fatalf(format string, args ...any)
    38  	Fatal(args ...any)
    39  	Helper()
    40  	Failed() bool
    41  }
    42  
    43  type AssertFunc func(testingT, *metricdata.ResourceMetrics)
    44  
    45  func AssertMetrics(t testingT, r metricReader, assertFns ...AssertFunc) {
    46  	t.Helper()
    47  
    48  	var rm metricdata.ResourceMetrics
    49  	r.Collect(context.Background(), &rm)
    50  
    51  	for _, assertFn := range assertFns {
    52  		assertFn(t, &rm)
    53  	}
    54  }
    55  
    56  func HasAttributes(
    57  	scopePrefix string,
    58  	metricPrefix string,
    59  	want ...attribute.KeyValue,
    60  ) AssertFunc {
    61  	return func(t testingT, rm *metricdata.ResourceMetrics) {
    62  		t.Helper()
    63  
    64  		assertCalled := false
    65  
    66  		if len(want) == 0 {
    67  			return
    68  		}
    69  
    70  		for _, sm := range rm.ScopeMetrics {
    71  			if !strings.HasPrefix(sm.Scope.Name, scopePrefix) {
    72  				continue
    73  			}
    74  
    75  			for _, metric := range sm.Metrics {
    76  				if !strings.HasPrefix(metric.Name, metricPrefix) {
    77  					continue
    78  				}
    79  
    80  				assertCalled = true
    81  
    82  				mt := t.(metricdatatest.TestingT)
    83  				switch data := metric.Data.(type) {
    84  				case metricdata.Sum[int64]:
    85  					metricdatatest.AssertHasAttributes(mt, data, want...)
    86  				case metricdata.Sum[float64]:
    87  					metricdatatest.AssertHasAttributes(mt, data, want...)
    88  				case metricdata.Histogram[int64]:
    89  					metricdatatest.AssertHasAttributes(mt, data, want...)
    90  				case metricdata.Histogram[float64]:
    91  					metricdatatest.AssertHasAttributes(mt, data, want...)
    92  				case metricdata.ExponentialHistogram[int64]:
    93  					metricdatatest.AssertHasAttributes(mt, data, want...)
    94  				case metricdata.ExponentialHistogram[float64]:
    95  					metricdatatest.AssertHasAttributes(mt, data, want...)
    96  				case metricdata.Gauge[int64]:
    97  					metricdatatest.AssertHasAttributes(mt, data, want...)
    98  				case metricdata.Gauge[float64]:
    99  					metricdatatest.AssertHasAttributes(mt, data, want...)
   100  				default:
   101  					t.Fatalf("unsupported metric data type for metric %q: %T", metric.Name, data)
   102  				}
   103  			}
   104  		}
   105  		if !assertCalled {
   106  			t.Error("expected attributes but scope and metric prefix didn't match any results")
   107  		}
   108  	}
   109  }
   110  
   111  func MetricsPresent(scopeName string, names ...string) AssertFunc {
   112  	return func(t testingT, rm *metricdata.ResourceMetrics) {
   113  		t.Helper()
   114  
   115  		want := sets.New(names...)
   116  		got := sets.New[string]()
   117  
   118  		for _, sm := range rm.ScopeMetrics {
   119  			if sm.Scope.Name != scopeName {
   120  				continue
   121  			}
   122  			for _, metric := range sm.Metrics {
   123  				if !want.Has(metric.Name) {
   124  					t.Fatal("unexpected metric", metric.Name)
   125  				}
   126  
   127  				got.Insert(metric.Name)
   128  			}
   129  		}
   130  
   131  		diff := want.Difference(got)
   132  		if len(diff) > 0 {
   133  			t.Fatal("expected metrics didn't appear", diff.UnsortedList())
   134  		}
   135  	}
   136  }
   137  
   138  func MetricsEqual(scopeName string, expected ...metricdata.Metrics) AssertFunc {
   139  	return func(t testingT, rm *metricdata.ResourceMetrics) {
   140  		t.Helper()
   141  		opts := metricdatatest.IgnoreTimestamp()
   142  
   143  		expectedSet := make(map[string]metricdata.Metrics)
   144  		for _, exp := range expected {
   145  			expectedSet[exp.Name] = exp
   146  		}
   147  
   148  		for _, sm := range rm.ScopeMetrics {
   149  			if sm.Scope.Name != scopeName {
   150  				continue
   151  			}
   152  
   153  			if len(sm.Metrics) != len(expected) {
   154  				t.Errorf("expected %d metrics in scope %q, got %d", len(expected), scopeName, len(sm.Metrics))
   155  				return
   156  			}
   157  
   158  			for _, actual := range sm.Metrics {
   159  				expected, exists := expectedSet[actual.Name]
   160  				if !exists {
   161  					t.Errorf("unexpected metric %q in scope %q", actual.Name, scopeName)
   162  					continue
   163  				}
   164  
   165  				// Compare metric properties
   166  				if actual.Name != expected.Name {
   167  					t.Errorf("metric name mismatch: expected %q, got %q", expected.Name, actual.Name)
   168  				}
   169  
   170  				if actual.Description != expected.Description {
   171  					t.Errorf("metric description mismatch for %q: expected %q, got %q", actual.Name, expected.Description, actual.Description)
   172  				}
   173  
   174  				if actual.Unit != expected.Unit {
   175  					t.Errorf("metric unit mismatch for %q: expected %q, got %q", actual.Name, expected.Unit, actual.Unit)
   176  				}
   177  
   178  				// Use metricdatatest to compare the actual metric data
   179  				mt := t.(metricdatatest.TestingT)
   180  				switch actualData := actual.Data.(type) {
   181  				case metricdata.Sum[int64]:
   182  					if expectedData, ok := expected.Data.(metricdata.Sum[int64]); ok {
   183  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   184  					} else {
   185  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   186  					}
   187  				case metricdata.Sum[float64]:
   188  					if expectedData, ok := expected.Data.(metricdata.Sum[float64]); ok {
   189  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   190  					} else {
   191  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   192  					}
   193  				case metricdata.Histogram[int64]:
   194  					if expectedData, ok := expected.Data.(metricdata.Histogram[int64]); ok {
   195  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   196  					} else {
   197  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   198  					}
   199  				case metricdata.Histogram[float64]:
   200  					if expectedData, ok := expected.Data.(metricdata.Histogram[float64]); ok {
   201  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   202  					} else {
   203  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   204  					}
   205  				case metricdata.ExponentialHistogram[int64]:
   206  					if expectedData, ok := expected.Data.(metricdata.ExponentialHistogram[int64]); ok {
   207  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   208  					} else {
   209  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   210  					}
   211  				case metricdata.ExponentialHistogram[float64]:
   212  					if expectedData, ok := expected.Data.(metricdata.ExponentialHistogram[float64]); ok {
   213  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   214  					} else {
   215  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   216  					}
   217  				case metricdata.Gauge[int64]:
   218  					if expectedData, ok := expected.Data.(metricdata.Gauge[int64]); ok {
   219  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   220  					} else {
   221  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   222  					}
   223  				case metricdata.Gauge[float64]:
   224  					if expectedData, ok := expected.Data.(metricdata.Gauge[float64]); ok {
   225  						metricdatatest.AssertEqual(mt, expectedData, actualData, opts)
   226  					} else {
   227  						t.Errorf("metric data type mismatch for %q: expected %T, got %T", actual.Name, expected.Data, actualData)
   228  					}
   229  				default:
   230  					t.Errorf("unsupported metric data type for metric %q: %T", actual.Name, actualData)
   231  				}
   232  			}
   233  			return
   234  		}
   235  
   236  		// If we get here, the scope wasn't found
   237  		t.Errorf("scope %q not found in metrics", scopeName)
   238  	}
   239  }