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