github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/cmd/services/m3comparator/main/querier.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 main 22 23 import ( 24 "bytes" 25 "context" 26 "fmt" 27 "math" 28 "math/rand" 29 "regexp" 30 "strconv" 31 "strings" 32 "sync" 33 "time" 34 35 "github.com/m3db/m3/src/cmd/services/m3comparator/main/parser" 36 "github.com/m3db/m3/src/dbnode/encoding" 37 "github.com/m3db/m3/src/dbnode/ts" 38 "github.com/m3db/m3/src/query/block" 39 "github.com/m3db/m3/src/query/models" 40 "github.com/m3db/m3/src/query/storage" 41 "github.com/m3db/m3/src/query/storage/m3" 42 "github.com/m3db/m3/src/query/storage/m3/consolidators" 43 xtime "github.com/m3db/m3/src/x/time" 44 45 "github.com/prometheus/common/model" 46 ) 47 48 var _ m3.Querier = (*querier)(nil) 49 50 type querier struct { 51 iteratorOpts parser.Options 52 handler seriesLoadHandler 53 blockSize time.Duration 54 defaultResolution time.Duration 55 histogramBucketCount uint 56 sync.Mutex 57 } 58 59 func newQuerier( 60 iteratorOpts parser.Options, 61 handler seriesLoadHandler, 62 blockSize time.Duration, 63 defaultResolution time.Duration, 64 histogramBucketCount uint, 65 ) (*querier, error) { 66 if blockSize <= 0 { 67 return nil, fmt.Errorf("blockSize must be positive, got %d", blockSize) 68 } 69 if defaultResolution <= 0 { 70 return nil, fmt.Errorf("defaultResolution must be positive, got %d", defaultResolution) 71 } 72 return &querier{ 73 iteratorOpts: iteratorOpts, 74 handler: handler, 75 blockSize: blockSize, 76 defaultResolution: defaultResolution, 77 histogramBucketCount: histogramBucketCount, 78 }, nil 79 } 80 81 func noop() error { return nil } 82 83 func (q *querier) generateSeriesBlock( 84 start time.Time, 85 resolution time.Duration, 86 integerValues bool, 87 ) parser.Data { 88 numPoints := int(q.blockSize / resolution) 89 dps := make(parser.Data, 0, numPoints) 90 for i := 0; i < numPoints; i++ { 91 stamp := start.Add(resolution * time.Duration(i)) 92 var value float64 93 if integerValues { 94 value = float64(rand.Intn(1000)) 95 } else { 96 value = rand.Float64() 97 } 98 dp := ts.Datapoint{ 99 TimestampNanos: xtime.ToUnixNano(stamp), 100 Value: value, 101 } 102 103 dps = append(dps, dp) 104 } 105 106 return dps 107 } 108 109 func (q *querier) generateSeries( 110 start time.Time, 111 end time.Time, 112 resolution time.Duration, 113 tags parser.Tags, 114 integerValues bool, 115 ) (parser.IngestSeries, error) { 116 numBlocks := int(math.Ceil(float64(end.Sub(start)) / float64(q.blockSize))) 117 if numBlocks == 0 { 118 return parser.IngestSeries{}, fmt.Errorf("comparator querier: no blocks generated") 119 } 120 121 blocks := make([]parser.Data, 0, numBlocks) 122 for i := 0; i < numBlocks; i++ { 123 blocks = append(blocks, q.generateSeriesBlock(start, resolution, integerValues)) 124 start = start.Add(q.blockSize) 125 } 126 127 return parser.IngestSeries{ 128 Datapoints: blocks, 129 Tags: tags, 130 }, nil 131 } 132 133 type seriesGen struct { 134 name string 135 res time.Duration 136 } 137 138 // FetchCompressedResult fetches timeseries data based on a query. 139 func (q *querier) FetchCompressedResult( 140 ctx context.Context, 141 query *storage.FetchQuery, 142 options *storage.FetchOptions, 143 ) (consolidators.SeriesFetchResult, m3.Cleanup, error) { 144 var ( 145 iters encoding.SeriesIterators 146 randomSeries []parser.IngestSeries 147 ignoreFilter bool 148 err error 149 strictMetricsFilter bool 150 ) 151 152 name := q.iteratorOpts.TagOptions.MetricName() 153 for _, matcher := range query.TagMatchers { 154 if bytes.Equal(name, matcher.Name) { 155 156 metricsName := string(matcher.Value) 157 158 // NB: the default behaviour of this querier is to return predefined metrics with random data if no match by 159 // metrics name is found. To force it return an empty result, query the "nonexistent*" metrics. 160 if match, _ := regexp.MatchString("^nonexist[ae]nt", metricsName); match { 161 return consolidators.SeriesFetchResult{}, noop, nil 162 } 163 164 if matcher.Type == models.MatchEqual { 165 strictMetricsFilter = true 166 iters, err = q.handler.getSeriesIterators(metricsName) 167 if err != nil { 168 return consolidators.SeriesFetchResult{}, noop, err 169 } 170 171 break 172 } 173 } 174 } 175 176 if iters == nil && !strictMetricsFilter && len(query.TagMatchers) > 0 { 177 iters, err = q.handler.getSeriesIterators("") 178 if err != nil { 179 return consolidators.SeriesFetchResult{}, noop, err 180 } 181 } 182 183 if iters == nil || iters.Len() == 0 { 184 randomSeries, ignoreFilter, err = q.generateRandomSeries(query) 185 if err != nil { 186 return consolidators.SeriesFetchResult{}, noop, err 187 } 188 iters, err = parser.BuildSeriesIterators( 189 randomSeries, xtime.ToUnixNano(query.Start), q.blockSize, q.iteratorOpts) 190 if err != nil { 191 return consolidators.SeriesFetchResult{}, noop, err 192 } 193 } 194 195 if !ignoreFilter { 196 filteredIters := filter(iters, query.TagMatchers) 197 198 cleanup := func() error { 199 iters.Close() 200 return nil 201 } 202 203 result, err := consolidators.NewSeriesFetchResult( 204 filteredIters, nil, block.NewResultMetadata()) 205 return result, cleanup, err 206 } 207 208 cleanup := func() error { 209 iters.Close() 210 return nil 211 } 212 213 result, err := consolidators.NewSeriesFetchResult( 214 iters, nil, block.NewResultMetadata()) 215 return result, cleanup, err 216 } 217 218 func (q *querier) generateRandomSeries( 219 query *storage.FetchQuery, 220 ) (series []parser.IngestSeries, ignoreFilter bool, err error) { 221 var ( 222 start = query.Start.Truncate(q.blockSize) 223 end = query.End.Truncate(q.blockSize).Add(q.blockSize) 224 ) 225 226 metricNameTag := q.iteratorOpts.TagOptions.MetricName() 227 for _, matcher := range query.TagMatchers { 228 if bytes.Equal(metricNameTag, matcher.Name) { 229 if matched, _ := regexp.Match(`^multi_\d+$`, matcher.Value); matched { 230 series, err = q.generateMultiSeriesMetrics(string(matcher.Value), start, end) 231 return 232 } 233 if matched, _ := regexp.Match(`^histogram_\d+_bucket$`, matcher.Value); matched { 234 series, err = q.generateHistogramMetrics(string(matcher.Value), start, end) 235 return 236 } 237 } 238 } 239 240 ignoreFilter = true 241 series, err = q.generateSingleSeriesMetrics(query, start, end) 242 return 243 } 244 245 func (q *querier) generateSingleSeriesMetrics( 246 query *storage.FetchQuery, 247 start time.Time, 248 end time.Time, 249 ) ([]parser.IngestSeries, error) { 250 var ( 251 gens = []seriesGen{ 252 {"foo", time.Second}, 253 {"bar", time.Second * 15}, 254 {"quail", time.Minute}, 255 } 256 257 actualGens []seriesGen 258 ) 259 260 unlock := q.lockAndSeed(start) 261 defer unlock() 262 263 metricNameTag := q.iteratorOpts.TagOptions.MetricName() 264 for _, matcher := range query.TagMatchers { 265 // filter if name, otherwise return all. 266 if bytes.Equal(metricNameTag, matcher.Name) { 267 value := string(matcher.Value) 268 for _, gen := range gens { 269 if value == gen.name { 270 actualGens = append(actualGens, gen) 271 break 272 } 273 } 274 275 break 276 } else if "gen" == string(matcher.Name) { 277 cStr := string(matcher.Value) 278 count, err := strconv.Atoi(cStr) 279 if err != nil { 280 return nil, err 281 } 282 283 actualGens = make([]seriesGen, 0, count) 284 for i := 0; i < count; i++ { 285 actualGens = append(actualGens, seriesGen{ 286 res: q.defaultResolution, 287 name: fmt.Sprintf("foo_%d", i), 288 }) 289 } 290 291 break 292 } 293 } 294 295 if len(actualGens) == 0 { 296 actualGens = gens 297 } 298 299 seriesList := make([]parser.IngestSeries, 0, len(actualGens)) 300 for _, gen := range actualGens { 301 tags := parser.Tags{ 302 parser.NewTag(model.MetricNameLabel, gen.name), 303 parser.NewTag("foobar", "qux"), 304 parser.NewTag("name", gen.name), 305 } 306 307 series, err := q.generateSeries(start, end, gen.res, tags, false) 308 if err != nil { 309 return nil, err 310 } 311 312 seriesList = append(seriesList, series) 313 } 314 315 return seriesList, nil 316 } 317 318 func (q *querier) generateMultiSeriesMetrics( 319 metricsName string, 320 start time.Time, 321 end time.Time, 322 ) ([]parser.IngestSeries, error) { 323 suffix := strings.TrimPrefix(metricsName, "multi_") 324 seriesCount, err := strconv.Atoi(suffix) 325 if err != nil { 326 return nil, err 327 } 328 329 unlock := q.lockAndSeed(start) 330 defer unlock() 331 332 seriesList := make([]parser.IngestSeries, 0, seriesCount) 333 for id := 0; id < seriesCount; id++ { 334 tags := multiSeriesTags(metricsName, id) 335 336 series, err := q.generateSeries(start, end, q.defaultResolution, tags, false) 337 if err != nil { 338 return nil, err 339 } 340 341 seriesList = append(seriesList, series) 342 } 343 344 return seriesList, nil 345 } 346 347 func (q *querier) generateHistogramMetrics( 348 metricsName string, 349 start time.Time, 350 end time.Time, 351 ) ([]parser.IngestSeries, error) { 352 suffix := strings.TrimPrefix(metricsName, "histogram_") 353 countStr := strings.TrimSuffix(suffix, "_bucket") 354 seriesCount, err := strconv.Atoi(countStr) 355 if err != nil { 356 return nil, err 357 } 358 359 unlock := q.lockAndSeed(start) 360 defer unlock() 361 362 seriesList := make([]parser.IngestSeries, 0, seriesCount) 363 for id := 0; id < seriesCount; id++ { 364 le := 1.0 365 var previousSeriesBlocks []parser.Data 366 for bucket := uint(0); bucket < q.histogramBucketCount; bucket++ { 367 tags := multiSeriesTags(metricsName, id) 368 leStr := "+Inf" 369 if bucket < q.histogramBucketCount-1 { 370 leStr = strconv.FormatFloat(le, 'f', -1, 64) 371 } 372 leTag := parser.NewTag("le", leStr) 373 tags = append(tags, leTag) 374 le *= 10 375 376 series, err := q.generateSeries(start, end, q.defaultResolution, tags, true) 377 if err != nil { 378 return nil, err 379 } 380 381 for i, prevBlock := range previousSeriesBlocks { 382 for j, prevValue := range prevBlock { 383 series.Datapoints[i][j].Value += prevValue.Value 384 } 385 } 386 387 seriesList = append(seriesList, series) 388 389 previousSeriesBlocks = series.Datapoints 390 } 391 } 392 393 return seriesList, nil 394 } 395 396 func multiSeriesTags(metricsName string, id int) parser.Tags { 397 return parser.Tags{ 398 parser.NewTag(model.MetricNameLabel, metricsName), 399 parser.NewTag("id", strconv.Itoa(id)), 400 parser.NewTag("parity", strconv.Itoa(id%2)), 401 parser.NewTag("const", "x"), 402 } 403 } 404 405 func (q *querier) lockAndSeed(start time.Time) func() { 406 q.Lock() 407 rand.Seed(start.Unix()) 408 409 return q.Unlock 410 } 411 412 // SearchCompressed fetches matching tags based on a query. 413 func (q *querier) SearchCompressed( 414 ctx context.Context, 415 query *storage.FetchQuery, 416 options *storage.FetchOptions, 417 ) (consolidators.TagResult, m3.Cleanup, error) { 418 return consolidators.TagResult{}, noop, fmt.Errorf("not impl") 419 } 420 421 // CompleteTagsCompressed returns autocompleted tag results. 422 func (q *querier) CompleteTagsCompressed( 423 ctx context.Context, 424 query *storage.CompleteTagsQuery, 425 options *storage.FetchOptions, 426 ) (*consolidators.CompleteTagsResult, error) { 427 nameOnly := query.CompleteNameOnly 428 // TODO: take from config. 429 return &consolidators.CompleteTagsResult{ 430 CompleteNameOnly: nameOnly, 431 CompletedTags: []consolidators.CompletedTag{ 432 { 433 Name: []byte("__name__"), 434 Values: [][]byte{[]byte("foo"), []byte("foo"), []byte("quail")}, 435 }, 436 }, 437 }, nil 438 }