Skip to content

Commit c777496

Browse files
committed
Add Metrics Recorder List and usage in ClientConn
1 parent 2bcbcab commit c777496

File tree

7 files changed

+561
-7
lines changed

7 files changed

+561
-7
lines changed

balancer/balancer.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"google.golang.org/grpc/channelz"
3131
"google.golang.org/grpc/connectivity"
3232
"google.golang.org/grpc/credentials"
33+
estats "google.golang.org/grpc/experimental/stats"
3334
"google.golang.org/grpc/grpclog"
3435
"google.golang.org/grpc/internal"
3536
"google.golang.org/grpc/metadata"
@@ -256,6 +257,10 @@ type BuildOptions struct {
256257
// same resolver.Target as passed to the resolver. See the documentation for
257258
// the resolver.Target type for details about what it contains.
258259
Target resolver.Target
260+
// MetricsRecorder is the metrics recorder that balancers can use to record
261+
// metrics. Balancer implementations which do not register metrics on
262+
// metrics registry and record on them can ignore this field.
263+
MetricsRecorder estats.MetricsRecorder
259264
}
260265

261266
// Builder creates a balancer.

balancer_wrapper.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func newCCBalancerWrapper(cc *ClientConn) *ccBalancerWrapper {
8282
CustomUserAgent: cc.dopts.copts.UserAgent,
8383
ChannelzParent: cc.channelz,
8484
Target: cc.parsedTarget,
85+
MetricsRecorder: cc.metricsRecorderList,
8586
},
8687
serializer: grpcsync.NewCallbackSerializer(ctx),
8788
serializerCancel: cancel,

clientconn.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"google.golang.org/grpc/internal/grpcsync"
4141
"google.golang.org/grpc/internal/idle"
4242
iresolver "google.golang.org/grpc/internal/resolver"
43+
"google.golang.org/grpc/internal/stats"
4344
"google.golang.org/grpc/internal/transport"
4445
"google.golang.org/grpc/keepalive"
4546
"google.golang.org/grpc/resolver"
@@ -195,8 +196,11 @@ func NewClient(target string, opts ...DialOption) (conn *ClientConn, err error)
195196
cc.csMgr = newConnectivityStateManager(cc.ctx, cc.channelz)
196197
cc.pickerWrapper = newPickerWrapper(cc.dopts.copts.StatsHandlers)
197198

199+
cc.metricsRecorderList = stats.NewMetricsRecorderList(cc.dopts.copts.StatsHandlers)
200+
198201
cc.initIdleStateLocked() // Safe to call without the lock, since nothing else has a reference to cc.
199202
cc.idlenessMgr = idle.NewManager((*idler)(cc), cc.dopts.idleTimeout)
203+
200204
return cc, nil
201205
}
202206

