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 }