github.com/yandex/pandora@v0.5.32/components/guns/grpc/core.go (about) 1 package grpc 2 3 import ( 4 "context" 5 "crypto/tls" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/golang/protobuf/proto" 14 "github.com/jhump/protoreflect/desc" 15 "github.com/jhump/protoreflect/dynamic" 16 "github.com/jhump/protoreflect/dynamic/grpcdynamic" 17 "github.com/jhump/protoreflect/grpcreflect" 18 ammo "github.com/yandex/pandora/components/providers/grpc" 19 "github.com/yandex/pandora/core" 20 "github.com/yandex/pandora/core/aggregator/netsample" 21 "github.com/yandex/pandora/core/clientpool" 22 "github.com/yandex/pandora/core/warmup" 23 "github.com/yandex/pandora/lib/answlog" 24 "go.uber.org/zap" 25 "golang.org/x/exp/maps" 26 "google.golang.org/grpc" 27 "google.golang.org/grpc/codes" 28 "google.golang.org/grpc/credentials" 29 "google.golang.org/grpc/metadata" 30 "google.golang.org/grpc/status" 31 ) 32 33 const defaultTimeout = time.Second * 15 34 35 type Sample struct { 36 URL string 37 ShootTimeSeconds float64 38 } 39 40 type GrpcDialOptions struct { 41 Authority string `config:"authority"` 42 Timeout time.Duration `config:"timeout"` 43 } 44 45 type GunConfig struct { 46 Target string `validate:"required"` 47 ReflectPort int64 `config:"reflect_port"` 48 ReflectMetadata map[string]string `config:"reflect_metadata"` 49 Timeout time.Duration `config:"timeout"` // grpc request timeout 50 TLS bool `config:"tls"` 51 DialOptions GrpcDialOptions `config:"dial_options"` 52 AnswLog AnswLogConfig `config:"answlog"` 53 SharedClient struct { 54 ClientNumber int `config:"client-number,omitempty"` 55 Enabled bool `config:"enabled"` 56 } `config:"shared-client,omitempty"` 57 } 58 59 type AnswLogConfig struct { 60 Enabled bool `config:"enabled"` 61 Path string `config:"path"` 62 Filter string `config:"filter" valid:"oneof=all warning error"` 63 } 64 65 type Gun struct { 66 DebugLog bool 67 Conf GunConfig 68 Aggr core.Aggregator 69 core.GunDeps 70 71 Stub grpcdynamic.Stub 72 Services map[string]desc.MethodDescriptor 73 74 AnswLog *zap.Logger 75 } 76 77 func DefaultGunConfig() GunConfig { 78 return GunConfig{ 79 Target: "default target", 80 AnswLog: AnswLogConfig{ 81 Enabled: false, 82 Path: "answ.log", 83 Filter: "all", 84 }, 85 } 86 } 87 88 func (g *Gun) WarmUp(opts *warmup.Options) (any, error) { 89 return g.createSharedDeps(opts) 90 } 91 92 func (g *Gun) createSharedDeps(opts *warmup.Options) (*SharedDeps, error) { 93 services, err := g.prepareMethodList(opts) 94 if err != nil { 95 return nil, err 96 } 97 clientPool, err := g.prepareClientPool() 98 if err != nil { 99 return nil, err 100 } 101 return &SharedDeps{ 102 services: services, 103 clientPool: clientPool, 104 }, nil 105 } 106 107 func (g *Gun) prepareMethodList(opts *warmup.Options) (map[string]desc.MethodDescriptor, error) { 108 conn, err := g.makeReflectionConnect() 109 if err != nil { 110 return nil, fmt.Errorf("failed to connect to target: %w", err) 111 } 112 defer conn.Close() 113 114 refCtx := metadata.NewOutgoingContext(context.Background(), metadata.New(g.Conf.ReflectMetadata)) 115 refClient := grpcreflect.NewClientAuto(refCtx, conn) 116 listServices, err := refClient.ListServices() 117 if err != nil { 118 opts.Log.Error("failed to get services list", zap.Error(err)) // WarmUp calls before Bind() 119 return nil, fmt.Errorf("refClient.ListServices err: %w", err) 120 } 121 services := make(map[string]desc.MethodDescriptor) 122 for _, s := range listServices { 123 service, err := refClient.ResolveService(s) 124 if err != nil { 125 if grpcreflect.IsElementNotFoundError(err) { 126 continue 127 } 128 opts.Log.Error("cant resolveService", zap.String("service_name", s), zap.Error(err)) // WarmUp calls before Bind() 129 return nil, fmt.Errorf("cant resolveService %s; err: %w", s, err) 130 } 131 listMethods := service.GetMethods() 132 for _, m := range listMethods { 133 services[m.GetFullyQualifiedName()] = *m 134 } 135 } 136 return services, nil 137 } 138 139 func (g *Gun) prepareClientPool() (*clientpool.Pool[grpcdynamic.Stub], error) { 140 if !g.Conf.SharedClient.Enabled { 141 return nil, nil 142 } 143 if g.Conf.SharedClient.ClientNumber < 1 { 144 g.Conf.SharedClient.ClientNumber = 1 145 } 146 clientPool, err := clientpool.New[grpcdynamic.Stub](g.Conf.SharedClient.ClientNumber) 147 if err != nil { 148 return nil, fmt.Errorf("create clientpool err: %w", err) 149 } 150 for i := 0; i < g.Conf.SharedClient.ClientNumber; i++ { 151 conn, err := g.makeConnect() 152 if err != nil { 153 return nil, fmt.Errorf("makeGRPCConnect fail %w", err) 154 } 155 clientPool.Add(grpcdynamic.NewStub(conn)) 156 } 157 return clientPool, nil 158 } 159 160 func NewGun(conf GunConfig) *Gun { 161 answLog := answlog.Init(conf.AnswLog.Path, conf.AnswLog.Enabled) 162 return &Gun{Conf: conf, AnswLog: answLog} 163 } 164 165 func (g *Gun) Bind(aggr core.Aggregator, deps core.GunDeps) error { 166 sharedDeps, ok := deps.Shared.(*SharedDeps) 167 if !ok { 168 return errors.New("grpc WarmUp result should be struct: *SharedDeps") 169 } 170 g.Services = sharedDeps.services 171 if sharedDeps.clientPool != nil { 172 g.Stub = sharedDeps.clientPool.Next() 173 } else { 174 conn, err := g.makeConnect() 175 if err != nil { 176 return fmt.Errorf("makeGRPCConnect fail %w", err) 177 } 178 g.Stub = grpcdynamic.NewStub(conn) 179 } 180 181 g.Aggr = aggr 182 g.GunDeps = deps 183 184 if ent := deps.Log.Check(zap.DebugLevel, "Gun bind"); ent != nil { 185 deps.Log.Warn("Deprecation Warning: log level: debug doesn't produce request/response logs anymore. Please use AnswLog option instead:\nanswlog:\n enabled: true\n filter: all|warning|error\n path: answ.log") 186 g.DebugLog = true 187 } 188 189 return nil 190 } 191 192 func (g *Gun) Shoot(am core.Ammo) { 193 customAmmo := am.(*ammo.Ammo) 194 g.shoot(customAmmo) 195 } 196 197 func (g *Gun) shoot(ammo *ammo.Ammo) { 198 code := 0 199 sample := netsample.Acquire(ammo.Tag) 200 defer func() { 201 sample.SetProtoCode(code) 202 g.Aggr.Report(sample) 203 }() 204 205 method, ok := g.Services[ammo.Call] 206 if !ok { 207 g.GunDeps.Log.Error("invalid ammo.Call", zap.String("method", ammo.Call), 208 zap.Strings("allowed_methods", maps.Keys(g.Services))) 209 return 210 } 211 212 payloadJSON, err := json.Marshal(ammo.Payload) 213 if err != nil { 214 g.GunDeps.Log.Error("invalid payload. Cant unmarshal json", zap.Error(err)) 215 return 216 } 217 md := method.GetInputType() 218 message := dynamic.NewMessage(md) 219 err = message.UnmarshalJSON(payloadJSON) 220 if err != nil { 221 code = 400 222 g.GunDeps.Log.Error("invalid payload. Cant unmarshal gRPC", zap.Error(err)) 223 return 224 } 225 226 timeout := defaultTimeout 227 if g.Conf.Timeout != 0 { 228 timeout = g.Conf.Timeout 229 } 230 231 ctx, cancel := context.WithTimeout(context.Background(), timeout) 232 defer cancel() 233 ctx = metadata.NewOutgoingContext(ctx, metadata.New(ammo.Metadata)) 234 out, grpcErr := g.Stub.InvokeRpc(ctx, &method, message) 235 code = ConvertGrpcStatus(grpcErr) 236 237 if grpcErr != nil { 238 g.GunDeps.Log.Error("response error", zap.Error(err)) 239 } 240 241 g.Answ(&method, message, ammo.Metadata, out, grpcErr, code) 242 } 243 244 func (g *Gun) Answ(method *desc.MethodDescriptor, message *dynamic.Message, metadata map[string]string, out proto.Message, grpcErr error, code int) { 245 if g.Conf.AnswLog.Enabled { 246 switch g.Conf.AnswLog.Filter { 247 case "all": 248 g.AnswLogging(g.AnswLog, method, message, metadata, out, grpcErr) 249 250 case "warning": 251 if code >= 400 { 252 g.AnswLogging(g.AnswLog, method, message, metadata, out, grpcErr) 253 } 254 255 case "error": 256 if code >= 500 { 257 g.AnswLogging(g.AnswLog, method, message, metadata, out, grpcErr) 258 } 259 } 260 } 261 } 262 263 func (g *Gun) AnswLogging(logger *zap.Logger, method *desc.MethodDescriptor, request proto.Message, metadata map[string]string, response proto.Message, grpcErr error) { 264 logger.Debug("Request:", zap.Stringer("method", method), zap.Stringer("message", request), zap.Any("metadata", metadata)) 265 if response != nil { 266 logger.Debug("Response:", zap.Stringer("resp", response), zap.Error(grpcErr)) 267 } else { 268 logger.Debug("Response:", zap.String("resp", "empty"), zap.Error(grpcErr)) 269 } 270 } 271 272 func (g *Gun) makeConnect() (conn *grpc.ClientConn, err error) { 273 return MakeGRPCConnect(g.Conf.Target, g.Conf.TLS, g.Conf.DialOptions) 274 } 275 276 func (g *Gun) makeReflectionConnect() (conn *grpc.ClientConn, err error) { 277 target := replacePort(g.Conf.Target, g.Conf.ReflectPort) 278 return MakeGRPCConnect(target, g.Conf.TLS, g.Conf.DialOptions) 279 } 280 281 func MakeGRPCConnect(target string, isTLS bool, dialOptions GrpcDialOptions) (conn *grpc.ClientConn, err error) { 282 opts := []grpc.DialOption{} 283 if isTLS { 284 opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))) 285 } else { 286 opts = append(opts, grpc.WithInsecure()) 287 } 288 timeout := time.Second 289 if dialOptions.Timeout != 0 { 290 timeout = dialOptions.Timeout 291 } 292 opts = append(opts, grpc.WithUserAgent("load test, pandora universal grpc shooter")) 293 294 if dialOptions.Authority != "" { 295 opts = append(opts, grpc.WithAuthority(dialOptions.Authority)) 296 } 297 298 ctx, cncl := context.WithTimeout(context.Background(), timeout) 299 defer cncl() 300 return grpc.DialContext(ctx, target, opts...) 301 } 302 303 func ConvertGrpcStatus(err error) int { 304 s := status.Convert(err) 305 306 switch s.Code() { 307 case codes.OK: 308 return 200 309 case codes.Canceled: 310 return 499 311 case codes.InvalidArgument: 312 return 400 313 case codes.DeadlineExceeded: 314 return 504 315 case codes.NotFound: 316 return 404 317 case codes.AlreadyExists: 318 return 409 319 case codes.PermissionDenied: 320 return 403 321 case codes.ResourceExhausted: 322 return 429 323 case codes.FailedPrecondition: 324 return 400 325 case codes.Aborted: 326 return 409 327 case codes.OutOfRange: 328 return 400 329 case codes.Unimplemented: 330 return 501 331 case codes.Unavailable: 332 return 503 333 case codes.Unauthenticated: 334 return 401 335 default: 336 return 500 337 } 338 } 339 340 func replacePort(host string, port int64) string { 341 if port == 0 { 342 return host 343 } 344 split := strings.Split(host, ":") 345 if len(split) == 1 { 346 return host + ":" + strconv.FormatInt(port, 10) 347 } 348 349 oldPort := split[len(split)-1] 350 if _, err := strconv.ParseInt(oldPort, 10, 64); err != nil { 351 return host + ":" + strconv.FormatInt(port, 10) 352 } 353 354 split[len(split)-1] = strconv.FormatInt(port, 10) 355 return strings.Join(split, ":") 356 } 357 358 var _ warmup.WarmedUp = (*Gun)(nil)