go.uber.org/yarpc@v1.72.1/encoding/protobuf/v2/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 v2_test
    22  
    23  import (
    24  	"context"
    25  	"net"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/golang/protobuf/ptypes/wrappers"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  	"go.uber.org/net/metrics"
    33  	"go.uber.org/yarpc"
    34  	"go.uber.org/yarpc/api/transport"
    35  	"go.uber.org/yarpc/encoding/protobuf/internal/testpb/v2"
    36  	"go.uber.org/yarpc/encoding/protobuf/v2"
    37  	"go.uber.org/yarpc/internal/testutils"
    38  	"go.uber.org/yarpc/transport/grpc"
    39  	"go.uber.org/yarpc/yarpcerrors"
    40  	"go.uber.org/zap"
    41  	"go.uber.org/zap/zapcore"
    42  	"go.uber.org/zap/zaptest/observer"
    43  	"google.golang.org/protobuf/proto"
    44  )
    45  
    46  const (
    47  	// from observability middleware
    48  	_errorInbound  = "Error handling inbound request."
    49  	_errorOutbound = "Error making outbound call."
    50  )
    51  
    52  func TestProtobufErrorDetailObservability(t *testing.T) {
    53  	client, observedLogs, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t)
    54  	defer cleanup()
    55  
    56  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    57  	defer cancel()
    58  
    59  	_, err := client.Unary(ctx, &testpb.TestMessage{})
    60  	require.Error(t, err, "expected call error")
    61  
    62  	require.NotEmpty(t, v2.GetErrorDetails(err),
    63  		"no error details, found error of type '%T': %v", err, err)
    64  
    65  	t.Run("logs", func(t *testing.T) {
    66  		wantFields := []zapcore.Field{
    67  			zap.String("errorCode", "invalid-argument"),
    68  			zap.String("errorName", "StringValue"),
    69  			zap.String("errorDetails", "[]{ StringValue{value:\"string value\"} , Int32Value{value:100} }"),
    70  		}
    71  		assertLogs(t, wantFields, observedLogs.TakeAll())
    72  	})
    73  
    74  	t.Run("metrics", func(t *testing.T) {
    75  		wantCounters := []testutils.CounterAssertion{
    76  			{
    77  				Name: "caller_failures",
    78  				Tags: map[string]string{
    79  					"error":      "invalid-argument",
    80  					"error_name": "StringValue",
    81  				},
    82  				Value: 1,
    83  			},
    84  			{Name: "calls", Value: 1},
    85  			{Name: "panics"},
    86  			{Name: "successes"},
    87  		}
    88  
    89  		testutils.AssertCounters(t, wantCounters, clientMetricsRoot.Snapshot().Counters)
    90  		testutils.AssertCounters(t, wantCounters, serverMetricsRoot.Snapshot().Counters)
    91  	})
    92  }
    93  
    94  func TestProtobufMetrics(t *testing.T) {
    95  	client, _, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t)
    96  	defer cleanup()
    97  
    98  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    99  	defer cancel()
   100  
   101  	_, err := client.Unary(ctx, &testpb.TestMessage{Value: "success"})
   102  	require.NoError(t, err, "unexpected call error")
   103  
   104  	t.Run("counters", func(t *testing.T) {
   105  		wantCounters := []testutils.CounterAssertion{
   106  			{Name: "calls", Value: 1},
   107  			{Name: "panics"},
   108  			{Name: "successes", Value: 1},
   109  		}
   110  
   111  		testutils.AssertCounters(t, wantCounters, clientMetricsRoot.Snapshot().Counters)
   112  		testutils.AssertCounters(t, wantCounters, serverMetricsRoot.Snapshot().Counters)
   113  	})
   114  	t.Run("inbound histograms", func(t *testing.T) {
   115  		wantHistograms := []testutils.HistogramAssertion{
   116  			{Name: "caller_failure_latency_ms"},
   117  			{Name: "request_payload_size_bytes", Value: []int64{16}},
   118  			{Name: "response_payload_size_bytes", Value: []int64{16}},
   119  			{Name: "server_failure_latency_ms"},
   120  			{Name: "success_latency_ms", IgnoreValueCompare: true, ValueLength: 1},
   121  			{Name: "timeout_ttl_ms"},
   122  			{Name: "ttl_ms", Value: []int64{1000}},
   123  		}
   124  		testutils.AssertClientAndServerHistograms(t, wantHistograms, clientMetricsRoot, serverMetricsRoot)
   125  	})
   126  }
   127  
   128  func TestProtobufStreamMetrics(t *testing.T) {
   129  	client, _, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t)
   130  	defer cleanup()
   131  
   132  	ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
   133  	defer cancel()
   134  
   135  	stream, err := client.Duplex(ctx)
   136  	require.NoError(t, err, "unexpected error on stream creation")
   137  
   138  	msg := &testpb.TestMessage{Value: "echo"}
   139  
   140  	err = stream.Send(msg)
   141  	require.NoError(t, err, "unexpected error on stream send")
   142  
   143  	reply, err := stream.Recv()
   144  	require.NoError(t, err, "unexpected error on stream receive")
   145  	assert.True(t, proto.Equal(msg, reply))
   146  
   147  	stream.CloseSend()
   148  
   149  	_, err = stream.Recv()
   150  	assert.Error(t, err)
   151  
   152  	t.Run("counters", func(t *testing.T) {
   153  		clientCounters := []testutils.CounterAssertion{
   154  			{Name: "calls", Value: 1},
   155  			{Name: "panics"},
   156  			{Name: "stream_receive_failures", Value: 1},
   157  			{Name: "stream_receive_successes", Value: 1},
   158  			{Name: "stream_receives", Value: 2},
   159  			{Name: "stream_send_successes", Value: 1},
   160  			{Name: "stream_sends", Value: 1},
   161  			{Name: "successes", Value: 1},
   162  		}
   163  
   164  		serverCounters := []testutils.CounterAssertion{
   165  			{Name: "calls", Value: 1},
   166  			{Name: "panics"},
   167  			{Name: "server_failures", Value: 1},
   168  			{Name: "stream_receive_successes", Value: 2},
   169  			{Name: "stream_receives", Value: 2},
   170  			{Name: "stream_send_successes", Value: 1},
   171  			{Name: "stream_sends", Value: 1},
   172  			{Name: "successes", Value: 1},
   173  		}
   174  
   175  		testutils.AssertCounters(t, clientCounters, clientMetricsRoot.Snapshot().Counters)
   176  		testutils.AssertCounters(t, serverCounters, serverMetricsRoot.Snapshot().Counters)
   177  	})
   178  	t.Run("inbound histograms", func(t *testing.T) {
   179  		wantHistograms := []testutils.HistogramAssertion{
   180  			{Name: "stream_duration_ms", IgnoreValueCompare: true, ValueLength: 1},
   181  			{Name: "stream_request_payload_size_bytes", Value: []int64{8}},
   182  			{Name: "stream_response_payload_size_bytes", Value: []int64{8}},
   183  		}
   184  		testutils.AssertHistograms(t, wantHistograms, serverMetricsRoot.Snapshot().Histograms)
   185  	})
   186  }
   187  
   188  func assertLogs(t *testing.T, wantFields []zapcore.Field, logs []observer.LoggedEntry) {
   189  	require.Len(t, logs, 2, "unexpected number of logs")
   190  
   191  	t.Run("inbound", func(t *testing.T) {
   192  		require.Equal(t, _errorInbound, logs[0].Message, "unexpected log")
   193  		assertLogFields(t, wantFields, logs[0].Context)
   194  	})
   195  
   196  	t.Run("outbound", func(t *testing.T) {
   197  		require.Equal(t, _errorOutbound, logs[1].Message, "unexpected log")
   198  		assertLogFields(t, wantFields, logs[1].Context)
   199  	})
   200  }
   201  
   202  func assertLogFields(t *testing.T, wantFields, gotContext []zapcore.Field) {
   203  	gotFields := make(map[string]zapcore.Field)
   204  	for _, log := range gotContext {
   205  		gotFields[log.Key] = log
   206  	}
   207  
   208  	for _, want := range wantFields {
   209  		got, ok := gotFields[want.Key]
   210  		if assert.True(t, ok, "key %q not found", want.Key) {
   211  			assert.Equal(t, want, got, "unexpected log field")
   212  		}
   213  	}
   214  }
   215  
   216  func initClientAndServer(t *testing.T) (
   217  	client testpb.TestYARPCClient,
   218  	observedLogs *observer.ObservedLogs,
   219  	clientMetricsRoot *metrics.Root,
   220  	serverMetricsRoot *metrics.Root,
   221  	cleanup func(),
   222  ) {
   223  	loggerCore, observedLogs := observer.New(zapcore.DebugLevel)
   224  	clientMetricsRoot, serverMetricsRoot = metrics.New(), metrics.New()
   225  
   226  	serverAddr, cleanupServer := newServer(t, loggerCore, serverMetricsRoot)
   227  	client, cleanupClient := newClient(t, serverAddr, loggerCore, clientMetricsRoot)
   228  
   229  	_ = observedLogs.TakeAll() // ignore all start up logs
   230  
   231  	return client, observedLogs, clientMetricsRoot, serverMetricsRoot, func() {
   232  		cleanupServer()
   233  		cleanupClient()
   234  	}
   235  }
   236  
   237  func newServer(t *testing.T, loggerCore zapcore.Core, metricsRoot *metrics.Root) (addr string, cleanup func()) {
   238  	listener, err := net.Listen("tcp", "127.0.0.1:0")
   239  	require.NoError(t, err)
   240  
   241  	inbound := grpc.NewTransport().NewInbound(listener)
   242  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
   243  		Name:     _serverName,
   244  		Inbounds: yarpc.Inbounds{inbound},
   245  		Logging:  yarpc.LoggingConfig{Zap: zap.New(loggerCore)},
   246  		Metrics:  yarpc.MetricsConfig{Metrics: metricsRoot.Scope()},
   247  	})
   248  
   249  	dispatcher.Register(testpb.BuildTestYARPCProcedures(&observabilityTestServer{}))
   250  	require.NoError(t, dispatcher.Start(), "could not start server dispatcher")
   251  
   252  	addr = inbound.Addr().String()
   253  	cleanup = func() { assert.NoError(t, dispatcher.Stop(), "could not stop dispatcher") }
   254  	return addr, cleanup
   255  }
   256  
   257  func newClient(t *testing.T, serverAddr string, loggerCore zapcore.Core, metricsRoot *metrics.Root) (client testpb.TestYARPCClient, cleanup func()) {
   258  	outbound := grpc.NewTransport().NewSingleOutbound(serverAddr)
   259  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
   260  		Name: _clientName,
   261  		Outbounds: map[string]transport.Outbounds{
   262  			_serverName: {
   263  				ServiceName: _serverName,
   264  				Unary:       outbound,
   265  				Stream:      outbound,
   266  			},
   267  		},
   268  		Logging: yarpc.LoggingConfig{Zap: zap.New(loggerCore)},
   269  		Metrics: yarpc.MetricsConfig{Metrics: metricsRoot.Scope()},
   270  	})
   271  
   272  	client = testpb.NewTestYARPCClient(dispatcher.ClientConfig(_serverName))
   273  	require.NoError(t, dispatcher.Start(), "could not start client dispatcher")
   274  
   275  	cleanup = func() { assert.NoError(t, dispatcher.Stop(), "could not stop dispatcher") }
   276  	return client, cleanup
   277  }
   278  
   279  type observabilityTestServer struct{}
   280  
   281  func (observabilityTestServer) Unary(ctx context.Context, msg *testpb.TestMessage) (*testpb.TestMessage, error) {
   282  	if msg.Value == "success" {
   283  		return &testpb.TestMessage{Value: msg.Value}, nil
   284  	}
   285  	details := []proto.Message{
   286  		&wrappers.StringValue{Value: "string value"},
   287  		&wrappers.Int32Value{Value: 100},
   288  	}
   289  	return nil, v2.NewError(yarpcerrors.CodeInvalidArgument, "my message", v2.WithErrorDetails(details...))
   290  }
   291  
   292  func (observabilityTestServer) Duplex(stream testpb.TestServiceDuplexYARPCServer) error {
   293  	for {
   294  		msg, err := stream.Recv()
   295  		if err != nil {
   296  			return err
   297  		}
   298  		err = stream.Send(msg)
   299  		if err != nil {
   300  			return err
   301  		}
   302  	}
   303  }