@@ -591,13 +595,14 @@ type ClientConn struct {
591595
cancel context.CancelFunc // Cancelled on close.
592596

593597
// The following are initialized at dial time, and are read-only after that.
594-
target string // User's dial target.
595-
parsedTarget resolver.Target // See initParsedTargetAndResolverBuilder().
596-
authority string // See initAuthority().
597-
dopts dialOptions // Default and user specified dial options.
598-
channelz *channelz.Channel // Channelz object.
599-
resolverBuilder resolver.Builder // See initParsedTargetAndResolverBuilder().
600-
idlenessMgr *idle.Manager
598+
target string // User's dial target.
599+
parsedTarget resolver.Target // See initParsedTargetAndResolverBuilder().
600+
authority string // See initAuthority().
601+
dopts dialOptions // Default and user specified dial options.
602+
channelz *channelz.Channel // Channelz object.
603+
resolverBuilder resolver.Builder // See initParsedTargetAndResolverBuilder().
604+
idlenessMgr *idle.Manager
605+
metricsRecorderList *stats.MetricsRecorderList
601606

602607
// The following provide their own synchronization, and therefore don't
603608
// require cc.mu to be held to access them.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2024 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package stats
18+
19+
import (
20+
estats "google.golang.org/grpc/experimental/stats"
21+
"google.golang.org/grpc/grpclog"
22+
"google.golang.org/grpc/stats"
23+
)
24+
25+
var logger = grpclog.Component("metrics-recorder-list")
26+
27+
// MetricsRecorderList forwards Record calls to all of it's metricsRecorders.
28+
//
29+
// It eats any record calls where the label values provided do not match the
30+
// number of label keys.
31+
type MetricsRecorderList struct {
32+
// metricsRecorders are the metrics recorders this list will forward to.
33+
metricsRecorders []estats.MetricsRecorder
34+
}
35+
36+
// NewMetricsRecorderList creates a new metric recorder list with all the stats
37+
// handlers provided which implement the MetricsRecorder interface.
38+
// If no stats handlers provided implement the MetricsRecorder interface,
39+
// the MetricsRecorder list returned is a no-op.
40+
func NewMetricsRecorderList(shs []stats.Handler) *MetricsRecorderList {
41+
var mrs []estats.MetricsRecorder
42+
for _, sh := range shs {
43+
if mr, ok := sh.(estats.MetricsRecorder); ok {
44+
mrs = append(mrs, mr)
45+
}
46+
}
47+
return &MetricsRecorderList{
48+
metricsRecorders: mrs,
49+
}
50+
}
51+
52+
func (l *MetricsRecorderList) RecordInt64Count(handle *estats.Int64CountHandle, incr int64, labels ...string) {
53+
if got, want := len(handle.Labels)+len(handle.OptionalLabels), len(labels); got != want {
54+
logger.Infof("length of labels passed to RecordInt64Count incorrect got: %v, want: %v", got, want)
55+
}
56+
57+
for _, metricRecorder := range l.metricsRecorders {
58+
metricRecorder.RecordInt64Count(handle, incr, labels...)
59+
}
60+
}
61+
62+
func (l *MetricsRecorderList) RecordFloat64Count(handle *estats.Float64CountHandle, incr float64, labels ...string) {
63+
if got, want := len(handle.Labels)+len(handle.OptionalLabels), len(labels); got != want {
64+
logger.Infof("length of labels passed to RecordFloat64Count incorrect got: %v, want: %v", got, want)
65+
}
66+
67+
for _, metricRecorder := range l.metricsRecorders {
68+
metricRecorder.RecordFloat64Count(handle, incr, labels...)
69+
}
70+
}
71+
72+
func (l *MetricsRecorderList) RecordInt64Histo(handle *estats.Int64HistoHandle, incr int64, labels ...string) {
73+
if got, want := len(handle.Labels)+len(handle.OptionalLabels), len(labels); got != want {
74+
logger.Infof("length of labels passed to RecordInt64Histo incorrect got: %v, want: %v", got, want)
75+
}
76+
77+
for _, metricRecorder := range l.metricsRecorders {
78+
metricRecorder.RecordInt64Histo(handle, incr, labels...)
79+
}
80+
}
81+
82+
func (l *MetricsRecorderList) RecordFloat64Histo(handle *estats.Float64HistoHandle, incr float64, labels ...string) {
83+
if got, want := len(handle.Labels)+len(handle.OptionalLabels), len(labels); got != want {
84+
logger.Infof("length of labels passed to RecordFloat64Histo incorrect got: %v, want: %v", got, want)
85+
}
86+
87+
for _, metricRecorder := range l.metricsRecorders {
88+
metricRecorder.RecordFloat64Histo(handle, incr, labels...)
89+
}
90+
}
91+
92+
func (l *MetricsRecorderList) RecordInt64Gauge(handle *estats.Int64GaugeHandle, incr int64, labels ...string) {
93+
if got, want := len(handle.Labels)+len(handle.OptionalLabels), len(labels); got != want {
94+
logger.Infof("length of labels passed to RecordInt64Gauge incorrect got: %v, want: %v", got, want)
95+
}
96+
97+
for _, metricRecorder := range l.metricsRecorders {
98+
metricRecorder.RecordInt64Gauge(handle, incr, labels...)
99+
}
100+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 test implements an e2e test for the Metrics Recorder List component
20+
// of the Client Conn, and a TestMetricsRecorder utility.
21+
package test
22+
23+
import (
24+
"context"
25+
"log"
26+
"testing"
27+
"time"
28+
29+
"google.golang.org/grpc"
30+
"google.golang.org/grpc/credentials/insecure"
31+
estats "google.golang.org/grpc/experimental/stats"
32+
"google.golang.org/grpc/internal"
33+
"google.golang.org/grpc/internal/grpctest"
34+
testgrpc "google.golang.org/grpc/interop/grpc_testing"
35+
testpb "google.golang.org/grpc/interop/grpc_testing"
36+
"google.golang.org/grpc/resolver"
37+
"google.golang.org/grpc/resolver/manual"
38+
"google.golang.org/grpc/serviceconfig"
39+
)
40+
41+
var defaultTestTimeout = 5 * time.Second
42+
43+
type s struct {
44+
grpctest.Tester
45+
}
46+
47+
func Test(t *testing.T) {
48+
grpctest.RunSubTests(t, s{})
49+
}
50+
51+
// TestMetricsRecorderList tests the metrics recorder list functionality of the
52+
// ClientConn. It configures a global and local stats handler Dial Option. These
53+
// stats handlers implement the MetricsRecorder interface. It also configures a
54+
// balancer which registers metrics and records on metrics at build time. This
55+
// test then asserts that the recorded metrics show up on both configured stats
56+
// handlers, and that metrics calls with the incorrect number of labels do not
57+
// make their way to stats handlers.
58+
func (s) TestMetricsRecorderList(t *testing.T) {
59+
mr := manual.NewBuilderWithScheme("test-metrics-recorder-list")
60+
defer mr.Close()
61+
62+
json := `{"loadBalancingConfig": [{"recording_load_balancer":{}}]}`
63+
sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json)
64+
mr.InitialState(resolver.State{
65+
ServiceConfig: sc,
66+
})
67+
68+
// Create two stats.Handlers which also implement MetricsRecorder, configure
69+
// one as a global dial option and one as a local dial option.
70+
mr1 := NewTestMetricsRecorder(t, []string{})
71+
mr2 := NewTestMetricsRecorder(t, []string{})
72+
73+
defer internal.ClearGlobalDialOptions()
74+
internal.AddGlobalDialOptions.(func(opt ...grpc.DialOption))(grpc.WithStatsHandler(mr1))
75+
76+
cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithStatsHandler(mr2))
77+
if err != nil {
78+
log.Fatalf("Failed to dial: %v", err)
79+
}
80+
defer cc.Close()
81+
82+
tsc := testgrpc.NewTestServiceClient(cc)
83+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
84+
defer cancel()
85+
86+
// Trigger the recording_load_balancer to build, which will trigger metrics
87+
// to record.
88+
tsc.UnaryCall(ctx, &testpb.SimpleRequest{})
89+
90+
mdWant := MetricsData{
91+
Handle: (*estats.MetricDescriptor)(intCountHandle),
92+
IntIncr: 1,
93+
LabelKeys: []string{"int counter label", "int counter optional label"},
94+
LabelVals: []string{"int counter label val", "int counter optional label val"},
95+
}
96+
mr1.WaitForInt64Count(ctx, mdWant)
97+
mr2.WaitForInt64Count(ctx, mdWant)
98+
99+
mdWant = MetricsData{
100+
Handle: (*estats.MetricDescriptor)(floatCountHandle),
101+
FloatIncr: 2,
102+
LabelKeys: []string{"float counter label", "float counter optional label"},
103+
LabelVals: []string{"float counter label val", "float counter optional label val"},
104+
}
105+
mr1.WaitForFloat64Count(ctx, mdWant)
106+
mr2.WaitForFloat64Count(ctx, mdWant)
107+
108+
mdWant = MetricsData{
109+
Handle: (*estats.MetricDescriptor)(intHistoHandle),
110+
IntIncr: 3,
111+
LabelKeys: []string{"int histo label", "int histo optional label"},
112+
LabelVals: []string{"int histo label val", "int histo optional label val"},
113+
}
114+
mr1.WaitForInt64Histo(ctx, mdWant)
115+
mr2.WaitForInt64Histo(ctx, mdWant)
116+
117+
mdWant = MetricsData{
118+
Handle: (*estats.MetricDescriptor)(floatHistoHandle),
119+
FloatIncr: 4,
120+
LabelKeys: []string{"float histo label", "float histo optional label"},
121+
LabelVals: []string{"float histo label val", "float histo optional label val"},
122+
}
123+
mr1.WaitForFloat64Histo(ctx, mdWant)
124+
mr2.WaitForFloat64Histo(ctx, mdWant)
125+
mdWant = MetricsData{
126+
Handle: (*estats.MetricDescriptor)(intGaugeHandle),
127+
IntIncr: 5, // Should ignore the 7 metrics recording point because emits wrong number of labels.
128+
LabelKeys: []string{"int gauge label", "int gauge optional label"},
129+
LabelVals: []string{"int gauge label val", "int gauge optional label val"},
130+
}
131+
mr1.WaitForInt64Gauge(ctx, mdWant)
132+
mr2.WaitForInt64Gauge(ctx, mdWant)
133+
}

0 commit comments

Comments
 (0)