go.uber.org/yarpc@v1.72.1/encoding/thrift/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 thrift_test
    22  
    23  import (
    24  	"context"
    25  	"net"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  	"go.uber.org/net/metrics"
    32  	"go.uber.org/yarpc"
    33  	"go.uber.org/yarpc/api/transport"
    34  	"go.uber.org/yarpc/encoding/thrift/internal/observabilitytest/test"
    35  	"go.uber.org/yarpc/encoding/thrift/internal/observabilitytest/test/testserviceclient"
    36  	"go.uber.org/yarpc/encoding/thrift/internal/observabilitytest/test/testserviceserver"
    37  	"go.uber.org/yarpc/internal/testutils"
    38  	"go.uber.org/yarpc/transport/http"
    39  	"go.uber.org/yarpc/transport/tchannel"
    40  	"go.uber.org/zap"
    41  	"go.uber.org/zap/zapcore"
    42  	"go.uber.org/zap/zaptest/observer"
    43  )
    44  
    45  const (
    46  	_clientName = "caller"
    47  	_serverName = "callee"
    48  
    49  	_wantSuccess              = "success"
    50  	_wantExceptionWithCode    = "exception with code"
    51  	_wantExceptionWithoutCode = "exception with no code"
    52  
    53  	// from observability middleware
    54  	_errorInbound  = "Error handling inbound request."
    55  	_errorOutbound = "Error making outbound call."
    56  )
    57  
    58  func TestThriftExceptionObservability(t *testing.T) {
    59  	transports := []string{tchannel.TransportName, http.TransportName}
    60  
    61  	for _, trans := range transports {
    62  		t.Run("exception with annotation", func(t *testing.T) {
    63  			client, observedLogs, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t, trans)
    64  			defer cleanup()
    65  
    66  			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    67  			defer cancel()
    68  
    69  			_, err := client.Call(ctx, _wantExceptionWithCode)
    70  			require.Error(t, err, "expected call error")
    71  
    72  			ex, ok := err.(*test.ExceptionWithCode)
    73  			require.True(t, ok, "unexpected Thrift exception %v", err)
    74  			assert.Equal(t, _wantExceptionWithCode, ex.Val, "unexpected response")
    75  
    76  			t.Run("logs", func(t *testing.T) {
    77  				wantFields := []zapcore.Field{
    78  					zap.String("error", "application_error"),
    79  					zap.String("errorName", "ExceptionWithCode"),
    80  					zap.String("errorCode", "data-loss"),
    81  					zap.String("errorDetails", "ExceptionWithCode{Val: exception with code}"),
    82  				}
    83  				assertLogs(t, wantFields, observedLogs.TakeAll())
    84  			})
    85  
    86  			t.Run("metrics", func(t *testing.T) {
    87  				wantCounters := []testutils.CounterAssertion{
    88  					{Name: "calls", Value: 1},
    89  					{Name: "panics"},
    90  					// Thrift exceptions without annotations are always classified as
    91  					// client_failures, so this metric check below is important
    92  					{
    93  						Name: "server_failures",
    94  						Tags: map[string]string{
    95  							"error":      "data-loss",
    96  							"error_name": "ExceptionWithCode",
    97  						},
    98  						Value: 1,
    99  					},
   100  					{Name: "successes"},
   101  				}
   102  
   103  				testutils.AssertClientAndServerCounters(t, wantCounters, clientMetricsRoot, serverMetricsRoot)
   104  			})
   105  		})
   106  
   107  		t.Run("exception without annotation ", func(t *testing.T) {
   108  			client, observedLogs, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t, trans)
   109  			defer cleanup()
   110  
   111  			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   112  			defer cancel()
   113  
   114  			_, err := client.Call(ctx, _wantExceptionWithoutCode)
   115  			require.Error(t, err, "expected call error")
   116  
   117  			ex, ok := err.(*test.ExceptionWithoutCode)
   118  			require.True(t, ok, "unexpected Thrift exception")
   119  			assert.Equal(t, _wantExceptionWithoutCode, ex.Val, "unexpected response")
   120  
   121  			t.Run("logs", func(t *testing.T) {
   122  				wantFields := []zapcore.Field{
   123  					zap.String("error", "application_error"),
   124  					zap.String("errorName", "ExceptionWithoutCode"),
   125  					zap.String("errorDetails", "ExceptionWithoutCode{Val: exception with no code}"),
   126  				}
   127  				assertLogs(t, wantFields, observedLogs.TakeAll())
   128  			})
   129  
   130  			t.Run("metrics", func(t *testing.T) {
   131  				wantCounters := []testutils.CounterAssertion{
   132  					{
   133  						Name: "caller_failures",
   134  						Tags: map[string]string{
   135  							"error":      "application_error",
   136  							"error_name": "ExceptionWithoutCode",
   137  						},
   138  						Value: 1,
   139  					},
   140  					{Name: "calls", Value: 1},
   141  					{Name: "panics"},
   142  					{Name: "successes"},
   143  				}
   144  
   145  				testutils.AssertClientAndServerCounters(t, wantCounters, clientMetricsRoot, serverMetricsRoot)
   146  			})
   147  		})
   148  	}
   149  }
   150  
   151  func TestThriftMetrics(t *testing.T) {
   152  	transports := []string{tchannel.TransportName, http.TransportName}
   153  
   154  	for _, trans := range transports {
   155  		t.Run(trans+" thift call", func(t *testing.T) {
   156  			client, _, clientMetricsRoot, serverMetricsRoot, cleanup := initClientAndServer(t, trans)
   157  			defer cleanup()
   158  
   159  			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   160  			defer cancel()
   161  
   162  			_, err := client.Call(ctx, _wantSuccess)
   163  			require.NoError(t, err, "unexpected error")
   164  
   165  			t.Run("counters", func(t *testing.T) {
   166  				wantCounters := []testutils.CounterAssertion{
   167  					{Name: "calls", Value: 1},
   168  					{Name: "panics"},
   169  					{Name: "successes", Value: 1},
   170  				}
   171  				testutils.AssertClientAndServerCounters(t, wantCounters, clientMetricsRoot, serverMetricsRoot)
   172  			})
   173  			t.Run("inbound histograms", func(t *testing.T) {
   174  				wantHistograms := []testutils.HistogramAssertion{
   175  					{Name: "caller_failure_latency_ms"},
   176  					{Name: "request_payload_size_bytes", Value: []int64{16}},
   177  					{Name: "response_payload_size_bytes", Value: []int64{16}},
   178  					{Name: "server_failure_latency_ms"},
   179  					{Name: "success_latency_ms", IgnoreValueCompare: true, ValueLength: 1},
   180  					{Name: "timeout_ttl_ms"},
   181  					{Name: "ttl_ms", Value: []int64{1000}},
   182  				}
   183  				testutils.AssertClientAndServerHistograms(t, wantHistograms, clientMetricsRoot, serverMetricsRoot)
   184  			})
   185  		})
   186  	}
   187  }
   188  
   189  func assertLogs(t *testing.T, wantFields []zapcore.Field, logs []observer.LoggedEntry) {
   190  	require.Len(t, logs, 2, "unexpected number of logs")
   191  
   192  	t.Run("inbound", func(t *testing.T) {
   193  		require.Equal(t, _errorInbound, logs[0].Message, "unexpected log")
   194  		assertLogFields(t, wantFields, logs[0].Context)
   195  	})
   196  
   197  	t.Run("outbound", func(t *testing.T) {
   198  		require.Equal(t, _errorOutbound, logs[1].Message, "unexpected log")
   199  		assertLogFields(t, wantFields, logs[1].Context)
   200  	})
   201  }
   202  
   203  func assertLogFields(t *testing.T, wantFields, gotContext []zapcore.Field) {
   204  	gotFields := make(map[string]zapcore.Field)
   205  	for _, log := range gotContext {
   206  		gotFields[log.Key] = log
   207  	}
   208  
   209  	for _, want := range wantFields {
   210  		got, ok := gotFields[want.Key]
   211  		if assert.True(t, ok, "key %q not found", want.Key) {
   212  			assert.Equal(t, want, got, "unexpected log field")
   213  		}
   214  	}
   215  }
   216  
   217  func initClientAndServer(
   218  	t *testing.T,
   219  	trans string,
   220  ) (
   221  	client testserviceclient.Interface,
   222  	observedLogs *observer.ObservedLogs,
   223  	clientMetricsRoot *metrics.Root,
   224  	serverMetricsRoot *metrics.Root,
   225  	cleanup func(),
   226  ) {
   227  	loggerCore, observedLogs := observer.New(zapcore.DebugLevel)
   228  	clientMetricsRoot, serverMetricsRoot = metrics.New(), metrics.New()
   229  
   230  	serverAddr, cleanupServer := newServer(t, trans, loggerCore, serverMetricsRoot)
   231  	client, cleanupClient := newClient(t, trans, serverAddr, loggerCore, clientMetricsRoot)
   232  
   233  	_ = observedLogs.TakeAll() // ignore all start up logs
   234  
   235  	return client, observedLogs, clientMetricsRoot, serverMetricsRoot, func() {
   236  		cleanupServer()
   237  		cleanupClient()
   238  	}
   239  }
   240  
   241  func newServer(t *testing.T, transportType string, loggerCore zapcore.Core, metricsRoot *metrics.Root) (addr string, cleanup func()) {
   242  	var inbound transport.Inbound
   243  
   244  	switch transportType {
   245  	case tchannel.TransportName:
   246  		listen, err := net.Listen("tcp", "127.0.0.1:0")
   247  		require.NoError(t, err)
   248  
   249  		trans, err := tchannel.NewTransport(
   250  			tchannel.ServiceName(_serverName),
   251  			tchannel.Listener(listen))
   252  		require.NoError(t, err)
   253  
   254  		inbound = trans.NewInbound()
   255  		addr = listen.Addr().String()
   256  
   257  	case http.TransportName:
   258  		hInbound := http.NewTransport().NewInbound("127.0.0.1:0")
   259  		defer func() { addr = "http://" + hInbound.Addr().String() }() // can only get addr after dispatcher has started
   260  		inbound = hInbound
   261  
   262  	default:
   263  		t.Fatal("unknown transport")
   264  	}
   265  
   266  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
   267  		Name:     _serverName,
   268  		Inbounds: yarpc.Inbounds{inbound},
   269  		Logging: yarpc.LoggingConfig{
   270  			Zap: zap.New(loggerCore),
   271  		},
   272  		Metrics: yarpc.MetricsConfig{
   273  			Metrics: metricsRoot.Scope(),
   274  		},
   275  	})
   276  
   277  	dispatcher.Register(testserviceserver.New(&testServer{}))
   278  	require.NoError(t, dispatcher.Start(), "could not start server dispatcher")
   279  
   280  	cleanup = func() { assert.NoError(t, dispatcher.Stop(), "could not stop dispatcher") }
   281  	return addr, cleanup
   282  }
   283  
   284  func newClient(t *testing.T, transportType string, serverAddr string, loggerCore zapcore.Core, metricsRoot *metrics.Root) (client testserviceclient.Interface, cleanup func()) {
   285  	var out transport.UnaryOutbound
   286  
   287  	switch transportType {
   288  	case tchannel.TransportName:
   289  		trans, err := tchannel.NewTransport(tchannel.ServiceName(_clientName))
   290  		require.NoError(t, err)
   291  		out = trans.NewSingleOutbound(serverAddr)
   292  
   293  	case http.TransportName:
   294  		out = http.NewTransport().NewSingleOutbound(serverAddr)
   295  	}
   296  
   297  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
   298  		Name: _clientName,
   299  		Outbounds: map[string]transport.Outbounds{
   300  			_serverName: {
   301  				ServiceName: _serverName,
   302  				Unary:       out,
   303  			},
   304  		},
   305  		Logging: yarpc.LoggingConfig{
   306  			Zap: zap.New(loggerCore),
   307  		},
   308  		Metrics: yarpc.MetricsConfig{
   309  			Metrics: metricsRoot.Scope(),
   310  		},
   311  	})
   312  
   313  	client = testserviceclient.New(dispatcher.ClientConfig(_serverName))
   314  	require.NoError(t, dispatcher.Start(), "could not start client dispatcher")
   315  
   316  	cleanup = func() { assert.NoError(t, dispatcher.Stop(), "could not stop dispatcher") }
   317  	return client, cleanup
   318  }
   319  
   320  type testServer struct{}
   321  
   322  func (testServer) Call(ctx context.Context, val string) (string, error) {
   323  	switch val {
   324  	case _wantExceptionWithoutCode:
   325  		return "", &test.ExceptionWithoutCode{Val: val}
   326  	case _wantExceptionWithCode:
   327  		return "", &test.ExceptionWithCode{Val: val}
   328  	default: // success
   329  		return val, nil
   330  	}
   331  }