github.com/alibaba/ilogtail/pkg@v0.0.0-20250526110833-c53b480d046c/helper/grpc_helper.go (about)

     1  // Copyright 2022 iLogtail Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package helper
    16  
    17  import (
    18  	"crypto/tls"
    19  	"fmt"
    20  	"strings"
    21  	"time"
    22  
    23  	tls_helper "github.com/influxdata/telegraf/plugins/common/tls"
    24  	"google.golang.org/genproto/googleapis/rpc/errdetails"
    25  	"google.golang.org/grpc"
    26  	"google.golang.org/grpc/codes"
    27  	"google.golang.org/grpc/credentials"
    28  	"google.golang.org/grpc/credentials/insecure"
    29  	"google.golang.org/grpc/status"
    30  )
    31  
    32  var supportedCompressionType = map[string]interface{}{"gzip": nil, "snappy": nil, "zstd": nil}
    33  
    34  type GrpcClientConfig struct {
    35  	Endpoint string `json:"Endpoint"`
    36  
    37  	// The compression key for supported compression types within collector.
    38  	Compression string `json:"Compression"`
    39  
    40  	// The headers associated with gRPC requests.
    41  	Headers map[string]string `json:"Headers"`
    42  
    43  	// Sets the balancer in grpclb_policy to discover the servers. Default is pick_first.
    44  	// https://github.com/grpc/grpc-go/blob/master/examples/features/load_balancing/README.md
    45  	BalancerName string `json:"BalancerName"`
    46  
    47  	// WaitForReady parameter configures client to wait for ready state before sending data.
    48  	// (https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md)
    49  	WaitForReady bool `json:"WaitForReady"`
    50  
    51  	// ReadBufferSize for gRPC client. See grpchelper.WithReadBufferSize.
    52  	// (https://godoc.org/google.golang.org/grpc#WithReadBufferSize).
    53  	ReadBufferSize int `json:"ReadBufferSize"`
    54  
    55  	// WriteBufferSize for gRPC gRPC. See grpchelper.WithWriteBufferSize.
    56  	// (https://godoc.org/google.golang.org/grpc#WithWriteBufferSize).
    57  	WriteBufferSize int `json:"WriteBufferSize"`
    58  
    59  	// Send retry setting
    60  	Retry RetryConfig `json:"Retry"`
    61  
    62  	Timeout int `json:"Timeout"`
    63  }
    64  
    65  type RetryConfig struct {
    66  	Enable       bool
    67  	MaxCount     int           `json:"MaxCount"`
    68  	DefaultDelay time.Duration `json:"DefaultDelay"`
    69  }
    70  
    71  // GetDialOptions maps GrpcClientConfig to a slice of dial options for gRPC.
    72  func (cfg *GrpcClientConfig) GetDialOptions() ([]grpc.DialOption, error) {
    73  	var opts []grpc.DialOption
    74  	if cfg.Compression != "" && cfg.Compression != "none" {
    75  		if _, ok := supportedCompressionType[cfg.Compression]; !ok {
    76  			return nil, fmt.Errorf("unsupported compression type %q", cfg.Compression)
    77  		}
    78  		opts = append(opts, grpc.WithDefaultCallOptions(grpc.UseCompressor(cfg.Compression)))
    79  	}
    80  
    81  	cred := insecure.NewCredentials()
    82  	if strings.HasPrefix(cfg.Endpoint, "https://") {
    83  		/* #nosec G402 - it is a false positive since tls.VersionTLS13 is the latest version */
    84  		cred = credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS13})
    85  	}
    86  	opts = append(opts, grpc.WithTransportCredentials(cred))
    87  
    88  	if cfg.ReadBufferSize > 0 {
    89  		opts = append(opts, grpc.WithReadBufferSize(cfg.ReadBufferSize))
    90  	}
    91  
    92  	if cfg.WriteBufferSize > 0 {
    93  		opts = append(opts, grpc.WithWriteBufferSize(cfg.WriteBufferSize))
    94  	}
    95  
    96  	opts = append(opts, grpc.WithTimeout(cfg.GetTimeout()))
    97  	opts = append(opts, grpc.WithDefaultCallOptions(grpc.WaitForReady(cfg.WaitForReady)))
    98  
    99  	if cfg.WaitForReady {
   100  		opts = append(opts, grpc.WithBlock())
   101  	}
   102  
   103  	return opts, nil
   104  }
   105  
   106  func (cfg *GrpcClientConfig) GetEndpoint() string {
   107  	if strings.HasPrefix(cfg.Endpoint, "http://") {
   108  		return strings.TrimPrefix(cfg.Endpoint, "http://")
   109  	}
   110  	if strings.HasPrefix(cfg.Endpoint, "https://") {
   111  		return strings.TrimPrefix(cfg.Endpoint, "https://")
   112  	}
   113  	return cfg.Endpoint
   114  }
   115  
   116  func (cfg *GrpcClientConfig) GetTimeout() time.Duration {
   117  	if cfg.Timeout <= 0 {
   118  		return 5000 * time.Millisecond
   119  	}
   120  	return time.Duration(cfg.Timeout) * time.Millisecond
   121  }
   122  
   123  // RetryInfo Handle retry for grpc. Refer to https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/otlpexporter/otlp.go#L121
   124  type RetryInfo struct {
   125  	delay time.Duration
   126  	err   error
   127  }
   128  
   129  func (r *RetryInfo) Error() error {
   130  	return r.err
   131  }
   132  
   133  func (r *RetryInfo) ShouldDelay(delay time.Duration) time.Duration {
   134  	if r.delay != 0 {
   135  		return r.delay
   136  	}
   137  	return delay
   138  }
   139  
   140  func GetRetryInfo(err error) *RetryInfo {
   141  	if err == nil {
   142  		// Request is successful, we are done.
   143  		return nil
   144  	}
   145  	// We have an error, check gRPC status code.
   146  
   147  	st := status.Convert(err)
   148  	if st.Code() == codes.OK {
   149  		// Not really an error, still success.
   150  		return nil
   151  	}
   152  	retryInfo := getRetryInfo(st)
   153  
   154  	if !shouldRetry(st.Code(), retryInfo) {
   155  		// It is not a retryable error, we should not retry.
   156  		return nil
   157  	}
   158  	throttle := getThrottleDuration(retryInfo)
   159  	return &RetryInfo{delay: throttle, err: err}
   160  }
   161  
   162  func shouldRetry(code codes.Code, retryInfo *errdetails.RetryInfo) bool {
   163  	switch code {
   164  	case codes.Canceled,
   165  		codes.DeadlineExceeded,
   166  		codes.Aborted,
   167  		codes.OutOfRange,
   168  		codes.Unavailable,
   169  		codes.DataLoss:
   170  		// These are retryable errors.
   171  		return true
   172  	case codes.ResourceExhausted:
   173  		// Retry only if RetryInfo was supplied by the server.
   174  		// This indicates that the server can still recover from resource exhaustion.
   175  		return retryInfo != nil
   176  	}
   177  	// Don't retry on any other code.
   178  	return false
   179  }
   180  
   181  func getRetryInfo(status *status.Status) *errdetails.RetryInfo {
   182  	for _, detail := range status.Details() {
   183  		if t, ok := detail.(*errdetails.RetryInfo); ok {
   184  			return t
   185  		}
   186  	}
   187  	return nil
   188  }
   189  
   190  func getThrottleDuration(t *errdetails.RetryInfo) time.Duration {
   191  	if t == nil || t.RetryDelay == nil {
   192  		return 0
   193  	}
   194  	if t.RetryDelay.Seconds > 0 || t.RetryDelay.Nanos > 0 {
   195  		return time.Duration(t.RetryDelay.Seconds)*time.Second + time.Duration(t.RetryDelay.Nanos)*time.Nanosecond
   196  	}
   197  	return 0
   198  }
   199  
   200  type GRPCServerSettings struct {
   201  	Endpoint string `json:"Endpoint"`
   202  
   203  	MaxRecvMsgSizeMiB int `json:"MaxRecvMsgSizeMiB"`
   204  
   205  	MaxConcurrentStreams int `json:"MaxConcurrentStreams"`
   206  
   207  	ReadBufferSize int `json:"ReadBufferSize"`
   208  
   209  	WriteBufferSize int `json:"WriteBufferSize"`
   210  
   211  	Compression string `json:"Compression"`
   212  
   213  	Decompression string `json:"Decompression"`
   214  
   215  	TLSConfig tls_helper.ServerConfig `json:"TLSConfig"`
   216  }
   217  
   218  func (cfg *GRPCServerSettings) GetServerOption() ([]grpc.ServerOption, error) {
   219  	var opts []grpc.ServerOption
   220  	var err error
   221  	if cfg != nil {
   222  		if cfg.MaxRecvMsgSizeMiB > 0 {
   223  			opts = append(opts, grpc.MaxRecvMsgSize(cfg.MaxRecvMsgSizeMiB*1024*1024))
   224  		}
   225  		if cfg.MaxConcurrentStreams > 0 {
   226  			opts = append(opts, grpc.MaxConcurrentStreams(uint32(cfg.MaxConcurrentStreams)))
   227  		}
   228  
   229  		if cfg.ReadBufferSize > 0 {
   230  			opts = append(opts, grpc.ReadBufferSize(cfg.ReadBufferSize))
   231  		}
   232  
   233  		if cfg.WriteBufferSize > 0 {
   234  			opts = append(opts, grpc.WriteBufferSize(cfg.WriteBufferSize))
   235  		}
   236  
   237  		var tlsConfig *tls.Config
   238  		tlsConfig, err = cfg.TLSConfig.TLSConfig()
   239  		if err == nil && tlsConfig != nil {
   240  			opts = append(opts, grpc.Creds(credentials.NewTLS(tlsConfig)))
   241  		}
   242  
   243  		dc := strings.ToLower(cfg.Decompression)
   244  		if dc != "" && dc != "none" {
   245  			dc := strings.ToLower(cfg.Decompression)
   246  			switch dc {
   247  			case "gzip":
   248  				opts = append(opts, grpc.RPCDecompressor(grpc.NewGZIPDecompressor()))
   249  			default:
   250  				err = fmt.Errorf("invalid decompression: %s", cfg.Decompression)
   251  			}
   252  		}
   253  
   254  		cp := strings.ToLower(cfg.Compression)
   255  		if cp != "" && cp != "none" {
   256  			switch cp {
   257  			case "gzip":
   258  				opts = append(opts, grpc.RPCCompressor(grpc.NewGZIPCompressor()))
   259  			default:
   260  				err = fmt.Errorf("invalid compression: %s", cfg.Compression)
   261  			}
   262  		}
   263  
   264  	}
   265  
   266  	return opts, err
   267  }