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  }