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 }