github.com/m3db/m3@v1.5.0/src/query/api/v1/middleware/request_classification.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 middleware 22 23 import ( 24 "net/http" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/prometheus/prometheus/promql/parser" 30 "github.com/uber-go/tally" 31 "go.uber.org/zap" 32 33 "github.com/m3db/m3/src/cmd/services/m3query/config" 34 "github.com/m3db/m3/src/query/api/v1/handler/prometheus" 35 "github.com/m3db/m3/src/query/api/v1/route" 36 "github.com/m3db/m3/src/x/headers" 37 ) 38 39 const ( 40 // promDefaultLookback is the default lookback in Prometheus, that affects 41 // the actual range it fetches datapoints from. 42 promDefaultLookback = time.Minute * 5 43 ) 44 45 type classificationMetrics struct { 46 classifiedResult tally.Counter 47 classifiedDuration tally.Counter 48 notClassifiable tally.Counter 49 badParams tally.Counter 50 error tally.Counter 51 } 52 53 func newClassificationMetrics(scope tally.Scope) *classificationMetrics { 54 buildCounter := func(status string) tally.Counter { 55 return scope.Tagged(map[string]string{"status": status}).Counter("count") 56 } 57 58 return &classificationMetrics{ 59 classifiedResult: buildCounter("classified_result"), 60 classifiedDuration: buildCounter("classified_duration"), 61 notClassifiable: buildCounter("not_classifiable"), 62 badParams: buildCounter("bad_params"), 63 error: buildCounter("error"), 64 } 65 } 66 67 func retrieveQueryRange(expr parser.Node, rangeSoFar time.Duration) time.Duration { 68 var ( 69 queryRange = rangeSoFar 70 useLookback = rangeSoFar == 0 71 ) 72 73 parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { 74 switch n := node.(type) { 75 case *parser.SubqueryExpr: 76 useLookback = false 77 d := retrieveQueryRange(n.Expr, n.Range+n.OriginalOffset+rangeSoFar) 78 if d > queryRange { 79 queryRange = d 80 } 81 case *parser.MatrixSelector: 82 useLookback = false 83 // nolint 84 v := n.VectorSelector.(*parser.VectorSelector) 85 if d := v.OriginalOffset + n.Range + rangeSoFar; d > queryRange { 86 queryRange = d 87 } 88 case *parser.VectorSelector: 89 if d := n.OriginalOffset + rangeSoFar; d > queryRange { 90 queryRange = d 91 } 92 } 93 94 return nil 95 }) 96 97 if useLookback { 98 // NB: no range provided in any query selectors; use lookback time. 99 queryRange += promDefaultLookback 100 } 101 102 return queryRange 103 } 104 105 type classificationTags map[string]string 106 107 const ( 108 resultsClassification = "results_bucket" 109 durationClassification = "duration_bucket" 110 unclassified = "unclassified" 111 ) 112 113 func newClassificationTags() classificationTags { 114 return map[string]string{ 115 resultsClassification: unclassified, 116 durationClassification: unclassified, 117 } 118 } 119 120 // classifyRequest determines the bucket(s) that important request metrics 121 // will fall into. Supported request metrics at this time are the number of results 122 // fetched and the time range of the request. The number of buckets and their values 123 // can be configured by the client. Currently, only query (i.e. query and query_range) 124 // and label (i.e. label names and label values) endpoints are supported. 125 func classifyRequest( 126 w http.ResponseWriter, 127 r *http.Request, 128 metrics *classificationMetrics, 129 opts Options, 130 requestStart time.Time, 131 path string, 132 ) classificationTags { 133 var ( 134 cfg = opts.Metrics.Config 135 tags = newClassificationTags() 136 ) 137 138 // NB(nate): have to check for /label/*/values this way since the URL is templated 139 labelValues := strings.Contains(path, "/label/") && strings.Contains(path, "/values") 140 if path == route.QueryRangeURL || path == route.QueryURL { 141 if opts.Metrics.ParseQueryParams == nil { 142 metrics.notClassifiable.Inc(1) 143 return tags 144 } 145 146 params, err := opts.Metrics.ParseQueryParams(r, requestStart) 147 if err != nil || params.Query == "" { 148 metrics.badParams.Inc(1) 149 return tags 150 } 151 152 if tags, err = classifyForQueryEndpoints( 153 cfg.QueryEndpointsClassification, params, w, metrics, 154 ); err != nil { 155 opts.InstrumentOpts.Logger().Error( 156 "failed to classify query endpoint request", zap.Error(err), 157 ) 158 metrics.error.Inc(1) 159 return tags 160 } 161 } else if path == route.LabelNamesURL || labelValues { 162 start, end, err := prometheus.ParseStartAndEnd(r, opts.Metrics.ParseOptions) 163 if err != nil { 164 metrics.badParams.Inc(1) 165 return tags 166 } 167 if tags, err = classifyForLabelEndpoints( 168 cfg.LabelEndpointsClassification, start, end, w, metrics, 169 ); err != nil { 170 opts.InstrumentOpts.Logger().Error( 171 "failed to classify label endpoint request", zap.Error(err), 172 ) 173 metrics.error.Inc(1) 174 return tags 175 } 176 } 177 178 return tags 179 } 180 181 func classifyForQueryEndpoints( 182 cfg config.QueryClassificationConfig, 183 params QueryParams, 184 w http.ResponseWriter, 185 metrics *classificationMetrics, 186 ) (classificationTags, error) { 187 resultsBuckets := cfg.ResultsBuckets 188 tags := newClassificationTags() 189 if len(resultsBuckets) > 0 { 190 fetchedCount := w.Header().Get(headers.FetchedSeriesCount) 191 if fetchedCount == "" { 192 fetchedCount = "0" 193 } 194 fetched, err := strconv.Atoi(fetchedCount) 195 if err != nil { 196 return newClassificationTags(), err 197 } 198 199 tags[resultsClassification] = strconv.Itoa(resultsBuckets[0]) 200 for _, bucket := range resultsBuckets { 201 if fetched >= bucket { 202 tags[resultsClassification] = strconv.Itoa(bucket) 203 } 204 } 205 metrics.classifiedResult.Inc(1) 206 } 207 208 durationBuckets := cfg.DurationBuckets 209 if len(durationBuckets) > 0 { 210 expr, err := parser.ParseExpr(params.Query) 211 if err != nil { 212 return newClassificationTags(), err 213 } 214 215 queryRange := retrieveQueryRange(expr, 0) 216 duration := params.Range() 217 totalDuration := duration + queryRange 218 219 tags[durationClassification] = durationBuckets[0].String() 220 for _, bucket := range durationBuckets { 221 if totalDuration >= bucket { 222 tags[durationClassification] = bucket.String() 223 } 224 } 225 metrics.classifiedDuration.Inc(1) 226 } 227 228 return tags, nil 229 } 230 231 func classifyForLabelEndpoints( 232 cfg config.QueryClassificationConfig, 233 start time.Time, 234 end time.Time, 235 w http.ResponseWriter, 236 metrics *classificationMetrics, 237 ) (classificationTags, error) { 238 resultsBuckets := cfg.ResultsBuckets 239 tags := newClassificationTags() 240 if len(resultsBuckets) > 0 { 241 fetchedCount := w.Header().Get(headers.FetchedMetadataCount) 242 if fetchedCount == "" { 243 fetchedCount = "0" 244 } 245 fetched, err := strconv.Atoi(fetchedCount) 246 if err != nil { 247 return newClassificationTags(), err 248 } 249 250 tags[resultsClassification] = strconv.Itoa(resultsBuckets[0]) 251 for _, bucket := range resultsBuckets { 252 if fetched >= bucket { 253 tags[resultsClassification] = strconv.Itoa(bucket) 254 } 255 } 256 metrics.classifiedResult.Inc(1) 257 } 258 259 durationBuckets := cfg.DurationBuckets 260 if len(durationBuckets) > 0 { 261 duration := end.Sub(start) 262 tags[durationClassification] = durationBuckets[0].String() 263 for _, bucket := range durationBuckets { 264 if duration >= bucket { 265 tags[durationClassification] = bucket.String() 266 } 267 } 268 metrics.classifiedDuration.Inc(1) 269 } 270 271 return tags, nil 272 }