diff --git a/examples/features/stats_monitoring/README.md b/examples/features/stats_monitoring/README.md new file mode 100644 index 00000000000..079b6b4f1ee --- /dev/null +++ b/examples/features/stats_monitoring/README.md @@ -0,0 +1,58 @@ +# Stats Monitoring Handler + +This example demonstrates the use of the [`stats`](https://pkg.go.dev/google.golang.org/grpc/stats) package for reporting various +network and RPC stats. +_Note that all fields are READ-ONLY and the APIs of the `stats` package are +experimental_. + +## Try it + +``` +go run server/main.go +``` + +``` +go run client/main.go +``` + +## Explanation + +gRPC provides a mechanism to hook on to various events (phases) of the +request-response network cycle through the [`stats.Handler`](https://pkg.go.dev/google.golang.org/grpc/stats#Handler) interface. To access +these events, a concrete type that implements `stats.Handler` should be passed to +`grpc.WithStatsHandler()` on the client side and `grpc.StatsHandler()` on the +server side. + +The `HandleRPC(context.Context, RPCStats)` method on `stats.Handler` is called +multiple times during a request-response cycle, and various event stats are +passed to its `RPCStats` parameter (an interface). The concrete types that +implement this interface are: `*stats.Begin`, `*stats.InHeader`, `*stats.InPayload`, +`*stats.InTrailer`, `*stats.OutHeader`, `*stats.OutPayload`, `*stats.OutTrailer`, and +`*stats.End`. The order of these events differs on client and server. + +Similarly, the `HandleConn(context.Context, ConnStats)` method on `stats.Handler` +is called twice, once at the beginning of the connection with `*stats.ConnBegin` +and once at the end with `*stats.ConnEnd`. + +The [`stats.Handler`](https://pkg.go.dev/google.golang.org/grpc/stats#Handler) interface also provides +`TagRPC(context.Context, *RPCTagInfo) context.Context` and +`TagConn(context.Context, *ConnTagInfo) context.Context` methods. These methods +are mainly used to attach network related information to the given context. + +The `TagRPC(context.Context, *RPCTagInfo) context.Context` method returns a +context from which the context used for the rest lifetime of the RPC will be +derived. This behavior is consistent between the gRPC client and server. + +The context returned from +`TagConn(context.Context, *ConnTagInfo) context.Context` has varied lifespan: + +- In the gRPC client: + The context used for the rest lifetime of the RPC will NOT be derived from + this context. Hence the information attached to this context can only be + consumed by `HandleConn(context.Context, ConnStats)` method. +- In the gRPC server: + The context used for the rest lifetime of the RPC will be derived from + this context. + +NOTE: The [stats](https://pkg.go.dev/google.golang.org/grpc/stats) package should only be used for network monitoring purposes, +and not as an alternative to [interceptors](https://github.com/grpc/grpc-go/blob/master/examples/features/metadata). diff --git a/examples/features/stats_monitoring/client/main.go b/examples/features/stats_monitoring/client/main.go new file mode 100644 index 00000000000..0fb820d11c6 --- /dev/null +++ b/examples/features/stats_monitoring/client/main.go @@ -0,0 +1,60 @@ +/* + * + * Copyright 2022 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. + * + */ + +// Binary client is an example client to illustrate the use of the stats handler. +package main + +import ( + "context" + "flag" + "log" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + echogrpc "google.golang.org/grpc/examples/features/proto/echo" + echopb "google.golang.org/grpc/examples/features/proto/echo" + "google.golang.org/grpc/examples/features/stats_monitoring/statshandler" +) + +var addr = flag.String("addr", "localhost:50051", "the address to connect to") + +func main() { + flag.Parse() + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(statshandler.New()), + } + conn, err := grpc.Dial(*addr, opts...) + if err != nil { + log.Fatalf("failed to connect to server %q: %v", *addr, err) + } + defer conn.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + c := echogrpc.NewEchoClient(conn) + + resp, err := c.UnaryEcho(ctx, &echopb.EchoRequest{Message: "stats handler demo"}) + if err != nil { + log.Fatalf("unexpected error from UnaryEcho: %v", err) + } + log.Printf("RPC response: %s", resp.Message) +} diff --git a/examples/features/stats_monitoring/server/main.go b/examples/features/stats_monitoring/server/main.go new file mode 100644 index 00000000000..a460522c29d --- /dev/null +++ b/examples/features/stats_monitoring/server/main.go @@ -0,0 +1,62 @@ +/* + * + * Copyright 2022 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. + * + */ + +// Binary server is an example server to illustrate the use of the stats handler. +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net" + "time" + + "google.golang.org/grpc" + + echogrpc "google.golang.org/grpc/examples/features/proto/echo" + echopb "google.golang.org/grpc/examples/features/proto/echo" + "google.golang.org/grpc/examples/features/stats_monitoring/statshandler" +) + +var port = flag.Int("port", 50051, "the port to serve on") + +type server struct { + echogrpc.UnimplementedEchoServer +} + +func (s *server) UnaryEcho(ctx context.Context, req *echopb.EchoRequest) (*echopb.EchoResponse, error) { + time.Sleep(2 * time.Second) + return &echopb.EchoResponse{Message: req.Message}, nil +} + +func main() { + flag.Parse() + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("failed to listen on port %d: %v", *port, err) + } + log.Printf("server listening at %v\n", lis.Addr()) + + s := grpc.NewServer(grpc.StatsHandler(statshandler.New())) + echogrpc.RegisterEchoServer(s, &server{}) + + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/examples/features/stats_monitoring/statshandler/handler.go b/examples/features/stats_monitoring/statshandler/handler.go new file mode 100644 index 00000000000..85688b8c385 --- /dev/null +++ b/examples/features/stats_monitoring/statshandler/handler.go @@ -0,0 +1,93 @@ +/* + * + * Copyright 2022 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 statshandler is an example pkg to illustrate the use of the stats handler. +package statshandler + +import ( + "context" + "log" + "net" + "path/filepath" + + "google.golang.org/grpc/stats" +) + +// Handler implements [stats.Handler](https://pkg.go.dev/google.golang.org/grpc/stats#Handler) interface. +type Handler struct{} + +type connStatCtxKey struct{} + +// TagConn can attach some information to the given context. +// The context used in HandleConn for this connection will be derived from the context returned. +// In the gRPC client: +// The context used in HandleRPC for RPCs on this connection will be the user's context and NOT derived from the context returned here. +// In the gRPC server: +// The context used in HandleRPC for RPCs on this connection will be derived from the context returned here. +func (st *Handler) TagConn(ctx context.Context, stat *stats.ConnTagInfo) context.Context { + log.Printf("[TagConn] [%T]: %+[1]v", stat) + return context.WithValue(ctx, connStatCtxKey{}, stat) +} + +// HandleConn processes the Conn stats. +func (st *Handler) HandleConn(ctx context.Context, stat stats.ConnStats) { + var rAddr net.Addr + if s, ok := ctx.Value(connStatCtxKey{}).(*stats.ConnTagInfo); ok { + rAddr = s.RemoteAddr + } + + if stat.IsClient() { + log.Printf("[server addr: %s] [HandleConn] [%T]: %+[2]v", rAddr, stat) + } else { + log.Printf("[client addr: %s] [HandleConn] [%T]: %+[2]v", rAddr, stat) + } +} + +type rpcStatCtxKey struct{} + +// TagRPC can attach some information to the given context. +// The context used for the rest lifetime of the RPC will be derived from the returned context. +func (st *Handler) TagRPC(ctx context.Context, stat *stats.RPCTagInfo) context.Context { + log.Printf("[TagRPC] [%T]: %+[1]v", stat) + return context.WithValue(ctx, rpcStatCtxKey{}, stat) +} + +// HandleRPC processes the RPC stats. Note: All stat fields are read-only. +func (st *Handler) HandleRPC(ctx context.Context, stat stats.RPCStats) { + var sMethod string + if s, ok := ctx.Value(rpcStatCtxKey{}).(*stats.RPCTagInfo); ok { + sMethod = filepath.Base(s.FullMethodName) + } + + var cAddr net.Addr + // for gRPC clients, key connStatCtxKey{} will not be present in HandleRPC's context. + if s, ok := ctx.Value(connStatCtxKey{}).(*stats.ConnTagInfo); ok { + cAddr = s.RemoteAddr + } + + if stat.IsClient() { + log.Printf("[server method: %s] [HandleRPC] [%T]: %+[2]v", sMethod, stat) + } else { + log.Printf("[client addr: %s] [HandleRPC] [%T]: %+[2]v", cAddr, stat) + } +} + +// New returns a new implementation of [stats.Handler](https://pkg.go.dev/google.golang.org/grpc/stats#Handler) interface. +func New() *Handler { + return &Handler{} +}