github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/graphite/render.go (about) 1 // Copyright (c) 2019 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 graphite 22 23 import ( 24 "fmt" 25 "net/http" 26 "sort" 27 "sync" 28 29 "go.uber.org/zap" 30 "go.uber.org/zap/zapcore" 31 32 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions" 33 "github.com/m3db/m3/src/query/api/v1/options" 34 "github.com/m3db/m3/src/query/api/v1/route" 35 "github.com/m3db/m3/src/query/block" 36 queryerrors "github.com/m3db/m3/src/query/errors" 37 "github.com/m3db/m3/src/query/graphite/common" 38 "github.com/m3db/m3/src/query/graphite/native" 39 graphite "github.com/m3db/m3/src/query/graphite/storage" 40 "github.com/m3db/m3/src/query/graphite/ts" 41 "github.com/m3db/m3/src/query/models" 42 "github.com/m3db/m3/src/query/util/logging" 43 "github.com/m3db/m3/src/x/errors" 44 "github.com/m3db/m3/src/x/instrument" 45 xhttp "github.com/m3db/m3/src/x/net/http" 46 ) 47 48 const ( 49 // ReadURL is the url for the graphite query handler. 50 ReadURL = route.Prefix + "/graphite/render" 51 ) 52 53 // ReadHTTPMethods are the HTTP methods used with this resource. 54 var ReadHTTPMethods = []string{http.MethodGet, http.MethodPost} 55 56 // A renderHandler implements the graphite /render endpoint, including full 57 // support for executing functions. It only works against data in M3. 58 type renderHandler struct { 59 opts options.HandlerOptions 60 engine *native.Engine 61 fetchOptionsBuilder handleroptions.FetchOptionsBuilder 62 queryContextOpts models.QueryContextOptions 63 graphiteOpts graphite.M3WrappedStorageOptions 64 instrumentOpts instrument.Options 65 } 66 67 type respError struct { 68 err error 69 code int 70 } 71 72 // NewRenderHandler returns a new render handler around the given storage. 73 func NewRenderHandler(opts options.HandlerOptions) http.Handler { 74 wrappedStore := graphite.NewM3WrappedStorage(opts.Storage(), 75 opts.M3DBOptions(), opts.InstrumentOpts(), opts.GraphiteStorageOptions()) 76 return &renderHandler{ 77 opts: opts, 78 engine: native.NewEngine(wrappedStore, native.CompileOptions{ 79 EscapeAllNotOnlyQuotes: opts.GraphiteStorageOptions().CompileEscapeAllNotOnlyQuotes, 80 }), 81 fetchOptionsBuilder: opts.GraphiteRenderFetchOptionsBuilder(), 82 queryContextOpts: opts.QueryContextOptions(), 83 graphiteOpts: opts.GraphiteStorageOptions(), 84 instrumentOpts: opts.InstrumentOpts(), 85 } 86 } 87 88 func sendError(errorCh chan error, err error) { 89 select { 90 case errorCh <- err: 91 default: 92 } 93 } 94 95 // ServeHTTP processes the render requests. 96 func (h *renderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 97 if err := h.serveHTTP(w, r); err != nil { 98 if queryerrors.IsTimeout(err) { 99 err = queryerrors.NewErrQueryTimeout(err) 100 } 101 xhttp.WriteError(w, err) 102 } 103 } 104 105 func (h *renderHandler) serveHTTP( 106 w http.ResponseWriter, 107 r *http.Request, 108 ) error { 109 reqCtx, p, fetchOpts, err := ParseRenderRequest(r.Context(), r, h.fetchOptionsBuilder) 110 if err != nil { 111 return xhttp.NewError(err, http.StatusBadRequest) 112 } 113 114 var ( 115 results = make([]ts.SeriesList, len(p.Targets)) 116 errorCh = make(chan error, 1) 117 mu sync.Mutex 118 ) 119 120 ctx := common.NewContext(common.ContextOptions{ 121 Engine: h.engine, 122 Start: p.From, 123 End: p.Until, 124 Timeout: p.Timeout, 125 MaxDataPoints: p.MaxDataPoints, 126 FetchOpts: fetchOpts, 127 }) 128 129 // Set the request context. 130 ctx.SetRequestContext(reqCtx) 131 defer ctx.Close() 132 133 var wg sync.WaitGroup 134 meta := block.NewResultMetadata() 135 wg.Add(len(p.Targets)) 136 for i, target := range p.Targets { 137 i, target := i, target 138 go func() { 139 childCtx := ctx.NewChildContext(common.NewChildContextOptions()) 140 defer func() { 141 if err := recover(); err != nil { 142 // Allow recover from panic. 143 sendError(errorCh, fmt.Errorf("error target '%s' caused panic: %v", target, err)) 144 145 // Log panic. 146 logger := logging.WithContext(r.Context(), h.instrumentOpts). 147 WithOptions(zap.AddStacktrace(zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { 148 return lvl >= zapcore.ErrorLevel 149 }))) 150 logger.Error("panic captured", zap.Any("stack", err)) 151 } 152 _ = childCtx.Close() 153 wg.Done() 154 }() 155 156 exp, err := h.engine.Compile(target) 157 if err != nil { 158 sendError(errorCh, errors.NewRenamedError(err, 159 fmt.Errorf("invalid 'target': %s => %s", target, err))) 160 return 161 } 162 163 targetSeries, err := exp.Execute(childCtx) 164 if err != nil { 165 sendError(errorCh, errors.NewRenamedError(err, 166 fmt.Errorf("error target '%s' returned: %w", target, err))) 167 return 168 } 169 170 // Apply LTTB downsampling to any series that hasn't been resized 171 // to fit max datapoints explicitly using "consolidateBy" function. 172 for i, s := range targetSeries.Values { 173 resizeMillisPerStep, needResize := s.ResizeToMaxDataPointsMillisPerStep(p.MaxDataPoints) 174 if !needResize { 175 continue 176 } 177 178 targetSeries.Values[i] = ts.LTTB(s, s.StartTime(), s.EndTime(), resizeMillisPerStep) 179 } 180 181 mu.Lock() 182 meta = meta.CombineMetadata(targetSeries.Metadata) 183 results[i] = targetSeries 184 mu.Unlock() 185 }() 186 } 187 188 wg.Wait() 189 close(errorCh) 190 err = <-errorCh 191 if err != nil { 192 return err 193 } 194 195 // Count and sort the groups if not sorted already. 196 // NB(r): For certain things like stacking different targets in Grafana 197 // returning targets in order matters to give a deterministic order for 198 // the series to display when stacking. However we should only mutate 199 // the order if no expressions have explicitly applied their own sort. 200 numSeries := 0 201 for _, r := range results { 202 numSeries += r.Len() 203 if !r.SortApplied { 204 // Use sort.Stable for deterministic output. 205 sort.Stable(ts.SeriesByName(r.Values)) 206 } 207 } 208 209 series := make([]*ts.Series, 0, numSeries) 210 for _, r := range results { 211 series = append(series, r.Values...) 212 } 213 214 // We've always sorted the response by this point 215 response := ts.SeriesList{ 216 Values: series, 217 SortApplied: true, 218 } 219 220 if err := handleroptions.AddDBResultResponseHeaders(w, meta, fetchOpts); err != nil { 221 return err 222 } 223 224 return WriteRenderResponse(w, response, p.Format, renderResultsJSONOptions{ 225 renderSeriesAllNaNs: h.graphiteOpts.RenderSeriesAllNaNs, 226 }) 227 }