github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/graphite/render_parser.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 "context" 25 "errors" 26 "fmt" 27 "io" 28 "math" 29 "net/http" 30 "strconv" 31 "time" 32 33 "github.com/m3db/m3/src/query/api/v1/handler/graphite/pickle" 34 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions" 35 "github.com/m3db/m3/src/query/graphite/graphite" 36 "github.com/m3db/m3/src/query/graphite/ts" 37 "github.com/m3db/m3/src/query/storage" 38 "github.com/m3db/m3/src/query/util/json" 39 xerrors "github.com/m3db/m3/src/x/errors" 40 xhttp "github.com/m3db/m3/src/x/net/http" 41 ) 42 43 const ( 44 realTimeQueryThreshold = time.Minute 45 queryRangeShiftThreshold = 55 * time.Minute 46 queryRangeShift = 15 * time.Second 47 pickleFormat = "pickle" 48 ) 49 50 var ( 51 errNoTarget = xerrors.NewInvalidParamsError(errors.New("no 'target' specified")) 52 errFromNotBeforeUntil = xerrors.NewInvalidParamsError(errors.New("'from' must come before 'until'")) 53 ) 54 55 // WriteRenderResponse writes the response to a render request 56 func WriteRenderResponse( 57 w http.ResponseWriter, 58 series ts.SeriesList, 59 format string, 60 opts renderResultsJSONOptions, 61 ) error { 62 if format == pickleFormat { 63 w.Header().Set(xhttp.HeaderContentType, xhttp.ContentTypeOctetStream) 64 return renderResultsPickle(w, series.Values) 65 } 66 67 // NB: return json unless requesting specifically `pickleFormat` 68 w.Header().Set(xhttp.HeaderContentType, xhttp.ContentTypeJSON) 69 return renderResultsJSON(w, series.Values, opts) 70 } 71 72 const ( 73 tzOffsetForAbsoluteTime = time.Duration(0) 74 maxTimeout = time.Minute 75 ) 76 77 // RenderRequest are the arguments to a render call. 78 type RenderRequest struct { 79 Targets []string 80 Format string 81 From time.Time 82 Until time.Time 83 MaxDataPoints int64 84 Compare time.Duration 85 Timeout time.Duration 86 } 87 88 // ParseRenderRequest parses the arguments to a render call from an incoming request. 89 func ParseRenderRequest( 90 ctx context.Context, 91 r *http.Request, 92 fetchOptsBuilder handleroptions.FetchOptionsBuilder, 93 ) (context.Context, RenderRequest, *storage.FetchOptions, error) { 94 ctx, fetchOpts, err := fetchOptsBuilder.NewFetchOptions(ctx, r) 95 if err != nil { 96 return nil, RenderRequest{}, nil, err 97 } 98 99 if err := r.ParseForm(); err != nil { 100 return nil, RenderRequest{}, nil, err 101 } 102 103 var ( 104 p = RenderRequest{ 105 Timeout: fetchOpts.Timeout, 106 } 107 now = time.Now() 108 ) 109 p.Targets = r.Form["target"] 110 if len(p.Targets) == 0 { 111 return nil, p, nil, errNoTarget 112 } 113 114 fromString, untilString := r.FormValue("from"), r.FormValue("until") 115 if len(fromString) == 0 { 116 fromString = "-30min" 117 } 118 119 if len(untilString) == 0 { 120 untilString = "now" 121 } 122 123 if p.From, err = graphite.ParseTime( 124 fromString, 125 now, 126 tzOffsetForAbsoluteTime, 127 ); err != nil { 128 return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'from': %s", fromString)) 129 } 130 131 if p.Until, err = graphite.ParseTime( 132 untilString, 133 now, 134 tzOffsetForAbsoluteTime, 135 ); err != nil { 136 return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'until': %s", untilString)) 137 } 138 139 if !p.From.Before(p.Until) { 140 return nil, p, nil, errFromNotBeforeUntil 141 } 142 143 // If this is a real-time query, and the query range is large enough, we shift the query 144 // range slightly to take into account the clock skew between the client's local time and 145 // the server's local time in order to take advantage of possibly higher-resolution data. 146 // In the future we could potentially distinguish absolute time and relative time and only 147 // use the time range for policy resolution, although we need to be careful when passing 148 // the range for cross-DC queries. 149 isRealTimeQuery := now.Sub(p.Until) < realTimeQueryThreshold 150 isLargeRangeQuery := p.Until.Sub(p.From) > queryRangeShiftThreshold 151 if isRealTimeQuery && isLargeRangeQuery { 152 p.From = p.From.Add(queryRangeShift) 153 p.Until = p.Until.Add(queryRangeShift) 154 } 155 156 offset := r.FormValue("offset") 157 if len(offset) > 0 { 158 dur, err := graphite.ParseDuration(offset) 159 if err != nil { 160 err = xerrors.NewInvalidParamsError(err) 161 return nil, p, nil, xerrors.NewRenamedError(err, fmt.Errorf("invalid 'offset': %w", err)) 162 } 163 164 p.Until = p.Until.Add(dur) 165 p.From = p.From.Add(dur) 166 } 167 168 maxDataPointsString := r.FormValue("maxDataPoints") 169 if len(maxDataPointsString) != 0 { 170 p.MaxDataPoints, err = strconv.ParseInt(maxDataPointsString, 10, 64) 171 172 if err != nil || p.MaxDataPoints < 1 { 173 return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'maxDataPoints': %s", maxDataPointsString)) 174 } 175 } else { 176 p.MaxDataPoints = math.MaxInt64 177 } 178 179 compareString := r.FormValue("compare") 180 181 if compareFrom, err := graphite.ParseTime( 182 compareString, 183 p.From, 184 tzOffsetForAbsoluteTime, 185 ); err != nil && len(compareString) != 0 { 186 return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'compare': %s", compareString)) 187 } else if p.From.Before(compareFrom) { 188 return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("'compare' must be in the past")) 189 } else { 190 p.Compare = compareFrom.Sub(p.From) 191 } 192 193 return ctx, p, fetchOpts, nil 194 } 195 196 type renderResultsJSONOptions struct { 197 renderSeriesAllNaNs bool 198 } 199 200 func renderResultsJSON( 201 w io.Writer, 202 series []*ts.Series, 203 opts renderResultsJSONOptions, 204 ) error { 205 jw := json.NewWriter(w) 206 jw.BeginArray() 207 for _, s := range series { 208 jw.BeginObject() 209 jw.BeginObjectField("target") 210 jw.WriteString(s.Name()) 211 jw.BeginObjectField("datapoints") 212 jw.BeginArray() 213 214 if !s.AllNaN() || opts.renderSeriesAllNaNs { 215 for i := 0; i < s.Len(); i++ { 216 timestamp, val := s.StartTimeForStep(i), s.ValueAt(i) 217 jw.BeginArray() 218 jw.WriteFloat64(val) 219 jw.WriteInt(int(timestamp.Unix())) 220 jw.EndArray() 221 } 222 } 223 224 jw.EndArray() 225 jw.BeginObjectField("step_size_ms") 226 jw.WriteInt(s.MillisPerStep()) 227 228 jw.EndObject() 229 } 230 jw.EndArray() 231 return jw.Close() 232 } 233 234 func renderResultsPickle(w io.Writer, series []*ts.Series) error { 235 pw := pickle.NewWriter(w) 236 pw.BeginList() 237 238 for _, s := range series { 239 pw.BeginDict() 240 pw.WriteDictKey("name") 241 pw.WriteString(s.Name()) 242 243 pw.WriteDictKey("start") 244 pw.WriteInt(int(s.StartTime().UTC().Unix())) 245 246 pw.WriteDictKey("end") 247 pw.WriteInt(int(s.EndTime().UTC().Unix())) 248 249 pw.WriteDictKey("step") 250 pw.WriteInt(s.MillisPerStep() / 1000) 251 252 pw.WriteDictKey("values") 253 pw.BeginList() 254 for i := 0; i < s.Len(); i++ { 255 pw.WriteFloat64(s.ValueAt(i)) 256 } 257 pw.EndList() 258 259 pw.EndDict() 260 } 261 262 pw.EndList() 263 264 return pw.Close() 265 }