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 }