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  }