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 }