google.golang.org/grpc@v1.74.2/internal/stats/metrics_recorder_list_test.go (about) 1 /* 2 * 3 * Copyright 2024 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 // Package stats_test implements an e2e test for the Metrics Recorder List 20 // component of the Client Conn. 21 package stats_test 22 23 import ( 24 "context" 25 "fmt" 26 "strings" 27 "testing" 28 "time" 29 30 "google.golang.org/grpc" 31 "google.golang.org/grpc/balancer" 32 "google.golang.org/grpc/balancer/pickfirst" 33 "google.golang.org/grpc/credentials/insecure" 34 estats "google.golang.org/grpc/experimental/stats" 35 "google.golang.org/grpc/internal" 36 "google.golang.org/grpc/internal/grpctest" 37 istats "google.golang.org/grpc/internal/stats" 38 "google.golang.org/grpc/internal/testutils/stats" 39 testgrpc "google.golang.org/grpc/interop/grpc_testing" 40 testpb "google.golang.org/grpc/interop/grpc_testing" 41 "google.golang.org/grpc/resolver" 42 "google.golang.org/grpc/resolver/manual" 43 "google.golang.org/grpc/serviceconfig" 44 ) 45 46 var defaultTestTimeout = 5 * time.Second 47 48 type s struct { 49 grpctest.Tester 50 } 51 52 func Test(t *testing.T) { 53 grpctest.RunSubTests(t, s{}) 54 } 55 56 var ( 57 intCountHandle = estats.RegisterInt64Count(estats.MetricDescriptor{ 58 Name: "simple counter", 59 Description: "sum of all emissions from tests", 60 Unit: "int", 61 Labels: []string{"int counter label"}, 62 OptionalLabels: []string{"int counter optional label"}, 63 Default: false, 64 }) 65 floatCountHandle = estats.RegisterFloat64Count(estats.MetricDescriptor{ 66 Name: "float counter", 67 Description: "sum of all emissions from tests", 68 Unit: "float", 69 Labels: []string{"float counter label"}, 70 OptionalLabels: []string{"float counter optional label"}, 71 Default: false, 72 }) 73 intHistoHandle = estats.RegisterInt64Histo(estats.MetricDescriptor{ 74 Name: "int histo", 75 Description: "sum of all emissions from tests", 76 Unit: "int", 77 Labels: []string{"int histo label"}, 78 OptionalLabels: []string{"int histo optional label"}, 79 Default: false, 80 }) 81 floatHistoHandle = estats.RegisterFloat64Histo(estats.MetricDescriptor{ 82 Name: "float histo", 83 Description: "sum of all emissions from tests", 84 Unit: "float", 85 Labels: []string{"float histo label"}, 86 OptionalLabels: []string{"float histo optional label"}, 87 Default: false, 88 }) 89 intGaugeHandle = estats.RegisterInt64Gauge(estats.MetricDescriptor{ 90 Name: "simple gauge", 91 Description: "the most recent int emitted by test", 92 Unit: "int", 93 Labels: []string{"int gauge label"}, 94 OptionalLabels: []string{"int gauge optional label"}, 95 Default: false, 96 }) 97 ) 98 99 func init() { 100 balancer.Register(recordingLoadBalancerBuilder{}) 101 } 102 103 const recordingLoadBalancerName = "recording_load_balancer" 104 105 type recordingLoadBalancerBuilder struct{} 106 107 func (recordingLoadBalancerBuilder) Name() string { 108 return recordingLoadBalancerName 109 } 110 111 func (recordingLoadBalancerBuilder) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer { 112 intCountHandle.Record(cc.MetricsRecorder(), 1, "int counter label val", "int counter optional label val") 113 floatCountHandle.Record(cc.MetricsRecorder(), 2, "float counter label val", "float counter optional label val") 114 intHistoHandle.Record(cc.MetricsRecorder(), 3, "int histo label val", "int histo optional label val") 115 floatHistoHandle.Record(cc.MetricsRecorder(), 4, "float histo label val", "float histo optional label val") 116 intGaugeHandle.Record(cc.MetricsRecorder(), 5, "int gauge label val", "int gauge optional label val") 117 118 return &recordingLoadBalancer{ 119 Balancer: balancer.Get(pickfirst.Name).Build(cc, bOpts), 120 } 121 } 122 123 type recordingLoadBalancer struct { 124 balancer.Balancer 125 } 126 127 // TestMetricsRecorderList tests the metrics recorder list functionality of the 128 // ClientConn. It configures a global and local stats handler Dial Option. These 129 // stats handlers implement the MetricsRecorder interface. It also configures a 130 // balancer which registers metrics and records on metrics at build time. This 131 // test then asserts that the recorded metrics show up on both configured stats 132 // handlers. 133 func (s) TestMetricsRecorderList(t *testing.T) { 134 cleanup := internal.SnapshotMetricRegistryForTesting() 135 defer cleanup() 136 137 mr := manual.NewBuilderWithScheme("test-metrics-recorder-list") 138 defer mr.Close() 139 140 json := `{"loadBalancingConfig": [{"recording_load_balancer":{}}]}` 141 sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json) 142 mr.InitialState(resolver.State{ 143 ServiceConfig: sc, 144 }) 145 146 // Create two stats.Handlers which also implement MetricsRecorder, configure 147 // one as a global dial option and one as a local dial option. 148 mr1 := stats.NewTestMetricsRecorder() 149 mr2 := stats.NewTestMetricsRecorder() 150 151 defer internal.ClearGlobalDialOptions() 152 internal.AddGlobalDialOptions.(func(opt ...grpc.DialOption))(grpc.WithStatsHandler(mr1)) 153 154 cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithStatsHandler(mr2)) 155 if err != nil { 156 t.Fatalf("grpc.NewClient() failed: %v", err) 157 } 158 defer cc.Close() 159 160 tsc := testgrpc.NewTestServiceClient(cc) 161 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 162 defer cancel() 163 164 // Trigger the recording_load_balancer to build, which will trigger metrics 165 // to record. 166 tsc.UnaryCall(ctx, &testpb.SimpleRequest{}) 167 168 mdWant := stats.MetricsData{ 169 Handle: intCountHandle.Descriptor(), 170 IntIncr: 1, 171 LabelKeys: []string{"int counter label", "int counter optional label"}, 172 LabelVals: []string{"int counter label val", "int counter optional label val"}, 173 } 174 if err := mr1.WaitForInt64Count(ctx, mdWant); err != nil { 175 t.Fatal(err.Error()) 176 } 177 if err := mr2.WaitForInt64Count(ctx, mdWant); err != nil { 178 t.Fatal(err.Error()) 179 } 180 181 mdWant = stats.MetricsData{ 182 Handle: floatCountHandle.Descriptor(), 183 FloatIncr: 2, 184 LabelKeys: []string{"float counter label", "float counter optional label"}, 185 LabelVals: []string{"float counter label val", "float counter optional label val"}, 186 } 187 if err := mr1.WaitForFloat64Count(ctx, mdWant); err != nil { 188 t.Fatal(err.Error()) 189 } 190 if err := mr2.WaitForFloat64Count(ctx, mdWant); err != nil { 191 t.Fatal(err.Error()) 192 } 193 194 mdWant = stats.MetricsData{ 195 Handle: intHistoHandle.Descriptor(), 196 IntIncr: 3, 197 LabelKeys: []string{"int histo label", "int histo optional label"}, 198 LabelVals: []string{"int histo label val", "int histo optional label val"}, 199 } 200 if err := mr1.WaitForInt64Histo(ctx, mdWant); err != nil { 201 t.Fatal(err.Error()) 202 } 203 if err := mr2.WaitForInt64Histo(ctx, mdWant); err != nil { 204 t.Fatal(err.Error()) 205 } 206 207 mdWant = stats.MetricsData{ 208 Handle: floatHistoHandle.Descriptor(), 209 FloatIncr: 4, 210 LabelKeys: []string{"float histo label", "float histo optional label"}, 211 LabelVals: []string{"float histo label val", "float histo optional label val"}, 212 } 213 if err := mr1.WaitForFloat64Histo(ctx, mdWant); err != nil { 214 t.Fatal(err.Error()) 215 } 216 if err := mr2.WaitForFloat64Histo(ctx, mdWant); err != nil { 217 t.Fatal(err.Error()) 218 } 219 mdWant = stats.MetricsData{ 220 Handle: intGaugeHandle.Descriptor(), 221 IntIncr: 5, 222 LabelKeys: []string{"int gauge label", "int gauge optional label"}, 223 LabelVals: []string{"int gauge label val", "int gauge optional label val"}, 224 } 225 if err := mr1.WaitForInt64Gauge(ctx, mdWant); err != nil { 226 t.Fatal(err.Error()) 227 } 228 if err := mr2.WaitForInt64Gauge(ctx, mdWant); err != nil { 229 t.Fatal(err.Error()) 230 } 231 } 232 233 // TestMetricRecorderListPanic tests that the metrics recorder list panics if 234 // received the wrong number of labels for a particular metric. 235 func (s) TestMetricRecorderListPanic(t *testing.T) { 236 cleanup := internal.SnapshotMetricRegistryForTesting() 237 defer cleanup() 238 239 intCountHandle := estats.RegisterInt64Count(estats.MetricDescriptor{ 240 Name: "simple counter", 241 Description: "sum of all emissions from tests", 242 Unit: "int", 243 Labels: []string{"int counter label"}, 244 OptionalLabels: []string{"int counter optional label"}, 245 Default: false, 246 }) 247 mrl := istats.NewMetricsRecorderList(nil) 248 249 want := `Received 1 labels in call to record metric "simple counter", but expected 2.` 250 defer func() { 251 if r := recover(); !strings.Contains(fmt.Sprint(r), want) { 252 t.Errorf("expected panic contains %q, got %q", want, r) 253 } 254 }() 255 256 intCountHandle.Record(mrl, 1, "only one label") 257 }