github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/storage/promremote/storage.go (about)

     1  // Copyright (c) 2021  Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package promremote
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"net/http"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/m3db/m3/src/query/block"
    34  	"github.com/m3db/m3/src/query/storage"
    35  	"github.com/m3db/m3/src/query/storage/m3/consolidators"
    36  	"github.com/m3db/m3/src/query/storage/m3/storagemetadata"
    37  	xerrors "github.com/m3db/m3/src/x/errors"
    38  	"github.com/m3db/m3/src/x/instrument"
    39  	xhttp "github.com/m3db/m3/src/x/net/http"
    40  
    41  	"github.com/pkg/errors"
    42  	"github.com/uber-go/tally"
    43  	"go.uber.org/zap"
    44  )
    45  
    46  const metricsScope = "prom_remote_storage"
    47  
    48  var errorReadingBody = []byte("error reading body")
    49  
    50  var errNoEndpoints = errors.New("write did not match any of known endpoints")
    51  
    52  // NewStorage returns new Prometheus remote write compatible storage
    53  func NewStorage(opts Options) (storage.Storage, error) {
    54  	client := xhttp.NewHTTPClient(opts.httpOptions)
    55  	scope := opts.scope.SubScope(metricsScope)
    56  	s := &promStorage{
    57  		opts:            opts,
    58  		client:          client,
    59  		endpointMetrics: initEndpointMetrics(opts.endpoints, scope),
    60  		droppedWrites:   scope.Counter("dropped_writes"),
    61  		logger:          opts.logger,
    62  	}
    63  	return s, nil
    64  }
    65  
    66  type promStorage struct {
    67  	unimplementedPromStorageMethods
    68  	opts            Options
    69  	client          *http.Client
    70  	endpointMetrics map[string]instrument.MethodMetrics
    71  	droppedWrites   tally.Counter
    72  	logger          *zap.Logger
    73  }
    74  
    75  func (p *promStorage) Write(ctx context.Context, query *storage.WriteQuery) error {
    76  	encoded, err := convertAndEncodeWriteQuery(query)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	var wg sync.WaitGroup
    82  	multiErr := xerrors.NewMultiError()
    83  	var errLock sync.Mutex
    84  	atLeastOneEndpointMatched := false
    85  	for _, endpoint := range p.opts.endpoints {
    86  		endpoint := endpoint
    87  		if endpoint.attributes.Resolution != query.Attributes().Resolution ||
    88  			endpoint.attributes.Retention != query.Attributes().Retention {
    89  			continue
    90  		}
    91  
    92  		metrics := p.endpointMetrics[endpoint.name]
    93  		atLeastOneEndpointMatched = true
    94  
    95  		wg.Add(1)
    96  		go func() {
    97  			defer wg.Done()
    98  			err := p.writeSingle(ctx, metrics, endpoint.address, bytes.NewReader(encoded))
    99  			if err != nil {
   100  				errLock.Lock()
   101  				multiErr = multiErr.Add(err)
   102  				errLock.Unlock()
   103  				return
   104  			}
   105  		}()
   106  	}
   107  
   108  	wg.Wait()
   109  
   110  	if !atLeastOneEndpointMatched {
   111  		p.droppedWrites.Inc(1)
   112  		multiErr = multiErr.Add(errNoEndpoints)
   113  		p.logger.Warn(
   114  			"write did not match any of known endpoints",
   115  			zap.Duration("retention", query.Attributes().Retention),
   116  			zap.Duration("resolution", query.Attributes().Resolution),
   117  		)
   118  	}
   119  	return multiErr.FinalError()
   120  }
   121  
   122  func (p *promStorage) Type() storage.Type {
   123  	return storage.TypeRemoteDC
   124  }
   125  
   126  func (p *promStorage) Close() error {
   127  	p.client.CloseIdleConnections()
   128  	return nil
   129  }
   130  
   131  func (p *promStorage) ErrorBehavior() storage.ErrorBehavior {
   132  	return storage.BehaviorFail
   133  }
   134  
   135  func (p *promStorage) Name() string {
   136  	return "prom-remote"
   137  }
   138  
   139  func (p *promStorage) writeSingle(
   140  	ctx context.Context,
   141  	metrics instrument.MethodMetrics,
   142  	address string,
   143  	encoded io.Reader,
   144  ) error {
   145  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, address, encoded)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	req.Header.Set("content-encoding", "snappy")
   150  	req.Header.Set(xhttp.HeaderContentType, xhttp.ContentTypeProtobuf)
   151  
   152  	start := time.Now()
   153  	resp, err := p.client.Do(req)
   154  	methodDuration := time.Since(start)
   155  	if err != nil {
   156  		metrics.ReportError(methodDuration)
   157  		return err
   158  	}
   159  	defer func() { _ = resp.Body.Close() }()
   160  	if resp.StatusCode/100 != 2 {
   161  		metrics.ReportError(methodDuration)
   162  		response, err := ioutil.ReadAll(resp.Body)
   163  		if err != nil {
   164  			p.logger.Error("error reading body", zap.Error(err))
   165  			response = errorReadingBody
   166  		}
   167  		genericError := fmt.Errorf(
   168  			"expected status code 2XX: actual=%v, address=%v, resp=%s",
   169  			resp.StatusCode, address, response,
   170  		)
   171  		if resp.StatusCode < 500 && resp.StatusCode != http.StatusTooManyRequests {
   172  			return xerrors.NewInvalidParamsError(genericError)
   173  		}
   174  		return genericError
   175  	}
   176  	metrics.ReportSuccess(methodDuration)
   177  	return nil
   178  }
   179  
   180  func initEndpointMetrics(endpoints []EndpointOptions, scope tally.Scope) map[string]instrument.MethodMetrics {
   181  	metrics := make(map[string]instrument.MethodMetrics, len(endpoints))
   182  	for _, endpoint := range endpoints {
   183  		endpointScope := scope.Tagged(map[string]string{"endpoint_name": endpoint.name})
   184  		methodMetrics := instrument.NewMethodMetrics(endpointScope, "writeSingle", instrument.TimerOptions{
   185  			Type:             instrument.HistogramTimerType,
   186  			HistogramBuckets: tally.DefaultBuckets,
   187  		})
   188  		metrics[endpoint.name] = methodMetrics
   189  	}
   190  	return metrics
   191  }
   192  
   193  var _ storage.Storage = &promStorage{}
   194  
   195  type unimplementedPromStorageMethods struct{}
   196  
   197  func (p *unimplementedPromStorageMethods) FetchProm(
   198  	_ context.Context,
   199  	_ *storage.FetchQuery,
   200  	_ *storage.FetchOptions,
   201  ) (storage.PromResult, error) {
   202  	return storage.PromResult{}, unimplementedError("FetchProm")
   203  }
   204  
   205  func (p *unimplementedPromStorageMethods) FetchBlocks(
   206  	_ context.Context,
   207  	_ *storage.FetchQuery,
   208  	_ *storage.FetchOptions,
   209  ) (block.Result, error) {
   210  	return block.Result{}, unimplementedError("FetchBlocks")
   211  }
   212  
   213  func (p *unimplementedPromStorageMethods) FetchCompressed(
   214  	_ context.Context,
   215  	_ *storage.FetchQuery,
   216  	_ *storage.FetchOptions,
   217  ) (consolidators.MultiFetchResult, error) {
   218  	return nil, unimplementedError("FetchCompressed")
   219  }
   220  
   221  func (p *unimplementedPromStorageMethods) SearchSeries(
   222  	_ context.Context,
   223  	_ *storage.FetchQuery,
   224  	_ *storage.FetchOptions,
   225  ) (*storage.SearchResults, error) {
   226  	return nil, unimplementedError("SearchSeries")
   227  }
   228  
   229  func (p *unimplementedPromStorageMethods) CompleteTags(
   230  	_ context.Context,
   231  	_ *storage.CompleteTagsQuery,
   232  	_ *storage.FetchOptions,
   233  ) (*consolidators.CompleteTagsResult, error) {
   234  	return nil, unimplementedError("CompleteTags")
   235  }
   236  
   237  func (p *unimplementedPromStorageMethods) QueryStorageMetadataAttributes(
   238  	_ context.Context,
   239  	_, _ time.Time,
   240  	_ *storage.FetchOptions,
   241  ) ([]storagemetadata.Attributes, error) {
   242  	return nil, unimplementedError("QueryStorageMetadataAttributes")
   243  }
   244  
   245  func unimplementedError(name string) error {
   246  	return fmt.Errorf("promStorage: %s method is not supported", name)
   247  }