go.uber.org/yarpc@v1.72.1/encoding/protobuf/observability_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package protobuf_test
    22  
    23  import (
    24  	"context"
    25  	"net"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/gogo/protobuf/proto"
    30  	"github.com/gogo/protobuf/types"
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/stretchr/testify/require"
    33  	"go.uber.org/net/metrics"
    34  	"go.uber.org/yarpc"
    35  	"go.uber.org/yarpc/api/transport"
    36  	"go.uber.org/yarpc/encoding/protobuf"
    37  	"go.uber.org/yarpc/encoding/protobuf/internal/testpb"
    38  	"go.uber.org/yarpc/internal/testutils"
    39  	"go.uber.org/yarpc/transport/grpc"
    40  	"go.uber.org/yarpc/yarpcerrors"
    41  	"go.uber.org/zap"
    42  	"go.uber.org/zap/zapcore"
    43  	"go.uber.org/zap/zaptest/observer"
    44  )
    45  
    46  const (
    47  	_clientName = "caller"
    48  	_serverName = "callee"
    49  
    50  	// from observability middleware
    51  	_errorInbound  = "Error handling inbound request."
    52  	_errorOutbound = "Error making outbound call."
    53  )
    54  
    55  func TestProtobufErrorDetailObservability(t *testing.T) {
    56  	client, observedLogs, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t)
    57  	defer cleanup()
    58  
    59  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    60  	defer cancel()
    61  
    62  	_, err := client.Unary(ctx, &testpb.TestMessage{})
    63  	require.Error(t, err, "expected call error")
    64  
    65  	require.NotEmpty(t, protobuf.GetErrorDetails(err),
    66  		"no error details, found error of type '%T': %v", err, err)
    67  
    68  	t.Run("logs", func(t *testing.T) {
    69  		wantFields := []zapcore.Field{
    70  			zap.String("errorCode", "invalid-argument"),
    71  			zap.String("errorName", "StringValue"),
    72  			zap.String("errorDetails", "[]{ StringValue{value:\"string value\" } , Int32Value{value:100 } }"),
    73  		}
    74  		assertLogs(t, wantFields, observedLogs.TakeAll())
    75  	})
    76  
    77  	t.Run("metrics", func(t *testing.T) {
    78  		wantCounters := []testutils.CounterAssertion{
    79  			{
    80  				Name: "caller_failures",
    81  				Tags: map[string]string{
    82  					"error":      "invalid-argument",
    83  					"error_name": "StringValue",
    84  				},
    85  				Value: 1,
    86  			},
    87  			{Name: "calls", Value: 1},
    88  			{Name: "panics"},
    89  			{Name: "successes"},
    90  		}
    91  
    92  		testutils.AssertCounters(t, wantCounters, clientMetricsRoot.Snapshot().Counters)
    93  		testutils.AssertCounters(t, wantCounters, serverMetricsRoot.Snapshot().Counters)
    94  	})
    95  }
    96  
    97  func TestProtobufMetrics(t *testing.T) {
    98  	client, _, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t)
    99  	defer cleanup()
   100  
   101  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   102  	defer cancel()
   103  
   104  	_, err := client.Unary(ctx, &testpb.TestMessage{Value: "success"})
   105  	require.NoError(t, err, "unexpected call error")
   106  
   107  	t.Run("counters", func(t *testing.T) {
   108  		wantCounters := []testutils.CounterAssertion{
   109  			{Name: "calls", Value: 1},
   110  			{Name: "panics"},
   111  			{Name: "successes", Value: 1},
   112  		}
   113  
   114  		testutils.AssertCounters(t, wantCounters, clientMetricsRoot.Snapshot().Counters)
   115  		testutils.AssertCounters(t, wantCounters, serverMetricsRoot.Snapshot().Counters)
   116  	})
   117  	t.Run("inbound histograms", func(t *testing.T) {
   118  		wantHistograms := []testutils.HistogramAssertion{
   119  			{Name: "caller_failure_latency_ms"},
   120  			{Name: "request_payload_size_bytes", Value: []int64{16}},
   121  			{Name: "response_payload_size_bytes", Value: []int64{16}},
   122  			{Name: "server_failure_latency_ms"},
   123  			{Name: "success_latency_ms", IgnoreValueCompare: true, ValueLength: 1},
   124  			{Name: "timeout_ttl_ms"},
   125  			{Name: "ttl_ms", Value: []int64{1000}},
   126  		}
   127  		testutils.AssertClientAndServerHistograms(t, wantHistograms, clientMetricsRoot, serverMetricsRoot)
   128  	})
   129  }
   130  
   131  func TestProtobufStreamMetrics(t *testing.T) {
   132  	client, _, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t)
   133  	defer cleanup()
   134  
   135  	ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
   136  	defer cancel()
   137  
   138  	stream, err := client.Duplex(ctx)
   139  	require.NoError(t, err, "unexpected error on stream creation")
   140  
   141  	msg := &testpb.TestMessage{Value: "echo"}
   142  
   143  	err = stream.Send(msg)
   144  	require.NoError(t, err, "unexpected error on stream send")
   145  
   146  	reply, err := stream.Recv()
   147  	require.NoError(t, err, "unexpected error on stream receive")
   148  	assert.Equal(t, msg, reply)
   149  
   150  	stream.CloseSend()
   151  
   152  	_, err = stream.Recv()
   153  	assert.Error(t, err)
   154  
   155  	t.Run("counters", func(t *testing.T) {
   156  		clientCounters := []testutils.CounterAssertion{
   157  			{Name: "calls", Value: 1},
   158  			{Name: "panics"},
   159  			{Name: "stream_receive_failures", Value: 1},
   160  			{Name: "stream_receive_successes", Value: 1},
   161  			{Name: "stream_receives", Value: 2},
   162  			{Name: "stream_send_successes", Value: 1},
   163  			{Name: "stream_sends", Value: 1},
   164  			{Name: "successes", Value: 1},
   165  		}
   166  
   167  		serverCounters := []testutils.CounterAssertion{
   168  			{Name: "calls", Value: 1},
   169  			{Name: "panics"},
   170  			{Name: "server_failures", Value: 1},
   171  			{Name: "stream_receive_successes", Value: 2},
   172  			{Name: "stream_receives", Value: 2},
   173  			{Name: "stream_send_successes", Value: 1},
   174  			{Name: "stream_sends", Value: 1},
   175  			{Name: "successes", Value: 1},
   176  		}
   177  
   178  		testutils.AssertCounters(t, clientCounters, clientMetricsRoot.Snapshot().Counters)
   179  		testutils.AssertCounters(t, serverCounters, serverMetricsRoot.Snapshot().Counters)
   180  	})
   181  	t.Run("inbound histograms", func(t *testing.T) {
   182  		wantHistograms := []testutils.HistogramAssertion{
   183  			{Name: "stream_duration_ms", IgnoreValueCompare: true, ValueLength: 1},
   184  			{Name: "stream_request_payload_size_bytes", Value: []int64{8}},
   185  			{Name: "stream_response_payload_size_bytes", Value: []int64{8}},
   186  		}
   187  		testutils.AssertHistograms(t, wantHistograms, serverMetricsRoot.Snapshot().Histograms)
   188  	})
   189  }
   190  
   191  func assertLogs(t *testing.T, wantFields []zapcore.Field, logs []observer.LoggedEntry) {
   192  	require.Len(t, logs, 2, "unexpected number of logs")
   193  
   194  	t.Run("inbound", func(t *testing.T) {
   195  		require.Equal(t, _errorInbound, logs[0].Message, "unexpected log")
   196  		assertLogFields(t, wantFields, logs[0].Context)
   197  	})
   198  
   199  	t.Run("outbound", func(t *testing.T) {
   200  		require.Equal(t, _errorOutbound, logs[1].Message, "unexpected log")
   201  		assertLogFields(t, wantFields, logs[1].Context)
   202  	})
   203  }
   204  
   205  func assertLogFields(t *testing.T, wantFields, gotContext []zapcore.Field) {
   206  	gotFields := make(map[string]zapcore.Field)
   207  	for _, log := range gotContext {
   208  		gotFields[log.Key] = log
   209  	}
   210  
   211  	for _, want := range wantFields {
   212  		got, ok := gotFields[want.Key]
   213  		if assert.True(t, ok, "key %q not found", want.Key) {
   214  			assert.Equal(t, want, got, "unexpected log field")
   215  		}
   216  	}
   217  }
   218  
   219  func initClientAndServer(t *testing.T) (
   220  	client testpb.TestYARPCClient,
   221  	observedLogs *observer.ObservedLogs,
   222  	clientMetricsRoot *metrics.Root,
   223  	serverMetricsRoot *metrics.Root,
   224  	cleanup func(),
   225  ) {
   226  	loggerCore, observedLogs := observer.New(zapcore.DebugLevel)
   227  	clientMetricsRoot, serverMetricsRoot = metrics.New(), metrics.New()
   228  
   229  	serverAddr, cleanupServer := newServer(t, loggerCore, serverMetricsRoot)
   230  	client, cleanupClient := newClient(t, serverAddr, loggerCore, clientMetricsRoot)
   231  
   232  	_ = observedLogs.TakeAll() // ignore all start up logs
   233  
   234  	return client, observedLogs, clientMetricsRoot, serverMetricsRoot, func() {
   235  		cleanupServer()
   236  		cleanupClient()
   237  	}
   238  }
   239  
   240  func newServer(t *testing.T, loggerCore zapcore.Core, metricsRoot *metrics.Root) (addr string, cleanup func()) {
   241  	listener, err := net.Listen("tcp", "127.0.0.1:0")
   242  	require.NoError(t, err)
   243  
   244  	inbound := grpc.NewTransport().NewInbound(listener)
   245  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
   246  		Name:     _serverName,
   247  		Inbounds: yarpc.Inbounds{inbound},
   248  		Logging:  yarpc.LoggingConfig{Zap: zap.New(loggerCore)},
   249  		Metrics:  yarpc.MetricsConfig{Metrics: metricsRoot.Scope()},
   250  	})
   251  
   252  	dispatcher.Register(testpb.BuildTestYARPCProcedures(&observabilityTestServer{}))
   253  	require.NoError(t, dispatcher.Start(), "could not start server dispatcher")
   254  
   255  	addr = inbound.Addr().String()
   256  	cleanup = func() { assert.NoError(t, dispatcher.Stop(), "could not stop dispatcher") }
   257  	return addr, cleanup
   258  }
   259  
   260  func newClient(t *testing.T, serverAddr string, loggerCore zapcore.Core, metricsRoot *metrics.Root) (client testpb.TestYARPCClient, cleanup func()) {
   261  	outbound := grpc.NewTransport().NewSingleOutbound(serverAddr)
   262  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
   263  		Name: _clientName,
   264  		Outbounds: map[string]transport.Outbounds{
   265  			_serverName: {
   266  				ServiceName: _serverName,
   267  				Unary:       outbound,
   268  				Stream:      outbound,
   269  			},
   270  		},
   271  		Logging: yarpc.LoggingConfig{Zap: zap.New(loggerCore)},
   272  		Metrics: yarpc.MetricsConfig{Metrics: metricsRoot.Scope()},
   273  	})
   274  
   275  	client = testpb.NewTestYARPCClient(dispatcher.ClientConfig(_serverName))
   276  	require.NoError(t, dispatcher.Start(), "could not start client dispatcher")
   277  
   278  	cleanup = func() { assert.NoError(t, dispatcher.Stop(), "could not stop dispatcher") }
   279  	return client, cleanup
   280  }
   281  
   282  type observabilityTestServer struct{}
   283  
   284  func (observabilityTestServer) Unary(ctx context.Context, msg *testpb.TestMessage) (*testpb.TestMessage, error) {
   285  	if msg.Value == "success" {
   286  		return &testpb.TestMessage{Value: msg.Value}, nil
   287  	}
   288  	details := []proto.Message{
   289  		&types.StringValue{Value: "string value"},
   290  		&types.Int32Value{Value: 100},
   291  	}
   292  	return nil, protobuf.NewError(yarpcerrors.CodeInvalidArgument, "my message", protobuf.WithErrorDetails(details...))
   293  }
   294  
   295  func (observabilityTestServer) Duplex(stream testpb.TestServiceDuplexYARPCServer) error {
   296  	for {
   297  		msg, err := stream.Recv()
   298  		if err != nil {
   299  			return err
   300  		}
   301  		err = stream.Send(msg)
   302  		if err != nil {
   303  			return err
   304  		}
   305  	}
   306  }