diff --git a/balancer/balancer.go b/balancer/balancer.go index 04e47a1254ba..b181f386a1ba 100644 --- a/balancer/balancer.go +++ b/balancer/balancer.go @@ -30,6 +30,7 @@ import ( "google.golang.org/grpc/channelz" "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials" + estats "google.golang.org/grpc/experimental/stats" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/internal" "google.golang.org/grpc/metadata" @@ -256,6 +257,10 @@ type BuildOptions struct { // same resolver.Target as passed to the resolver. See the documentation for // the resolver.Target type for details about what it contains. Target resolver.Target + // MetricsRecorder is the metrics recorder that balancers can use to record + // metrics. Balancer implementations which do not register metrics on + // metrics registry and record on them can ignore this field. + MetricsRecorder estats.MetricsRecorder } // Builder creates a balancer. diff --git a/balancer_wrapper.go b/balancer_wrapper.go index 5895732225d2..5877b71533bc 100644 --- a/balancer_wrapper.go +++ b/balancer_wrapper.go @@ -82,6 +82,7 @@ func newCCBalancerWrapper(cc *ClientConn) *ccBalancerWrapper { CustomUserAgent: cc.dopts.copts.UserAgent, ChannelzParent: cc.channelz, Target: cc.parsedTarget, + MetricsRecorder: cc.metricsRecorderList, }, serializer: grpcsync.NewCallbackSerializer(ctx), serializerCancel: cancel, diff --git a/clientconn.go b/clientconn.go index 3af8ba490727..2d1ce9c362d3 100644 --- a/clientconn.go +++ b/clientconn.go @@ -40,6 +40,7 @@ import ( "google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/idle" iresolver "google.golang.org/grpc/internal/resolver" + "google.golang.org/grpc/internal/stats" "google.golang.org/grpc/internal/transport" "google.golang.org/grpc/keepalive" "google.golang.org/grpc/resolver" @@ -195,8 +196,11 @@ func NewClient(target string, opts ...DialOption) (conn *ClientConn, err error) cc.csMgr = newConnectivityStateManager(cc.ctx, cc.channelz) cc.pickerWrapper = newPickerWrapper(cc.dopts.copts.StatsHandlers) + cc.metricsRecorderList = stats.NewMetricsRecorderList(cc.dopts.copts.StatsHandlers) + cc.initIdleStateLocked() // Safe to call without the lock, since nothing else has a reference to cc. cc.idlenessMgr = idle.NewManager((*idler)(cc), cc.dopts.idleTimeout) + return cc, nil } @@ -591,13 +595,14 @@ type ClientConn struct { cancel context.CancelFunc // Cancelled on close. // The following are initialized at dial time, and are read-only after that. - target string // User's dial target. - parsedTarget resolver.Target // See initParsedTargetAndResolverBuilder(). - authority string // See initAuthority(). - dopts dialOptions // Default and user specified dial options. - channelz *channelz.Channel // Channelz object. - resolverBuilder resolver.Builder // See initParsedTargetAndResolverBuilder(). - idlenessMgr *idle.Manager + target string // User's dial target. + parsedTarget resolver.Target // See initParsedTargetAndResolverBuilder(). + authority string // See initAuthority(). + dopts dialOptions // Default and user specified dial options. + channelz *channelz.Channel // Channelz object. + resolverBuilder resolver.Builder // See initParsedTargetAndResolverBuilder(). + idlenessMgr *idle.Manager + metricsRecorderList *stats.MetricsRecorderList // The following provide their own synchronization, and therefore don't // require cc.mu to be held to access them. diff --git a/experimental/stats/metricregistry.go b/experimental/stats/metricregistry.go index e82eda566e3b..cc1d5617bb91 100644 --- a/experimental/stats/metricregistry.go +++ b/experimental/stats/metricregistry.go @@ -81,6 +81,12 @@ const ( // on. type Int64CountHandle MetricDescriptor +// Descriptor returns the int64 count handle typecast to a pointer to a +// MetricDescriptor. +func (h *Int64CountHandle) Descriptor() *MetricDescriptor { + return (*MetricDescriptor)(h) +} + // Record records the int64 count value on the metrics recorder provided. func (h *Int64CountHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { recorder.RecordInt64Count(h, incr, labels...) @@ -90,6 +96,12 @@ func (h *Int64CountHandle) Record(recorder MetricsRecorder, incr int64, labels . // passed at the recording point in order to know which metric to record on. type Float64CountHandle MetricDescriptor +// Descriptor returns the float64 count handle typecast to a pointer to a +// MetricDescriptor. +func (h *Float64CountHandle) Descriptor() *MetricDescriptor { + return (*MetricDescriptor)(h) +} + // Record records the float64 count value on the metrics recorder provided. func (h *Float64CountHandle) Record(recorder MetricsRecorder, incr float64, labels ...string) { recorder.RecordFloat64Count(h, incr, labels...) @@ -99,6 +111,12 @@ func (h *Float64CountHandle) Record(recorder MetricsRecorder, incr float64, labe // is passed at the recording point in order to know which metric to record on. type Int64HistoHandle MetricDescriptor +// Descriptor returns the int64 histo handle typecast to a pointer to a +// MetricDescriptor. +func (h *Int64HistoHandle) Descriptor() *MetricDescriptor { + return (*MetricDescriptor)(h) +} + // Record records the int64 histo value on the metrics recorder provided. func (h *Int64HistoHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { recorder.RecordInt64Histo(h, incr, labels...) @@ -109,6 +127,12 @@ func (h *Int64HistoHandle) Record(recorder MetricsRecorder, incr int64, labels . // record on. type Float64HistoHandle MetricDescriptor +// Descriptor returns the float64 histo handle typecast to a pointer to a +// MetricDescriptor. +func (h *Float64HistoHandle) Descriptor() *MetricDescriptor { + return (*MetricDescriptor)(h) +} + // Record records the float64 histo value on the metrics recorder provided. func (h *Float64HistoHandle) Record(recorder MetricsRecorder, incr float64, labels ...string) { recorder.RecordFloat64Histo(h, incr, labels...) @@ -118,6 +142,12 @@ func (h *Float64HistoHandle) Record(recorder MetricsRecorder, incr float64, labe // passed at the recording point in order to know which metric to record on. type Int64GaugeHandle MetricDescriptor +// Descriptor returns the int64 gauge handle typecast to a pointer to a +// MetricDescriptor. +func (h *Int64GaugeHandle) Descriptor() *MetricDescriptor { + return (*MetricDescriptor)(h) +} + // Record records the int64 histo value on the metrics recorder provided. func (h *Int64GaugeHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { recorder.RecordInt64Gauge(h, incr, labels...) diff --git a/experimental/stats/metricregistry_test.go b/experimental/stats/metricregistry_test.go index bd243648c262..6db7cf9ee402 100644 --- a/experimental/stats/metricregistry_test.go +++ b/experimental/stats/metricregistry_test.go @@ -112,27 +112,27 @@ func (s) TestMetricRegistry(t *testing.T) { // The Metric Descriptor in the handle should be able to identify the metric // information. This is the key passed to metrics recorder to identify // metric. - if got := fmr.intValues[(*MetricDescriptor)(intCountHandle1)]; got != 1 { + if got := fmr.intValues[intCountHandle1.Descriptor()]; got != 1 { t.Fatalf("fmr.intValues[intCountHandle1.MetricDescriptor] got %v, want: %v", got, 1) } floatCountHandle1.Record(fmr, 1.2, []string{"some label value", "some optional label value"}...) - if got := fmr.floatValues[(*MetricDescriptor)(floatCountHandle1)]; got != 1.2 { + if got := fmr.floatValues[floatCountHandle1.Descriptor()]; got != 1.2 { t.Fatalf("fmr.floatValues[floatCountHandle1.MetricDescriptor] got %v, want: %v", got, 1.2) } intHistoHandle1.Record(fmr, 3, []string{"some label value", "some optional label value"}...) - if got := fmr.intValues[(*MetricDescriptor)(intHistoHandle1)]; got != 3 { + if got := fmr.intValues[intHistoHandle1.Descriptor()]; got != 3 { t.Fatalf("fmr.intValues[intHistoHandle1.MetricDescriptor] got %v, want: %v", got, 3) } floatHistoHandle1.Record(fmr, 4.3, []string{"some label value", "some optional label value"}...) - if got := fmr.floatValues[(*MetricDescriptor)(floatHistoHandle1)]; got != 4.3 { + if got := fmr.floatValues[floatHistoHandle1.Descriptor()]; got != 4.3 { t.Fatalf("fmr.floatValues[floatHistoHandle1.MetricDescriptor] got %v, want: %v", got, 4.3) } intGaugeHandle1.Record(fmr, 7, []string{"some label value", "some optional label value"}...) - if got := fmr.intValues[(*MetricDescriptor)(intGaugeHandle1)]; got != 7 { + if got := fmr.intValues[intGaugeHandle1.Descriptor()]; got != 7 { t.Fatalf("fmr.intValues[intGaugeHandle1.MetricDescriptor] got %v, want: %v", got, 7) } } @@ -170,28 +170,28 @@ func TestNumerousIntCounts(t *testing.T) { fmr := newFakeMetricsRecorder(t) intCountHandle1.Record(fmr, 1, []string{"some label value", "some optional label value"}...) - got := []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + got := []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]} want := []int64{1, 0, 0} if diff := cmp.Diff(got, want); diff != "" { t.Fatalf("fmr.intValues (-got, +want): %v", diff) } intCountHandle2.Record(fmr, 1, []string{"some label value", "some optional label value"}...) - got = []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + got = []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]} want = []int64{1, 1, 0} if diff := cmp.Diff(got, want); diff != "" { t.Fatalf("fmr.intValues (-got, +want): %v", diff) } intCountHandle3.Record(fmr, 1, []string{"some label value", "some optional label value"}...) - got = []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + got = []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]} want = []int64{1, 1, 1} if diff := cmp.Diff(got, want); diff != "" { t.Fatalf("fmr.intValues (-got, +want): %v", diff) } intCountHandle3.Record(fmr, 1, []string{"some label value", "some optional label value"}...) - got = []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + got = []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]} want = []int64{1, 1, 2} if diff := cmp.Diff(got, want); diff != "" { t.Fatalf("fmr.intValues (-got, +want): %v", diff) @@ -236,26 +236,26 @@ func verifyLabels(t *testing.T, labelsWant []string, optionalLabelsWant []string } func (r *fakeMetricsRecorder) RecordInt64Count(handle *Int64CountHandle, incr int64, labels ...string) { - verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) - r.intValues[(*MetricDescriptor)(handle)] += incr + verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels) + r.intValues[handle.Descriptor()] += incr } func (r *fakeMetricsRecorder) RecordFloat64Count(handle *Float64CountHandle, incr float64, labels ...string) { - verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) - r.floatValues[(*MetricDescriptor)(handle)] += incr + verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels) + r.floatValues[handle.Descriptor()] += incr } func (r *fakeMetricsRecorder) RecordInt64Histo(handle *Int64HistoHandle, incr int64, labels ...string) { - verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) - r.intValues[(*MetricDescriptor)(handle)] += incr + verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels) + r.intValues[handle.Descriptor()] += incr } func (r *fakeMetricsRecorder) RecordFloat64Histo(handle *Float64HistoHandle, incr float64, labels ...string) { - verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) - r.floatValues[(*MetricDescriptor)(handle)] += incr + verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels) + r.floatValues[handle.Descriptor()] += incr } func (r *fakeMetricsRecorder) RecordInt64Gauge(handle *Int64GaugeHandle, incr int64, labels ...string) { - verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) - r.intValues[(*MetricDescriptor)(handle)] += incr + verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels) + r.intValues[handle.Descriptor()] += incr } diff --git a/internal/stats/metrics_recorder_list.go b/internal/stats/metrics_recorder_list.go new file mode 100644 index 000000000000..be110d41f9a4 --- /dev/null +++ b/internal/stats/metrics_recorder_list.go @@ -0,0 +1,95 @@ +/* + * Copyright 2024 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package stats + +import ( + "fmt" + + estats "google.golang.org/grpc/experimental/stats" + "google.golang.org/grpc/stats" +) + +// MetricsRecorderList forwards Record calls to all of its metricsRecorders. +// +// It eats any record calls where the label values provided do not match the +// number of label keys. +type MetricsRecorderList struct { + // metricsRecorders are the metrics recorders this list will forward to. + metricsRecorders []estats.MetricsRecorder +} + +// NewMetricsRecorderList creates a new metric recorder list with all the stats +// handlers provided which implement the MetricsRecorder interface. +// If no stats handlers provided implement the MetricsRecorder interface, +// the MetricsRecorder list returned is a no-op. +func NewMetricsRecorderList(shs []stats.Handler) *MetricsRecorderList { + var mrs []estats.MetricsRecorder + for _, sh := range shs { + if mr, ok := sh.(estats.MetricsRecorder); ok { + mrs = append(mrs, mr) + } + } + return &MetricsRecorderList{ + metricsRecorders: mrs, + } +} + +func verifyLabels(desc *estats.MetricDescriptor, labelsRecv ...string) { + if got, want := len(labelsRecv), len(desc.Labels)+len(desc.OptionalLabels); got != want { + panic(fmt.Sprintf("Received %d labels in call to record metric %q, but expected %d.", got, desc.Name, want)) + } +} + +func (l *MetricsRecorderList) RecordInt64Count(handle *estats.Int64CountHandle, incr int64, labels ...string) { + verifyLabels(handle.Descriptor(), labels...) + + for _, metricRecorder := range l.metricsRecorders { + metricRecorder.RecordInt64Count(handle, incr, labels...) + } +} + +func (l *MetricsRecorderList) RecordFloat64Count(handle *estats.Float64CountHandle, incr float64, labels ...string) { + verifyLabels(handle.Descriptor(), labels...) + + for _, metricRecorder := range l.metricsRecorders { + metricRecorder.RecordFloat64Count(handle, incr, labels...) + } +} + +func (l *MetricsRecorderList) RecordInt64Histo(handle *estats.Int64HistoHandle, incr int64, labels ...string) { + verifyLabels(handle.Descriptor(), labels...) + + for _, metricRecorder := range l.metricsRecorders { + metricRecorder.RecordInt64Histo(handle, incr, labels...) + } +} + +func (l *MetricsRecorderList) RecordFloat64Histo(handle *estats.Float64HistoHandle, incr float64, labels ...string) { + verifyLabels(handle.Descriptor(), labels...) + + for _, metricRecorder := range l.metricsRecorders { + metricRecorder.RecordFloat64Histo(handle, incr, labels...) + } +} + +func (l *MetricsRecorderList) RecordInt64Gauge(handle *estats.Int64GaugeHandle, incr int64, labels ...string) { + verifyLabels(handle.Descriptor(), labels...) + + for _, metricRecorder := range l.metricsRecorders { + metricRecorder.RecordInt64Gauge(handle, incr, labels...) + } +} diff --git a/internal/stats/metrics_recorder_list_test.go b/internal/stats/metrics_recorder_list_test.go new file mode 100644 index 000000000000..b09ad043ce65 --- /dev/null +++ b/internal/stats/metrics_recorder_list_test.go @@ -0,0 +1,234 @@ +/* + * + * Copyright 2024 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package stats_test implements an e2e test for the Metrics Recorder List +// component of the Client Conn. +package stats_test + +import ( + "context" + "fmt" + "log" + "strings" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/balancer" + "google.golang.org/grpc/balancer/pickfirst" + "google.golang.org/grpc/credentials/insecure" + estats "google.golang.org/grpc/experimental/stats" + "google.golang.org/grpc/internal" + "google.golang.org/grpc/internal/grpctest" + istats "google.golang.org/grpc/internal/stats" + "google.golang.org/grpc/internal/testutils/stats" + testgrpc "google.golang.org/grpc/interop/grpc_testing" + testpb "google.golang.org/grpc/interop/grpc_testing" + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/resolver/manual" + "google.golang.org/grpc/serviceconfig" +) + +var defaultTestTimeout = 5 * time.Second + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +var ( + intCountHandle = estats.RegisterInt64Count(estats.MetricDescriptor{ + Name: "simple counter", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int counter label"}, + OptionalLabels: []string{"int counter optional label"}, + Default: false, + }) + floatCountHandle = estats.RegisterFloat64Count(estats.MetricDescriptor{ + Name: "float counter", + Description: "sum of all emissions from tests", + Unit: "float", + Labels: []string{"float counter label"}, + OptionalLabels: []string{"float counter optional label"}, + Default: false, + }) + intHistoHandle = estats.RegisterInt64Histo(estats.MetricDescriptor{ + Name: "int histo", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int histo label"}, + OptionalLabels: []string{"int histo optional label"}, + Default: false, + }) + floatHistoHandle = estats.RegisterFloat64Histo(estats.MetricDescriptor{ + Name: "float histo", + Description: "sum of all emissions from tests", + Unit: "float", + Labels: []string{"float histo label"}, + OptionalLabels: []string{"float histo optional label"}, + Default: false, + }) + intGaugeHandle = estats.RegisterInt64Gauge(estats.MetricDescriptor{ + Name: "simple gauge", + Description: "the most recent int emitted by test", + Unit: "int", + Labels: []string{"int gauge label"}, + OptionalLabels: []string{"int gauge optional label"}, + Default: false, + }) +) + +func init() { + balancer.Register(recordingLoadBalancerBuilder{}) +} + +const recordingLoadBalancerName = "recording_load_balancer" + +type recordingLoadBalancerBuilder struct{} + +func (recordingLoadBalancerBuilder) Name() string { + return recordingLoadBalancerName +} + +func (recordingLoadBalancerBuilder) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer { + intCountHandle.Record(bOpts.MetricsRecorder, 1, "int counter label val", "int counter optional label val") + floatCountHandle.Record(bOpts.MetricsRecorder, 2, "float counter label val", "float counter optional label val") + intHistoHandle.Record(bOpts.MetricsRecorder, 3, "int histo label val", "int histo optional label val") + floatHistoHandle.Record(bOpts.MetricsRecorder, 4, "float histo label val", "float histo optional label val") + intGaugeHandle.Record(bOpts.MetricsRecorder, 5, "int gauge label val", "int gauge optional label val") + + return &recordingLoadBalancer{ + Balancer: balancer.Get(pickfirst.Name).Build(cc, bOpts), + } +} + +type recordingLoadBalancer struct { + balancer.Balancer +} + +// TestMetricsRecorderList tests the metrics recorder list functionality of the +// ClientConn. It configures a global and local stats handler Dial Option. These +// stats handlers implement the MetricsRecorder interface. It also configures a +// balancer which registers metrics and records on metrics at build time. This +// test then asserts that the recorded metrics show up on both configured stats +// handlers. +func (s) TestMetricsRecorderList(t *testing.T) { + internal.SnapshotMetricRegistryForTesting.(func(t *testing.T))(t) + mr := manual.NewBuilderWithScheme("test-metrics-recorder-list") + defer mr.Close() + + json := `{"loadBalancingConfig": [{"recording_load_balancer":{}}]}` + sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json) + mr.InitialState(resolver.State{ + ServiceConfig: sc, + }) + + // Create two stats.Handlers which also implement MetricsRecorder, configure + // one as a global dial option and one as a local dial option. + mr1 := stats.NewTestMetricsRecorder(t, []string{}) + mr2 := stats.NewTestMetricsRecorder(t, []string{}) + + defer internal.ClearGlobalDialOptions() + internal.AddGlobalDialOptions.(func(opt ...grpc.DialOption))(grpc.WithStatsHandler(mr1)) + + cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithStatsHandler(mr2)) + if err != nil { + log.Fatalf("Failed to dial: %v", err) + } + defer cc.Close() + + tsc := testgrpc.NewTestServiceClient(cc) + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + // Trigger the recording_load_balancer to build, which will trigger metrics + // to record. + tsc.UnaryCall(ctx, &testpb.SimpleRequest{}) + + mdWant := stats.MetricsData{ + Handle: intCountHandle.Descriptor(), + IntIncr: 1, + LabelKeys: []string{"int counter label", "int counter optional label"}, + LabelVals: []string{"int counter label val", "int counter optional label val"}, + } + mr1.WaitForInt64Count(ctx, mdWant) + mr2.WaitForInt64Count(ctx, mdWant) + + mdWant = stats.MetricsData{ + Handle: floatCountHandle.Descriptor(), + FloatIncr: 2, + LabelKeys: []string{"float counter label", "float counter optional label"}, + LabelVals: []string{"float counter label val", "float counter optional label val"}, + } + mr1.WaitForFloat64Count(ctx, mdWant) + mr2.WaitForFloat64Count(ctx, mdWant) + + mdWant = stats.MetricsData{ + Handle: intHistoHandle.Descriptor(), + IntIncr: 3, + LabelKeys: []string{"int histo label", "int histo optional label"}, + LabelVals: []string{"int histo label val", "int histo optional label val"}, + } + mr1.WaitForInt64Histo(ctx, mdWant) + mr2.WaitForInt64Histo(ctx, mdWant) + + mdWant = stats.MetricsData{ + Handle: floatHistoHandle.Descriptor(), + FloatIncr: 4, + LabelKeys: []string{"float histo label", "float histo optional label"}, + LabelVals: []string{"float histo label val", "float histo optional label val"}, + } + mr1.WaitForFloat64Histo(ctx, mdWant) + mr2.WaitForFloat64Histo(ctx, mdWant) + mdWant = stats.MetricsData{ + Handle: intGaugeHandle.Descriptor(), + IntIncr: 5, + LabelKeys: []string{"int gauge label", "int gauge optional label"}, + LabelVals: []string{"int gauge label val", "int gauge optional label val"}, + } + mr1.WaitForInt64Gauge(ctx, mdWant) + mr2.WaitForInt64Gauge(ctx, mdWant) +} + +// TestMetricRecorderListPanic tests that the metrics recorder list panics if +// received the wrong number of labels for a particular metric. +func (s) TestMetricRecorderListPanic(t *testing.T) { + internal.SnapshotMetricRegistryForTesting.(func(t *testing.T))(t) + intCountHandle := estats.RegisterInt64Count(estats.MetricDescriptor{ + Name: "simple counter", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int counter label"}, + OptionalLabels: []string{"int counter optional label"}, + Default: false, + }) + mrl := istats.NewMetricsRecorderList(nil) + + want := `Received 1 labels in call to record metric "simple counter", but expected 2.` + defer func() { + if r := recover(); !strings.Contains(fmt.Sprint(r), want) { + t.Errorf("expected panic contains %q, got %q", want, r) + } + }() + + intCountHandle.Record(mrl, 1, "only one label") +} diff --git a/internal/testutils/stats/test_metrics_recorder.go b/internal/testutils/stats/test_metrics_recorder.go new file mode 100644 index 000000000000..25817be50b37 --- /dev/null +++ b/internal/testutils/stats/test_metrics_recorder.go @@ -0,0 +1,182 @@ +/* + * + * Copyright 2024 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package stats implements a TestMetricsRecorder utility. +package stats + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + estats "google.golang.org/grpc/experimental/stats" + "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/stats" +) + +// TestMetricsRecorder is a MetricsRecorder to be used in tests. It sends +// recording events on channels and provides helpers to check if certain events +// have taken place. It also persists metrics data keyed on the metrics +// descriptor. +type TestMetricsRecorder struct { + t *testing.T + + intCountCh *testutils.Channel + floatCountCh *testutils.Channel + intHistoCh *testutils.Channel + floatHistoCh *testutils.Channel + intGaugeCh *testutils.Channel +} + +func NewTestMetricsRecorder(t *testing.T, metrics []string) *TestMetricsRecorder { + return &TestMetricsRecorder{ + t: t, + + intCountCh: testutils.NewChannelWithSize(10), + floatCountCh: testutils.NewChannelWithSize(10), + intHistoCh: testutils.NewChannelWithSize(10), + floatHistoCh: testutils.NewChannelWithSize(10), + intGaugeCh: testutils.NewChannelWithSize(10), + } +} + +type MetricsData struct { + Handle *estats.MetricDescriptor + + // Only set based on the type of metric. So only one of IntIncr or FloatIncr + // is set. + IntIncr int64 + FloatIncr float64 + + LabelKeys []string + LabelVals []string +} + +func (r *TestMetricsRecorder) WaitForInt64Count(ctx context.Context, metricsDataWant MetricsData) { + got, err := r.intCountCh.Receive(ctx) + if err != nil { + r.t.Fatalf("timeout waiting for int64Count") + } + metricsDataGot := got.(MetricsData) + if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { + r.t.Fatalf("int64count metricsData received unexpected value (-got, +want): %v", diff) + } +} + +func (r *TestMetricsRecorder) RecordInt64Count(handle *estats.Int64CountHandle, incr int64, labels ...string) { + r.intCountCh.Send(MetricsData{ + Handle: handle.Descriptor(), + IntIncr: incr, + LabelKeys: append(handle.Labels, handle.OptionalLabels...), + LabelVals: labels, + }) +} + +func (r *TestMetricsRecorder) WaitForFloat64Count(ctx context.Context, metricsDataWant MetricsData) { + got, err := r.floatCountCh.Receive(ctx) + if err != nil { + r.t.Fatalf("timeout waiting for float64Count") + } + metricsDataGot := got.(MetricsData) + if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { + r.t.Fatalf("float64count metricsData received unexpected value (-got, +want): %v", diff) + } +} + +func (r *TestMetricsRecorder) RecordFloat64Count(handle *estats.Float64CountHandle, incr float64, labels ...string) { + r.floatCountCh.Send(MetricsData{ + Handle: handle.Descriptor(), + FloatIncr: incr, + LabelKeys: append(handle.Labels, handle.OptionalLabels...), + LabelVals: labels, + }) +} + +func (r *TestMetricsRecorder) WaitForInt64Histo(ctx context.Context, metricsDataWant MetricsData) { + got, err := r.intHistoCh.Receive(ctx) + if err != nil { + r.t.Fatalf("timeout waiting for int64Histo") + } + metricsDataGot := got.(MetricsData) + if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { + r.t.Fatalf("int64Histo metricsData received unexpected value (-got, +want): %v", diff) + } +} + +func (r *TestMetricsRecorder) RecordInt64Histo(handle *estats.Int64HistoHandle, incr int64, labels ...string) { + r.intHistoCh.Send(MetricsData{ + Handle: handle.Descriptor(), + IntIncr: incr, + LabelKeys: append(handle.Labels, handle.OptionalLabels...), + LabelVals: labels, + }) +} + +func (r *TestMetricsRecorder) WaitForFloat64Histo(ctx context.Context, metricsDataWant MetricsData) { + got, err := r.floatHistoCh.Receive(ctx) + if err != nil { + r.t.Fatalf("timeout waiting for float64Histo") + } + metricsDataGot := got.(MetricsData) + if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { + r.t.Fatalf("float64Histo metricsData received unexpected value (-got, +want): %v", diff) + } +} + +func (r *TestMetricsRecorder) RecordFloat64Histo(handle *estats.Float64HistoHandle, incr float64, labels ...string) { + r.floatHistoCh.Send(MetricsData{ + Handle: handle.Descriptor(), + FloatIncr: incr, + LabelKeys: append(handle.Labels, handle.OptionalLabels...), + LabelVals: labels, + }) +} + +func (r *TestMetricsRecorder) WaitForInt64Gauge(ctx context.Context, metricsDataWant MetricsData) { + got, err := r.intGaugeCh.Receive(ctx) + if err != nil { + r.t.Fatalf("timeout waiting for int64Gauge") + } + metricsDataGot := got.(MetricsData) + if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { + r.t.Fatalf("int64Gauge metricsData received unexpected value (-got, +want): %v", diff) + } +} + +func (r *TestMetricsRecorder) RecordInt64Gauge(handle *estats.Int64GaugeHandle, incr int64, labels ...string) { + r.intGaugeCh.Send(MetricsData{ + Handle: handle.Descriptor(), + IntIncr: incr, + LabelKeys: append(handle.Labels, handle.OptionalLabels...), + LabelVals: labels, + }) +} + +// To implement a stats.Handler, which allows it to be set as a dial option: + +func (r *TestMetricsRecorder) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { + return ctx +} + +func (r *TestMetricsRecorder) HandleRPC(context.Context, stats.RPCStats) {} + +func (r *TestMetricsRecorder) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { + return ctx +} + +func (r *TestMetricsRecorder) HandleConn(context.Context, stats.ConnStats) {} diff --git a/stats/opentelemetry/opentelemetry.go b/stats/opentelemetry/opentelemetry.go index fbc1ec1c0549..b0d0cde7b891 100644 --- a/stats/opentelemetry/opentelemetry.go +++ b/stats/opentelemetry/opentelemetry.go @@ -329,7 +329,7 @@ func (rm *registryMetrics) registerMetrics(metrics *estats.Metrics, meter otelme } func (rm *registryMetrics) RecordInt64Count(handle *estats.Int64CountHandle, incr int64, labels ...string) { - desc := (*estats.MetricDescriptor)(handle) + desc := handle.Descriptor() ao := optionFromLabels(desc.Labels, desc.OptionalLabels, rm.optionalLabels, labels...) if ic, ok := rm.intCounts[desc]; ok { @@ -338,7 +338,7 @@ func (rm *registryMetrics) RecordInt64Count(handle *estats.Int64CountHandle, inc } func (rm *registryMetrics) RecordFloat64Count(handle *estats.Float64CountHandle, incr float64, labels ...string) { - desc := (*estats.MetricDescriptor)(handle) + desc := handle.Descriptor() ao := optionFromLabels(desc.Labels, desc.OptionalLabels, rm.optionalLabels, labels...) if fc, ok := rm.floatCounts[desc]; ok { fc.Add(context.TODO(), incr, ao) @@ -346,7 +346,7 @@ func (rm *registryMetrics) RecordFloat64Count(handle *estats.Float64CountHandle, } func (rm *registryMetrics) RecordInt64Histo(handle *estats.Int64HistoHandle, incr int64, labels ...string) { - desc := (*estats.MetricDescriptor)(handle) + desc := handle.Descriptor() ao := optionFromLabels(desc.Labels, desc.OptionalLabels, rm.optionalLabels, labels...) if ih, ok := rm.intHistos[desc]; ok { ih.Record(context.TODO(), incr, ao) @@ -354,7 +354,7 @@ func (rm *registryMetrics) RecordInt64Histo(handle *estats.Int64HistoHandle, inc } func (rm *registryMetrics) RecordFloat64Histo(handle *estats.Float64HistoHandle, incr float64, labels ...string) { - desc := (*estats.MetricDescriptor)(handle) + desc := handle.Descriptor() ao := optionFromLabels(desc.Labels, desc.OptionalLabels, rm.optionalLabels, labels...) if fh, ok := rm.floatHistos[desc]; ok { fh.Record(context.TODO(), incr, ao) @@ -362,7 +362,7 @@ func (rm *registryMetrics) RecordFloat64Histo(handle *estats.Float64HistoHandle, } func (rm *registryMetrics) RecordInt64Gauge(handle *estats.Int64GaugeHandle, incr int64, labels ...string) { - desc := (*estats.MetricDescriptor)(handle) + desc := handle.Descriptor() ao := optionFromLabels(desc.Labels, desc.OptionalLabels, rm.optionalLabels, labels...) if ig, ok := rm.intGauges[desc]; ok { ig.Record(context.TODO(), incr, ao)