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)