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 }