github.com/m3db/m3@v1.5.0/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/pkg/errors" 34 "github.com/uber-go/tally" 35 "go.uber.org/zap" 36 37 "github.com/m3db/m3/src/query/block" 38 "github.com/m3db/m3/src/query/storage" 39 "github.com/m3db/m3/src/query/storage/m3/consolidators" 40 "github.com/m3db/m3/src/query/storage/m3/storagemetadata" 41 xerrors "github.com/m3db/m3/src/x/errors" 42 "github.com/m3db/m3/src/x/instrument" 43 xhttp "github.com/m3db/m3/src/x/net/http" 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 }