github.com/thanos-io/thanos@v0.32.5/pkg/queryfrontend/queryrange_codec.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package queryfrontend 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "math" 11 "net/http" 12 "net/url" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/prometheus/common/model" 18 "github.com/prometheus/prometheus/model/labels" 19 "github.com/prometheus/prometheus/promql/parser" 20 "github.com/weaveworks/common/httpgrpc" 21 22 "github.com/thanos-io/thanos/internal/cortex/querier/queryrange" 23 cortexutil "github.com/thanos-io/thanos/internal/cortex/util" 24 25 queryv1 "github.com/thanos-io/thanos/pkg/api/query" 26 "github.com/thanos-io/thanos/pkg/store/storepb" 27 ) 28 29 const ( 30 // Name of the cache control header. 31 cacheControlHeader = "Cache-Control" 32 33 // Value that cacheControlHeader has if the response indicates that the results should not be cached. 34 noStoreValue = "no-store" 35 ) 36 37 var ( 38 errEndBeforeStart = httpgrpc.Errorf(http.StatusBadRequest, "end timestamp must not be before start time") 39 errNegativeStep = httpgrpc.Errorf(http.StatusBadRequest, "zero or negative query resolution step widths are not accepted. Try a positive integer") 40 errStepTooSmall = httpgrpc.Errorf(http.StatusBadRequest, "exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)") 41 errCannotParse = "cannot parse parameter %s" 42 ) 43 44 // queryRangeCodec is used to encode/decode Thanos query range requests and responses. 45 type queryRangeCodec struct { 46 queryrange.Codec 47 partialResponse bool 48 } 49 50 // NewThanosQueryRangeCodec initializes a queryRangeCodec. 51 func NewThanosQueryRangeCodec(partialResponse bool) *queryRangeCodec { 52 return &queryRangeCodec{ 53 Codec: queryrange.PrometheusCodec, 54 partialResponse: partialResponse, 55 } 56 } 57 58 func (c queryRangeCodec) DecodeRequest(_ context.Context, r *http.Request, forwardHeaders []string) (queryrange.Request, error) { 59 var ( 60 result ThanosQueryRangeRequest 61 err error 62 ) 63 result.Start, err = cortexutil.ParseTime(r.FormValue("start")) 64 if err != nil { 65 return nil, err 66 } 67 68 result.End, err = cortexutil.ParseTime(r.FormValue("end")) 69 if err != nil { 70 return nil, err 71 } 72 73 if result.End < result.Start { 74 return nil, errEndBeforeStart 75 } 76 77 result.Step, err = parseDurationMillis(r.FormValue("step")) 78 if err != nil { 79 return nil, err 80 } 81 82 if result.Step <= 0 { 83 return nil, errNegativeStep 84 } 85 86 // For safety, limit the number of returned points per timeseries. 87 // This is sufficient for 60s resolution for a week or 1h resolution for a year. 88 if (result.End-result.Start)/result.Step > 11000 { 89 return nil, errStepTooSmall 90 } 91 92 result.Dedup, err = parseEnableDedupParam(r.FormValue(queryv1.DedupParam)) 93 if err != nil { 94 return nil, err 95 } 96 97 if r.FormValue(queryv1.MaxSourceResolutionParam) == "auto" { 98 result.AutoDownsampling = true 99 result.MaxSourceResolution = result.Step / 5 100 } else { 101 result.MaxSourceResolution, err = parseDownsamplingParamMillis(r.FormValue(queryv1.MaxSourceResolutionParam)) 102 if err != nil { 103 return nil, err 104 } 105 } 106 107 result.PartialResponse, err = parsePartialResponseParam(r.FormValue(queryv1.PartialResponseParam), c.partialResponse) 108 if err != nil { 109 return nil, err 110 } 111 112 if len(r.Form[queryv1.ReplicaLabelsParam]) > 0 { 113 result.ReplicaLabels = r.Form[queryv1.ReplicaLabelsParam] 114 } 115 116 result.StoreMatchers, err = parseMatchersParam(r.Form, queryv1.StoreMatcherParam) 117 if err != nil { 118 return nil, err 119 } 120 121 result.ShardInfo, err = parseShardInfo(r.Form, queryv1.ShardInfoParam) 122 if err != nil { 123 return nil, err 124 } 125 126 result.LookbackDelta, err = parseLookbackDelta(r.Form, queryv1.LookbackDeltaParam) 127 if err != nil { 128 return nil, err 129 } 130 131 result.Query = r.FormValue("query") 132 result.Explain = r.FormValue(queryv1.QueryExplainParam) 133 result.Engine = r.FormValue(queryv1.EngineParam) 134 result.Path = r.URL.Path 135 136 for _, value := range r.Header.Values(cacheControlHeader) { 137 if strings.Contains(value, noStoreValue) { 138 result.CachingOptions.Disabled = true 139 break 140 } 141 } 142 143 for _, header := range forwardHeaders { 144 for h, hv := range r.Header { 145 if strings.EqualFold(h, header) { 146 result.Headers = append(result.Headers, &RequestHeader{Name: h, Values: hv}) 147 break 148 } 149 } 150 } 151 return &result, nil 152 } 153 154 func (c queryRangeCodec) EncodeRequest(ctx context.Context, r queryrange.Request) (*http.Request, error) { 155 thanosReq, ok := r.(*ThanosQueryRangeRequest) 156 if !ok { 157 return nil, httpgrpc.Errorf(http.StatusBadRequest, "invalid request format") 158 } 159 params := url.Values{ 160 "start": []string{encodeTime(thanosReq.Start)}, 161 "end": []string{encodeTime(thanosReq.End)}, 162 "step": []string{encodeDurationMillis(thanosReq.Step)}, 163 "query": []string{thanosReq.Query}, 164 queryv1.QueryExplainParam: []string{thanosReq.Explain}, 165 queryv1.EngineParam: []string{thanosReq.Engine}, 166 queryv1.DedupParam: []string{strconv.FormatBool(thanosReq.Dedup)}, 167 queryv1.PartialResponseParam: []string{strconv.FormatBool(thanosReq.PartialResponse)}, 168 queryv1.ReplicaLabelsParam: thanosReq.ReplicaLabels, 169 } 170 171 if thanosReq.AutoDownsampling { 172 params[queryv1.MaxSourceResolutionParam] = []string{"auto"} 173 } else if thanosReq.MaxSourceResolution != 0 { 174 // Add this param only if it is set. Set to 0 will impact 175 // auto-downsampling in the querier. 176 params[queryv1.MaxSourceResolutionParam] = []string{encodeDurationMillis(thanosReq.MaxSourceResolution)} 177 } 178 179 if len(thanosReq.StoreMatchers) > 0 { 180 params[queryv1.StoreMatcherParam] = matchersToStringSlice(thanosReq.StoreMatchers) 181 } 182 183 if thanosReq.ShardInfo != nil { 184 data, err := encodeShardInfo(thanosReq.ShardInfo) 185 if err != nil { 186 return nil, err 187 } 188 params[queryv1.ShardInfoParam] = []string{data} 189 } 190 191 if thanosReq.LookbackDelta > 0 { 192 params[queryv1.LookbackDeltaParam] = []string{encodeDurationMillis(thanosReq.LookbackDelta)} 193 } 194 195 req, err := http.NewRequest(http.MethodPost, thanosReq.Path, bytes.NewBufferString(params.Encode())) 196 if err != nil { 197 return nil, httpgrpc.Errorf(http.StatusBadRequest, "error creating request: %s", err.Error()) 198 } 199 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 200 for _, hv := range thanosReq.Headers { 201 for _, v := range hv.Values { 202 req.Header.Add(hv.Name, v) 203 } 204 } 205 return req.WithContext(ctx), nil 206 } 207 208 func parseDurationMillis(s string) (int64, error) { 209 if d, err := strconv.ParseFloat(s, 64); err == nil { 210 ts := d * float64(time.Second/time.Millisecond) 211 if ts > float64(math.MaxInt64) || ts < float64(math.MinInt64) { 212 return 0, httpgrpc.Errorf(http.StatusBadRequest, "cannot parse %q to a valid duration. It overflows int64", s) 213 } 214 return int64(ts), nil 215 } 216 if d, err := model.ParseDuration(s); err == nil { 217 return int64(d) / int64(time.Millisecond/time.Nanosecond), nil 218 } 219 return 0, httpgrpc.Errorf(http.StatusBadRequest, "cannot parse %q to a valid duration", s) 220 } 221 222 func parseEnableDedupParam(s string) (bool, error) { 223 enableDeduplication := true // Deduplication is enabled by default. 224 if s != "" { 225 var err error 226 enableDeduplication, err = strconv.ParseBool(s) 227 if err != nil { 228 return enableDeduplication, httpgrpc.Errorf(http.StatusBadRequest, errCannotParse, queryv1.DedupParam) 229 } 230 } 231 232 return enableDeduplication, nil 233 } 234 235 func parseDownsamplingParamMillis(s string) (int64, error) { 236 var maxSourceResolution int64 237 if s != "" { 238 var err error 239 maxSourceResolution, err = parseDurationMillis(s) 240 if err != nil { 241 return maxSourceResolution, httpgrpc.Errorf(http.StatusBadRequest, errCannotParse, queryv1.MaxSourceResolutionParam) 242 } 243 } 244 245 if maxSourceResolution < 0 { 246 return 0, httpgrpc.Errorf(http.StatusBadRequest, "negative max_source_resolution is not accepted. Try a positive integer") 247 } 248 249 return maxSourceResolution, nil 250 } 251 252 func parsePartialResponseParam(s string, defaultEnablePartialResponse bool) (bool, error) { 253 if s != "" { 254 var err error 255 defaultEnablePartialResponse, err = strconv.ParseBool(s) 256 if err != nil { 257 return defaultEnablePartialResponse, httpgrpc.Errorf(http.StatusBadRequest, errCannotParse, queryv1.PartialResponseParam) 258 } 259 } 260 261 return defaultEnablePartialResponse, nil 262 } 263 264 func parseMatchersParam(ss url.Values, matcherParam string) ([][]*labels.Matcher, error) { 265 matchers := make([][]*labels.Matcher, 0, len(ss[matcherParam])) 266 for _, s := range ss[matcherParam] { 267 ms, err := parser.ParseMetricSelector(s) 268 if err != nil { 269 return nil, httpgrpc.Errorf(http.StatusBadRequest, errCannotParse, matcherParam) 270 } 271 matchers = append(matchers, ms) 272 } 273 return matchers, nil 274 } 275 276 func parseLookbackDelta(ss url.Values, key string) (int64, error) { 277 data, ok := ss[key] 278 if !ok || len(data) == 0 { 279 return 0, nil 280 } 281 282 return parseDurationMillis(data[0]) 283 } 284 285 func parseShardInfo(ss url.Values, key string) (*storepb.ShardInfo, error) { 286 data, ok := ss[key] 287 if !ok || len(data) == 0 { 288 return nil, nil 289 } 290 291 var info storepb.ShardInfo 292 if err := json.Unmarshal([]byte(data[0]), &info); err != nil { 293 return nil, err 294 } 295 296 return &info, nil 297 } 298 299 func encodeTime(t int64) string { 300 f := float64(t) / 1.0e3 301 return strconv.FormatFloat(f, 'f', -1, 64) 302 } 303 304 func encodeDurationMillis(d int64) string { 305 return strconv.FormatFloat(float64(d)/float64(time.Second/time.Millisecond), 'f', -1, 64) 306 } 307 308 // matchersToStringSlice converts storeMatchers to string slice. 309 func matchersToStringSlice(storeMatchers [][]*labels.Matcher) []string { 310 res := make([]string, 0, len(storeMatchers)) 311 for _, storeMatcher := range storeMatchers { 312 res = append(res, storepb.PromMatchersToString(storeMatcher...)) 313 } 314 return res 315 } 316 317 func encodeShardInfo(info *storepb.ShardInfo) (string, error) { 318 if info == nil { 319 return "", nil 320 } 321 322 data, err := json.Marshal(info) 323 if err != nil { 324 return "", err 325 } 326 327 return string(data), nil 328 }