github.com/weaveworks/common@v0.0.0-20230728070032-dd9e68f319d5/middleware/grpc_stats_test.go (about)

     1  package middleware
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/rand"
     7  	"net"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/golang/protobuf/proto"
    12  	"github.com/prometheus/client_golang/prometheus"
    13  	"github.com/prometheus/client_golang/prometheus/testutil"
    14  	"github.com/stretchr/testify/require"
    15  	"google.golang.org/grpc"
    16  	"google.golang.org/grpc/credentials/insecure"
    17  	"google.golang.org/grpc/health"
    18  	"google.golang.org/grpc/health/grpc_health_v1"
    19  
    20  	"github.com/weaveworks/common/middleware/middleware_test"
    21  )
    22  
    23  func TestGrpcStats(t *testing.T) {
    24  	reg := prometheus.NewRegistry()
    25  
    26  	received := prometheus.NewHistogramVec(prometheus.HistogramOpts{
    27  		Name:    "received_payload_bytes",
    28  		Help:    "Size of received gRPC messages",
    29  		Buckets: BodySizeBuckets,
    30  	}, []string{"method", "route"})
    31  	require.NoError(t, reg.Register(received))
    32  
    33  	sent := prometheus.NewHistogramVec(prometheus.HistogramOpts{
    34  		Name:    "sent_payload_bytes",
    35  		Help:    "Size of sent gRPC",
    36  		Buckets: BodySizeBuckets,
    37  	}, []string{"method", "route"})
    38  	require.NoError(t, reg.Register(sent))
    39  
    40  	inflightRequests := prometheus.NewGaugeVec(prometheus.GaugeOpts{
    41  		Name: "inflight_requests",
    42  		Help: "Current number of inflight requests.",
    43  	}, []string{"method", "route"})
    44  	require.NoError(t, reg.Register(inflightRequests))
    45  
    46  	stats := NewStatsHandler(received, sent, inflightRequests)
    47  
    48  	serv := grpc.NewServer(grpc.StatsHandler(stats), grpc.MaxRecvMsgSize(10e6))
    49  	defer serv.GracefulStop()
    50  
    51  	listener, err := net.Listen("tcp", "localhost:0")
    52  	require.NoError(t, err)
    53  
    54  	go func() {
    55  		require.NoError(t, serv.Serve(listener))
    56  	}()
    57  
    58  	grpc_health_v1.RegisterHealthServer(serv, health.NewServer())
    59  
    60  	closed := false
    61  	conn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
    62  	require.NoError(t, err)
    63  	defer func() {
    64  		if !closed {
    65  			require.NoError(t, conn.Close())
    66  		}
    67  	}()
    68  
    69  	hc := grpc_health_v1.NewHealthClient(conn)
    70  
    71  	// First request (empty).
    72  	resp, err := hc.Check(context.Background(), &grpc_health_v1.HealthCheckRequest{})
    73  	require.NoError(t, err)
    74  	require.Equal(t, grpc_health_v1.HealthCheckResponse_SERVING, resp.Status)
    75  
    76  	// Second request, with large service name. This returns error, which doesn't count as "payload".
    77  	_, err = hc.Check(context.Background(), &grpc_health_v1.HealthCheckRequest{
    78  		Service: generateString(8 * 1024 * 1024),
    79  	})
    80  	require.EqualError(t, err, "rpc error: code = NotFound desc = unknown service")
    81  
    82  	err = testutil.GatherAndCompare(reg, bytes.NewBufferString(`
    83  			# HELP received_payload_bytes Size of received gRPC messages
    84  			# TYPE received_payload_bytes histogram
    85  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="1.048576e+06"} 1
    86  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="2.62144e+06"} 1
    87  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="5.24288e+06"} 1
    88  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="1.048576e+07"} 2
    89  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="2.62144e+07"} 2
    90  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="5.24288e+07"} 2
    91  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="1.048576e+08"} 2
    92  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="2.62144e+08"} 2
    93  			received_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="+Inf"} 2
    94  			received_payload_bytes_sum{method="gRPC", route="/grpc.health.v1.Health/Check"} 8.388623e+06
    95  			received_payload_bytes_count{method="gRPC", route="/grpc.health.v1.Health/Check"} 2
    96  
    97  			# HELP sent_payload_bytes Size of sent gRPC
    98  			# TYPE sent_payload_bytes histogram
    99  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="1.048576e+06"} 1
   100  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="2.62144e+06"} 1
   101  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="5.24288e+06"} 1
   102  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="1.048576e+07"} 1
   103  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="2.62144e+07"} 1
   104  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="5.24288e+07"} 1
   105  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="1.048576e+08"} 1
   106  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="2.62144e+08"} 1
   107  			sent_payload_bytes_bucket{method="gRPC", route="/grpc.health.v1.Health/Check",le="+Inf"} 1
   108  			sent_payload_bytes_sum{method="gRPC", route="/grpc.health.v1.Health/Check"} 7
   109  			sent_payload_bytes_count{method="gRPC", route="/grpc.health.v1.Health/Check"} 1
   110  	`), "received_payload_bytes", "sent_payload_bytes")
   111  	require.NoError(t, err)
   112  
   113  	closed = true
   114  	require.NoError(t, conn.Close())
   115  }
   116  
   117  func TestGrpcStatsStreaming(t *testing.T) {
   118  	reg := prometheus.NewRegistry()
   119  
   120  	received := prometheus.NewHistogramVec(prometheus.HistogramOpts{
   121  		Name:    "received_payload_bytes",
   122  		Help:    "Size of received gRPC messages",
   123  		Buckets: BodySizeBuckets,
   124  	}, []string{"method", "route"})
   125  	require.NoError(t, reg.Register(received))
   126  
   127  	sent := prometheus.NewHistogramVec(prometheus.HistogramOpts{
   128  		Name:    "sent_payload_bytes",
   129  		Help:    "Size of sent gRPC",
   130  		Buckets: BodySizeBuckets,
   131  	}, []string{"method", "route"})
   132  	require.NoError(t, reg.Register(sent))
   133  
   134  	inflightRequests := prometheus.NewGaugeVec(prometheus.GaugeOpts{
   135  		Name: "inflight_requests",
   136  		Help: "Current number of inflight requests.",
   137  	}, []string{"method", "route"})
   138  	require.NoError(t, reg.Register(inflightRequests))
   139  
   140  	stats := NewStatsHandler(received, sent, inflightRequests)
   141  
   142  	serv := grpc.NewServer(grpc.StatsHandler(stats), grpc.MaxSendMsgSize(10e6), grpc.MaxRecvMsgSize(10e6))
   143  	defer serv.GracefulStop()
   144  
   145  	listener, err := net.Listen("tcp", "localhost:0")
   146  	require.NoError(t, err)
   147  
   148  	go func() {
   149  		require.NoError(t, serv.Serve(listener))
   150  	}()
   151  
   152  	middleware_test.RegisterEchoServerServer(serv, &halfEcho{log: t.Log})
   153  
   154  	conn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(10e6), grpc.MaxCallSendMsgSize(10e6)))
   155  	require.NoError(t, err)
   156  	defer func() {
   157  		require.NoError(t, conn.Close())
   158  	}()
   159  
   160  	fc := middleware_test.NewEchoServerClient(conn)
   161  
   162  	s, err := fc.Process(context.Background())
   163  	require.NoError(t, err)
   164  
   165  	for ix := 0; ix < 5; ix++ {
   166  		msg := &middleware_test.Msg{
   167  			Body: []byte(generateString((ix + 1) * 1024 * 1024)),
   168  		}
   169  
   170  		t.Log("Client Sending", proto.Size(msg))
   171  		err = s.Send(msg)
   172  		require.NoError(t, err)
   173  
   174  		_, err := s.Recv()
   175  		require.NoError(t, err)
   176  
   177  		err = testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   178  			# HELP inflight_requests Current number of inflight requests.
   179  			# TYPE inflight_requests gauge
   180  			inflight_requests{method="gRPC", route="/middleware.EchoServer/Process"} 1
   181  		`), "inflight_requests")
   182  		require.NoError(t, err)
   183  	}
   184  	require.NoError(t, s.CloseSend())
   185  
   186  	// Wait for inflight_requests to go to 0.
   187  	timeout := 1 * time.Second
   188  	sleep := timeout / 10
   189  
   190  	for endTime := time.Now().Add(timeout); time.Now().Before(endTime); {
   191  		err = testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   192  			# HELP inflight_requests Current number of inflight requests.
   193  			# TYPE inflight_requests gauge
   194  			inflight_requests{method="gRPC", route="/middleware.EchoServer/Process"} 0
   195  		`), "inflight_requests")
   196  		if err == nil {
   197  			break
   198  		}
   199  		time.Sleep(sleep)
   200  	}
   201  	require.NoError(t, err)
   202  
   203  	err = testutil.GatherAndCompare(reg, bytes.NewBufferString(`
   204  			# HELP received_payload_bytes Size of received gRPC messages
   205  			# TYPE received_payload_bytes histogram
   206  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="1.048576e+06"} 0
   207  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="2.62144e+06"} 2
   208  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="5.24288e+06"} 4
   209  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="1.048576e+07"} 5
   210  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="2.62144e+07"} 5
   211  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="5.24288e+07"} 5
   212  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="1.048576e+08"} 5
   213  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="2.62144e+08"} 5
   214  			received_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="+Inf"} 5
   215  			received_payload_bytes_sum{method="gRPC",route="/middleware.EchoServer/Process"} 1.5728689e+07
   216  			received_payload_bytes_count{method="gRPC",route="/middleware.EchoServer/Process"} 5
   217  
   218  			# HELP sent_payload_bytes Size of sent gRPC
   219  			# TYPE sent_payload_bytes histogram
   220  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="1.048576e+06"} 1
   221  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="2.62144e+06"} 4
   222  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="5.24288e+06"} 5
   223  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="1.048576e+07"} 5
   224  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="2.62144e+07"} 5
   225  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="5.24288e+07"} 5
   226  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="1.048576e+08"} 5
   227  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="2.62144e+08"} 5
   228  			sent_payload_bytes_bucket{method="gRPC",route="/middleware.EchoServer/Process",le="+Inf"} 5
   229  			sent_payload_bytes_sum{method="gRPC",route="/middleware.EchoServer/Process"} 7.864367e+06
   230  			sent_payload_bytes_count{method="gRPC",route="/middleware.EchoServer/Process"} 5
   231  	`), "received_payload_bytes", "sent_payload_bytes")
   232  
   233  	require.NoError(t, err)
   234  }
   235  
   236  type halfEcho struct {
   237  	log func(args ...interface{})
   238  }
   239  
   240  func (f halfEcho) Process(server middleware_test.EchoServer_ProcessServer) error {
   241  	for {
   242  		msg, err := server.Recv()
   243  		if err != nil {
   244  			return err
   245  		}
   246  
   247  		// Half the body
   248  		msg.Body = msg.Body[:len(msg.Body)/2]
   249  
   250  		f.log("Server Sending", proto.Size(msg))
   251  		err = server.Send(msg)
   252  		if err != nil {
   253  			return err
   254  		}
   255  	}
   256  }
   257  
   258  func generateString(size int) string {
   259  	// Use random bytes, to avoid compression.
   260  	buf := make([]byte, size)
   261  	_, err := rand.Read(buf)
   262  	if err != nil {
   263  		// Should not happen.
   264  		panic(err)
   265  	}
   266  
   267  	// To avoid invalid UTF-8 sequences (which protobuf complains about), we cleanup the data a bit.
   268  	for ix, b := range buf {
   269  		if b < ' ' {
   270  			b += ' '
   271  		}
   272  		b = b & 0x7f
   273  		buf[ix] = b
   274  	}
   275  	return string(buf)
   276  }