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  }