github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/chunk/client/gcp/gcs_object_client.go (about) 1 package gcp 2 3 import ( 4 "context" 5 "crypto/tls" 6 "flag" 7 "io" 8 "net" 9 "net/http" 10 "time" 11 12 "cloud.google.com/go/storage" 13 "github.com/grafana/dskit/flagext" 14 "github.com/pkg/errors" 15 "github.com/prometheus/client_golang/prometheus" 16 "google.golang.org/api/iterator" 17 "google.golang.org/api/option" 18 google_http "google.golang.org/api/transport/http" 19 20 "github.com/grafana/loki/pkg/storage/chunk/client" 21 "github.com/grafana/loki/pkg/storage/chunk/client/hedging" 22 "github.com/grafana/loki/pkg/storage/chunk/client/util" 23 ) 24 25 type ClientFactory func(ctx context.Context, opts ...option.ClientOption) (*storage.Client, error) 26 27 type GCSObjectClient struct { 28 cfg GCSConfig 29 30 defaultBucket *storage.BucketHandle 31 getsBuckets *storage.BucketHandle 32 } 33 34 // GCSConfig is config for the GCS Chunk Client. 35 type GCSConfig struct { 36 BucketName string `yaml:"bucket_name"` 37 ServiceAccount flagext.Secret `yaml:"service_account"` 38 ChunkBufferSize int `yaml:"chunk_buffer_size"` 39 RequestTimeout time.Duration `yaml:"request_timeout"` 40 EnableOpenCensus bool `yaml:"enable_opencensus"` 41 EnableHTTP2 bool `yaml:"enable_http2"` 42 43 Insecure bool `yaml:"-"` 44 } 45 46 // RegisterFlags registers flags. 47 func (cfg *GCSConfig) RegisterFlags(f *flag.FlagSet) { 48 cfg.RegisterFlagsWithPrefix("", f) 49 } 50 51 // RegisterFlagsWithPrefix registers flags with prefix. 52 func (cfg *GCSConfig) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) { 53 f.StringVar(&cfg.BucketName, prefix+"gcs.bucketname", "", "Name of GCS bucket. Please refer to https://cloud.google.com/docs/authentication/production for more information about how to configure authentication.") 54 f.Var(&cfg.ServiceAccount, prefix+"gcs.service-account", "Service account key content in JSON format, refer to https://cloud.google.com/iam/docs/creating-managing-service-account-keys for creation.") 55 f.IntVar(&cfg.ChunkBufferSize, prefix+"gcs.chunk-buffer-size", 0, "The size of the buffer that GCS client for each PUT request. 0 to disable buffering.") 56 f.DurationVar(&cfg.RequestTimeout, prefix+"gcs.request-timeout", 0, "The duration after which the requests to GCS should be timed out.") 57 f.BoolVar(&cfg.EnableOpenCensus, prefix+"gcs.enable-opencensus", true, "Enable OpenCensus (OC) instrumentation for all requests.") 58 f.BoolVar(&cfg.EnableHTTP2, prefix+"gcs.enable-http2", true, "Enable HTTP2 connections.") 59 } 60 61 // NewGCSObjectClient makes a new chunk.Client that writes chunks to GCS. 62 func NewGCSObjectClient(ctx context.Context, cfg GCSConfig, hedgingCfg hedging.Config) (*GCSObjectClient, error) { 63 return newGCSObjectClient(ctx, cfg, hedgingCfg, storage.NewClient) 64 } 65 66 func newGCSObjectClient(ctx context.Context, cfg GCSConfig, hedgingCfg hedging.Config, clientFactory ClientFactory) (*GCSObjectClient, error) { 67 // Disabling http2 and hedging is not allowed for POST/LIST/DELETE requests. 68 // This is because there's no benefit for these requests. 69 // Those requests are handled by the default bucket handle. 70 bucket, err := newBucketHandle(ctx, cfg, hedgingCfg, true, false, clientFactory) 71 if err != nil { 72 return nil, err 73 } 74 getsBucket, err := newBucketHandle(ctx, cfg, hedgingCfg, cfg.EnableHTTP2, true, clientFactory) 75 if err != nil { 76 return nil, err 77 } 78 return &GCSObjectClient{ 79 cfg: cfg, 80 defaultBucket: bucket, 81 getsBuckets: getsBucket, 82 }, nil 83 } 84 85 func newBucketHandle(ctx context.Context, cfg GCSConfig, hedgingCfg hedging.Config, enableHTTP2, hedging bool, clientFactory ClientFactory) (*storage.BucketHandle, error) { 86 var opts []option.ClientOption 87 transport, err := gcsTransport(ctx, storage.ScopeReadWrite, cfg.Insecure, enableHTTP2, cfg.ServiceAccount) 88 if err != nil { 89 return nil, err 90 } 91 httpClient := gcsInstrumentation(transport) 92 93 if hedging { 94 httpClient, err = hedgingCfg.ClientWithRegisterer(httpClient, prometheus.WrapRegistererWithPrefix("loki_", prometheus.DefaultRegisterer)) 95 if err != nil { 96 return nil, err 97 } 98 } 99 100 opts = append(opts, option.WithHTTPClient(httpClient)) 101 if !cfg.EnableOpenCensus { 102 opts = append(opts, option.WithTelemetryDisabled()) 103 } 104 105 client, err := clientFactory(ctx, opts...) 106 if err != nil { 107 return nil, err 108 } 109 110 return client.Bucket(cfg.BucketName), nil 111 } 112 113 func (s *GCSObjectClient) Stop() { 114 } 115 116 // GetObject returns a reader and the size for the specified object key from the configured GCS bucket. 117 func (s *GCSObjectClient) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, int64, error) { 118 var cancel context.CancelFunc = func() {} 119 if s.cfg.RequestTimeout > 0 { 120 ctx, cancel = context.WithTimeout(ctx, s.cfg.RequestTimeout) 121 } 122 123 rc, size, err := s.getObject(ctx, objectKey) 124 if err != nil { 125 // cancel the context if there is an error. 126 cancel() 127 return nil, 0, err 128 } 129 // else return a wrapped ReadCloser which cancels the context while closing the reader. 130 return util.NewReadCloserWithContextCancelFunc(rc, cancel), size, nil 131 } 132 133 func (s *GCSObjectClient) getObject(ctx context.Context, objectKey string) (rc io.ReadCloser, size int64, err error) { 134 reader, err := s.getsBuckets.Object(objectKey).NewReader(ctx) 135 if err != nil { 136 return nil, 0, err 137 } 138 139 return reader, reader.Attrs.Size, nil 140 } 141 142 // PutObject puts the specified bytes into the configured GCS bucket at the provided key 143 func (s *GCSObjectClient) PutObject(ctx context.Context, objectKey string, object io.ReadSeeker) error { 144 writer := s.defaultBucket.Object(objectKey).NewWriter(ctx) 145 // Default GCSChunkSize is 8M and for each call, 8M is allocated xD 146 // By setting it to 0, we just upload the object in a single a request 147 // which should work for our chunk sizes. 148 writer.ChunkSize = s.cfg.ChunkBufferSize 149 150 if _, err := io.Copy(writer, object); err != nil { 151 _ = writer.Close() 152 return err 153 } 154 return writer.Close() 155 } 156 157 // List implements chunk.ObjectClient. 158 func (s *GCSObjectClient) List(ctx context.Context, prefix, delimiter string) ([]client.StorageObject, []client.StorageCommonPrefix, error) { 159 var storageObjects []client.StorageObject 160 var commonPrefixes []client.StorageCommonPrefix 161 q := &storage.Query{Prefix: prefix, Delimiter: delimiter} 162 163 // Using delimiter and selected attributes doesn't work well together -- it returns nothing. 164 // Reason is that Go's API only sets "fields=items(name,updated)" parameter in the request, 165 // but what we really need is "fields=prefixes,items(name,updated)". Unfortunately we cannot set that, 166 // so instead we don't use attributes selection when using delimiter. 167 if delimiter == "" { 168 err := q.SetAttrSelection([]string{"Name", "Updated"}) 169 if err != nil { 170 return nil, nil, err 171 } 172 } 173 174 iter := s.defaultBucket.Objects(ctx, q) 175 for { 176 if ctx.Err() != nil { 177 return nil, nil, ctx.Err() 178 } 179 180 attr, err := iter.Next() 181 if err != nil { 182 if err == iterator.Done { 183 break 184 } 185 return nil, nil, err 186 } 187 188 // When doing query with Delimiter, Prefix is the only field set for entries which represent synthetic "directory entries". 189 if attr.Prefix != "" { 190 commonPrefixes = append(commonPrefixes, client.StorageCommonPrefix(attr.Prefix)) 191 continue 192 } 193 194 storageObjects = append(storageObjects, client.StorageObject{ 195 Key: attr.Name, 196 ModifiedAt: attr.Updated, 197 }) 198 } 199 200 return storageObjects, commonPrefixes, nil 201 } 202 203 // DeleteObject deletes the specified object key from the configured GCS bucket. 204 func (s *GCSObjectClient) DeleteObject(ctx context.Context, objectKey string) error { 205 err := s.defaultBucket.Object(objectKey).Delete(ctx) 206 if err != nil { 207 return err 208 } 209 210 return nil 211 } 212 213 // IsObjectNotFoundErr returns true if error means that object is not found. Relevant to GetObject and DeleteObject operations. 214 func (s *GCSObjectClient) IsObjectNotFoundErr(err error) bool { 215 return errors.Is(err, storage.ErrObjectNotExist) 216 } 217 218 func gcsTransport(ctx context.Context, scope string, insecure bool, http2 bool, serviceAccount flagext.Secret) (http.RoundTripper, error) { 219 customTransport := &http.Transport{ 220 Proxy: http.ProxyFromEnvironment, 221 DialContext: (&net.Dialer{ 222 Timeout: 30 * time.Second, 223 KeepAlive: 30 * time.Second, 224 }).DialContext, 225 ForceAttemptHTTP2: true, 226 MaxIdleConns: 200, 227 MaxIdleConnsPerHost: 200, 228 IdleConnTimeout: 90 * time.Second, 229 TLSHandshakeTimeout: 10 * time.Second, 230 ExpectContinueTimeout: 1 * time.Second, 231 } 232 if !http2 { 233 // disable HTTP/2 by setting TLSNextProto to non-nil empty map, as per the net/http documentation. 234 // see http2 section of https://pkg.go.dev/net/http 235 customTransport.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper) 236 customTransport.ForceAttemptHTTP2 = false 237 } 238 transportOptions := []option.ClientOption{option.WithScopes(scope)} 239 if insecure { 240 customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 241 // When using `insecure` (testing only), we add a fake API key as well to skip credential chain lookups. 242 transportOptions = append(transportOptions, option.WithAPIKey("insecure")) 243 } 244 if serviceAccount.String() != "" { 245 transportOptions = append(transportOptions, option.WithCredentialsJSON([]byte(serviceAccount.String()))) 246 } 247 return google_http.NewTransport(ctx, customTransport, transportOptions...) 248 }