Skip to content
This repository was archived by the owner on Apr 18, 2023. It is now read-only.

Commit 6b7015e

Browse files
author
Michal Witkowski
authored
Merge pull request #10 from grpc-ecosystem/feature/client-metrics
add Client-Side monitoring.
2 parents 1de93bf + 40bc88d commit 6b7015e

File tree

6 files changed

+463
-54
lines changed

6 files changed

+463
-54
lines changed

README.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![GoDoc](http://img.shields.io/badge/GoDoc-Reference-blue.svg)](https://godoc.org/github.com/grpc-ecosystem/go-grpc-prometheus)
66
[![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
77

8-
[Prometheus](https://prometheus.io/) monitoring for your [gRPC Go](https://github.com/grpc/grpc-go) servers.
8+
[Prometheus](https://prometheus.io/) monitoring for your [gRPC Go](https://github.com/grpc/grpc-go) servers and clients.
99

1010
A sister implementation for [gRPC Java](https://github.com/grpc/grpc-java) (same metrics, same semantics) is in [grpc-ecosystem/java-grpc-prometheus](https://github.com/grpc-ecosystem/java-grpc-prometheus).
1111

@@ -19,6 +19,10 @@ To use Interceptors in chains, please see [`go-grpc-middleware`](https://github.
1919

2020
## Usage
2121

22+
There are two types of interceptors: client-side and server-side. This package provides monitoring Interceptors for both.
23+
24+
### Server-side
25+
2226
```go
2327
import "github.com/grpc-ecosystem/go-grpc-prometheus"
2428
...
@@ -27,17 +31,35 @@ import "github.com/grpc-ecosystem/go-grpc-prometheus"
2731
grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor),
2832
grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor),
2933
)
34+
// Register your gRPC service implementations.
35+
myservice.RegisterMyServiceServer(s.server, &myServiceImpl{})
36+
// After all your registrations, make sure all of the Prometheus metrics are initialized.
37+
grpc_prometheus.Register(myServer)
3038
// Register Prometheus metrics handler.
3139
http.Handle("/metrics", prometheus.Handler())
3240
...
3341
```
3442

35-
# Metrics
43+
### Client-side
3644

45+
```go
46+
import "github.com/grpc-ecosystem/go-grpc-prometheus"
47+
...
48+
clientConn, err = grpc.Dial(
49+
address,
50+
grpc.WithUnaryInterceptor(UnaryClientInterceptor),
51+
grpc.WithStreamInterceptor(StreamClientInterceptor)
52+
)
53+
client = pb_testproto.NewTestServiceClient(clientConn)
54+
resp, err := client.PingEmpty(s.ctx, &myservice.Request{Msg: "hello"})
55+
...
56+
```
57+
58+
# Metrics
3759

3860
## Labels
3961

40-
All server-side metrics start with `grpc_server` as Prometheus subsystem name. Similarly all methods
62+
All server-side metrics start with `grpc_server` as Prometheus subsystem name. All client-side metrics start with `grpc_client`. Both of them have mirror-concepts. Similarly all methods
4163
contain the same rich labels:
4264

4365
* `grpc_service` - the [gRPC service](http://www.grpc.io/docs/#defining-a-service) name, which is the combination of protobuf `package` and
@@ -65,9 +87,11 @@ Additionally for completed RPCs, the following labels are used:
6587

6688
## Counters
6789

68-
The counters and their up to date documentation is in [server_reporter.go](server_reporter.go) and
90+
The counters and their up to date documentation is in [server_reporter.go](server_reporter.go) and [client_reporter.go](client_reporter.go)
6991
the respective Prometheus handler (usually `/metrics`).
7092

93+
For the purpose of this documentation we will only discuss `grpc_server` metrics. The `grpc_client` ones contain mirror concepts.
94+
7195
For simplicity, let's assume we're tracking a single server-side RPC call of [`mwitkow.testproto.TestService`](examples/testproto/test.proto),
7296
calling the method `PingList`. The call succeeds and returns 20 messages in the stream.
7397

@@ -214,8 +238,7 @@ e.g. "less than 1% of requests are slower than 250ms".
214238

215239
## Status
216240

217-
This code has been in an upstream [pull request](https://github.com/grpc/grpc-go/pull/299) since August 2015. It has
218-
served as the basis for monitoring of production gRPC micro services at [Improbable](https://improbable.io) since then.
241+
This code has been used since August 2015 as the basis for monitoring of *production* gRPC micro services at [Improbable](https://improbable.io).
219242

220243
## License
221244

client.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2016 Michal Witkowski. All Rights Reserved.
2+
// See LICENSE for licensing terms.
3+
4+
// gRPC Prometheus monitoring interceptors for client-side gRPC.
5+
6+
package grpc_prometheus
7+
8+
import (
9+
"io"
10+
11+
"golang.org/x/net/context"
12+
"google.golang.org/grpc"
13+
"google.golang.org/grpc/codes"
14+
)
15+
16+
// UnaryClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Unary RPCs.
17+
func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
18+
monitor := newClientReporter(Unary, method)
19+
monitor.SentMessage()
20+
err := invoker(ctx, method, req, reply, cc, opts...)
21+
if err != nil {
22+
monitor.ReceivedMessage()
23+
}
24+
monitor.Handled(grpc.Code(err))
25+
return err
26+
}
27+
28+
// StreamServerInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Streaming RPCs.
29+
func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
30+
monitor := newClientReporter(clientStreamType(desc), method)
31+
clientStream, err := streamer(ctx, desc, cc, method, opts...)
32+
if err != nil {
33+
monitor.Handled(grpc.Code(err))
34+
return nil, err
35+
}
36+
return &monitoredClientStream{clientStream, monitor}, nil
37+
}
38+
39+
func clientStreamType(desc *grpc.StreamDesc) grpcType {
40+
if desc.ClientStreams && !desc.ServerStreams {
41+
return ClientStream
42+
} else if !desc.ClientStreams && desc.ServerStreams {
43+
return ServerStream
44+
}
45+
return BidiStream
46+
}
47+
48+
// monitoredClientStream wraps grpc.ClientStream allowing each Sent/Recv of message to increment counters.
49+
type monitoredClientStream struct {
50+
grpc.ClientStream
51+
monitor *clientReporter
52+
}
53+
54+
func (s *monitoredClientStream) SendMsg(m interface{}) error {
55+
err := s.ClientStream.SendMsg(m)
56+
if err == nil {
57+
s.monitor.SentMessage()
58+
}
59+
return err
60+
}
61+
62+
func (s *monitoredClientStream) RecvMsg(m interface{}) error {
63+
err := s.ClientStream.RecvMsg(m)
64+
if err == nil {
65+
s.monitor.ReceivedMessage()
66+
} else if err == io.EOF {
67+
s.monitor.Handled(codes.OK)
68+
} else {
69+
s.monitor.Handled(grpc.Code(err))
70+
}
71+
return err
72+
}

client_reporter.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2016 Michal Witkowski. All Rights Reserved.
2+
// See LICENSE for licensing terms.
3+
4+
package grpc_prometheus
5+
6+
import (
7+
"time"
8+
9+
"google.golang.org/grpc/codes"
10+
11+
prom "github.com/prometheus/client_golang/prometheus"
12+
)
13+
14+
var (
15+
clientStartedCounter = prom.NewCounterVec(
16+
prom.CounterOpts{
17+
Namespace: "grpc",
18+
Subsystem: "client",
19+
Name: "started_total",
20+
Help: "Total number of RPCs started on the client.",
21+
}, []string{"grpc_type", "grpc_service", "grpc_method"})
22+
23+
clientHandledCounter = prom.NewCounterVec(
24+
prom.CounterOpts{
25+
Namespace: "grpc",
26+
Subsystem: "client",
27+
Name: "handled_total",
28+
Help: "Total number of RPCs completed by the client, regardless of success or failure.",
29+
}, []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"})
30+
31+
clientStreamMsgReceived = prom.NewCounterVec(
32+
prom.CounterOpts{
33+
Namespace: "grpc",
34+
Subsystem: "client",
35+
Name: "msg_received_total",
36+
Help: "Total number of RPC stream messages received by the client.",
37+
}, []string{"grpc_type", "grpc_service", "grpc_method"})
38+
39+
clientStreamMsgSent = prom.NewCounterVec(
40+
prom.CounterOpts{
41+
Namespace: "grpc",
42+
Subsystem: "client",
43+
Name: "msg_sent_total",
44+
Help: "Total number of gRPC stream messages sent by the client.",
45+
}, []string{"grpc_type", "grpc_service", "grpc_method"})
46+
47+
clientHandledHistogramEnabled = false
48+
clientHandledHistogramOpts = prom.HistogramOpts{
49+
Namespace: "grpc",
50+
Subsystem: "client",
51+
Name: "handling_seconds",
52+
Help: "Histogram of response latency (seconds) of the gRPC until it is finished by the application.",
53+
Buckets: prom.DefBuckets,
54+
}
55+
clientHandledHistogram *prom.HistogramVec
56+
)
57+
58+
func init() {
59+
prom.MustRegister(clientStartedCounter)
60+
prom.MustRegister(clientHandledCounter)
61+
prom.MustRegister(clientStreamMsgReceived)
62+
prom.MustRegister(clientStreamMsgSent)
63+
}
64+
65+
// EnableClientHandlingTimeHistogram turns on recording of handling time of RPCs.
66+
// Histogram metrics can be very expensive for Prometheus to retain and query.
67+
func EnableClientHandlingTimeHistogram(opts ...HistogramOption) {
68+
for _, o := range opts {
69+
o(&clientHandledHistogramOpts)
70+
}
71+
if !clientHandledHistogramEnabled {
72+
clientHandledHistogram = prom.NewHistogramVec(
73+
clientHandledHistogramOpts,
74+
[]string{"grpc_type", "grpc_service", "grpc_method"},
75+
)
76+
prom.Register(clientHandledHistogram)
77+
}
78+
clientHandledHistogramEnabled = true
79+
}
80+
81+
type clientReporter struct {
82+
rpcType grpcType
83+
serviceName string
84+
methodName string
85+
startTime time.Time
86+
}
87+
88+
func newClientReporter(rpcType grpcType, fullMethod string) *clientReporter {
89+
r := &clientReporter{rpcType: rpcType}
90+
if clientHandledHistogramEnabled {
91+
r.startTime = time.Now()
92+
}
93+
r.serviceName, r.methodName = splitMethodName(fullMethod)
94+
clientStartedCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
95+
return r
96+
}
97+
98+
func (r *clientReporter) ReceivedMessage() {
99+
clientStreamMsgReceived.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
100+
}
101+
102+
func (r *clientReporter) SentMessage() {
103+
clientStreamMsgSent.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
104+
}
105+
106+
func (r *clientReporter) Handled(code codes.Code) {
107+
clientHandledCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName, code.String()).Inc()
108+
if clientHandledHistogramEnabled {
109+
clientHandledHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Observe(time.Since(r.startTime).Seconds())
110+
}
111+
}

0 commit comments

Comments
 (0)