github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/segment/results/mresults/metricresults.go (about)

     1  /*
     2  Copyright 2023.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package mresults
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  
    26  	pql "github.com/influxdata/promql/v2"
    27  	"github.com/influxdata/promql/v2/pkg/labels"
    28  	tsidtracker "github.com/siglens/siglens/pkg/segment/results/mresults/tsid"
    29  	"github.com/siglens/siglens/pkg/segment/structs"
    30  	segutils "github.com/siglens/siglens/pkg/segment/utils"
    31  	"github.com/siglens/siglens/pkg/utils"
    32  	"github.com/valyala/bytebufferpool"
    33  )
    34  
    35  type bucketState uint8
    36  
    37  const (
    38  	SERIES_READING bucketState = iota
    39  	DOWNSAMPLING
    40  	AGGREGATED
    41  )
    42  
    43  /*
    44  Represents the results for a running query
    45  
    46  Depending on the State the stored information is different:
    47    - SERIES_READING: maps a tsid to the all raw read dp & downsampled times
    48    - DOWNSAMPLING: maps a group to all downsampled series results.  This downsampled series may have repeated timestamps from different tsids
    49    - AGGREGATING: maps a groupid to the resulting aggregated values
    50  */
    51  type MetricsResult struct {
    52  	// maps tsid to the raw read series (with downsampled timestamp)
    53  	AllSeries map[uint64]*Series
    54  
    55  	// maps groupid to all raw downsampled series. This downsampled series may have repeated timestamps from different tsids
    56  	DsResults map[string]*DownsampleSeries
    57  	// maps groupid to a map of ts to value. This aggregates DsResults based on the aggregation function
    58  	Results map[string]map[uint32]float64
    59  
    60  	State bucketState
    61  
    62  	rwLock  *sync.RWMutex
    63  	ErrList []error
    64  }
    65  
    66  /*
    67  Inits a metricsresults holder
    68  
    69  TODO: depending on metrics query, have different cases on how to resolve dps
    70  */
    71  func InitMetricResults(mQuery *structs.MetricsQuery, qid uint64) *MetricsResult {
    72  	return &MetricsResult{
    73  		AllSeries: make(map[uint64]*Series),
    74  		rwLock:    &sync.RWMutex{},
    75  		ErrList:   make([]error, 0),
    76  	}
    77  }
    78  
    79  /*
    80  Add a given series for the tsid and group information
    81  
    82  This does not protect againt concurrency. The caller is responsible for coordination
    83  */
    84  func (r *MetricsResult) AddSeries(series *Series, tsid uint64, tsGroupId *bytebufferpool.ByteBuffer) {
    85  	currSeries, ok := r.AllSeries[tsid]
    86  	if !ok {
    87  		currSeries = series
    88  		r.AllSeries[tsid] = series
    89  		return
    90  	}
    91  	currSeries.Merge(series)
    92  }
    93  
    94  /*
    95  Return the number of final series in the metrics result
    96  */
    97  func (r *MetricsResult) GetNumSeries() uint64 {
    98  	return uint64(len(r.Results))
    99  }
   100  
   101  /*
   102  Downsample all series
   103  
   104  Insert into r.DsResults, mapping a groupid to all RunningDownsample Series entries
   105  This means that a single tsid will have unique timetamps, but those timestamps can exist for another tsid
   106  */
   107  func (r *MetricsResult) DownsampleResults(ds structs.Downsampler, parallelism int) []error {
   108  
   109  	// maps a group id to the running downsampled series
   110  	allDSSeries := make(map[string]*DownsampleSeries, len(r.AllSeries))
   111  
   112  	var idx int
   113  	wg := &sync.WaitGroup{}
   114  	dataLock := &sync.Mutex{}
   115  
   116  	errorLock := &sync.Mutex{}
   117  	errors := make([]error, 0)
   118  
   119  	for _, series := range r.AllSeries {
   120  		wg.Add(1)
   121  
   122  		go func(s *Series) {
   123  			defer wg.Done()
   124  
   125  			dsSeries, err := s.Downsample(ds)
   126  			if err != nil {
   127  				errorLock.Lock()
   128  				errors = append(errors, err)
   129  				errorLock.Unlock()
   130  				return
   131  			}
   132  
   133  			grp := s.grpID.String()
   134  
   135  			dataLock.Lock()
   136  			allDS, ok := allDSSeries[grp]
   137  			if !ok {
   138  				allDSSeries[grp] = dsSeries
   139  			} else {
   140  				allDS.Merge(dsSeries)
   141  			}
   142  			dataLock.Unlock()
   143  		}(series)
   144  		idx++
   145  		if idx%parallelism == 0 {
   146  			wg.Wait()
   147  		}
   148  	}
   149  	wg.Wait()
   150  	r.DsResults = allDSSeries
   151  	r.State = DOWNSAMPLING
   152  	r.AllSeries = nil
   153  
   154  	if len(errors) > 0 {
   155  		return errors
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  /*
   162  Aggregate results for series sharing a groupid
   163  
   164  Internally, this will store the final aggregated results
   165  e.g. will store avg instead of running sum&count
   166  */
   167  func (r *MetricsResult) AggregateResults(parallelism int) []error {
   168  	if r.State != DOWNSAMPLING {
   169  		return []error{errors.New("results is not in downsampling state")}
   170  	}
   171  
   172  	r.Results = make(map[string]map[uint32]float64)
   173  	lock := &sync.Mutex{}
   174  	wg := &sync.WaitGroup{}
   175  
   176  	errorLock := &sync.Mutex{}
   177  	errors := make([]error, 0)
   178  
   179  	var idx int
   180  	for grpID, runningDS := range r.DsResults {
   181  		wg.Add(1)
   182  		go func(grp string, ds *DownsampleSeries) {
   183  			defer wg.Done()
   184  
   185  			grpVal, err := ds.Aggregate()
   186  			if err != nil {
   187  				errorLock.Lock()
   188  				errors = append(errors, err)
   189  				errorLock.Unlock()
   190  				return
   191  			}
   192  
   193  			lock.Lock()
   194  			r.Results[grp] = grpVal
   195  			lock.Unlock()
   196  		}(grpID, runningDS)
   197  		idx++
   198  		if idx%parallelism == 0 {
   199  			wg.Wait()
   200  		}
   201  	}
   202  
   203  	wg.Wait()
   204  	r.DsResults = nil
   205  	r.State = AGGREGATED
   206  
   207  	if len(errors) > 0 {
   208  		return errors
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  /*
   215  Apply range function to results for series sharing a groupid.
   216  */
   217  func (r *MetricsResult) ApplyRangeFunctionsToResults(parallelism int, function segutils.RangeFunctions) error {
   218  
   219  	lock := &sync.Mutex{}
   220  	wg := &sync.WaitGroup{}
   221  	errList := []error{} // Thread-safe list of errors
   222  
   223  	var idx int
   224  	for grpID, timeSeries := range r.Results {
   225  		wg.Add(1)
   226  		go func(grp string, ts map[uint32]float64, function segutils.RangeFunctions) {
   227  			defer wg.Done()
   228  			grpVal, err := ApplyRangeFunction(ts, function)
   229  			if err != nil {
   230  				lock.Lock()
   231  				errList = append(errList, err)
   232  				lock.Unlock()
   233  				return
   234  			}
   235  			lock.Lock()
   236  			r.Results[grp] = grpVal
   237  			lock.Unlock()
   238  		}(grpID, timeSeries, function)
   239  		idx++
   240  		if idx%parallelism == 0 {
   241  			wg.Wait()
   242  		}
   243  	}
   244  
   245  	wg.Wait()
   246  	r.DsResults = nil
   247  
   248  	return nil
   249  }
   250  
   251  func (r *MetricsResult) AddError(err error) {
   252  	r.rwLock.Lock()
   253  	r.ErrList = append(r.ErrList, err)
   254  	r.rwLock.Unlock()
   255  }
   256  
   257  /*
   258  Merge series with global results
   259  
   260  This can only merge results if both structs are in SERIES_READING state
   261  */
   262  func (r *MetricsResult) Merge(localRes *MetricsResult) error {
   263  	if r.State != SERIES_READING || localRes.State != SERIES_READING {
   264  		return errors.New("merged results are not in serires reading state")
   265  	}
   266  	r.rwLock.Lock()
   267  	defer r.rwLock.Unlock()
   268  	r.ErrList = append(r.ErrList, localRes.ErrList...)
   269  	for tsid, series := range localRes.AllSeries {
   270  		currSeries, ok := r.AllSeries[tsid]
   271  		if !ok {
   272  			currSeries = series
   273  			r.AllSeries[tsid] = series
   274  			continue
   275  		}
   276  		currSeries.Merge(series)
   277  	}
   278  	return nil
   279  }
   280  
   281  func (r *MetricsResult) GetOTSDBResults(mQuery *structs.MetricsQuery) ([]*structs.MetricsQueryResponse, error) {
   282  	if r.State != AGGREGATED {
   283  		return nil, errors.New("results is not in aggregated state")
   284  	}
   285  	retVal := make([]*structs.MetricsQueryResponse, len(r.Results))
   286  
   287  	idx := 0
   288  	uniqueTagKeys := make(map[string]bool)
   289  	tagKeys := make([]string, 0)
   290  	for _, tag := range mQuery.TagsFilters {
   291  		if _, ok := uniqueTagKeys[tag.TagKey]; !ok {
   292  			uniqueTagKeys[tag.TagKey] = true
   293  			tagKeys = append(tagKeys, tag.TagKey)
   294  		}
   295  	}
   296  
   297  	for grpId, results := range r.Results {
   298  		tags := make(map[string]string)
   299  		tagValues := strings.Split(grpId, tsidtracker.TAG_VALUE_DELIMITER_STR)
   300  		if len(tagKeys) != len(tagValues)-1 {
   301  			err := errors.New("GetResults: the length of tag key and tag value pair must match")
   302  			return nil, err
   303  		}
   304  
   305  		for index, val := range tagValues[:len(tagValues)-1] {
   306  			tags[tagKeys[index]] = val
   307  		}
   308  		retVal[idx] = &structs.MetricsQueryResponse{
   309  			MetricName: mQuery.MetricName,
   310  			Tags:       tags,
   311  			Dps:        results,
   312  		}
   313  		idx++
   314  	}
   315  	return retVal, nil
   316  }
   317  func (r *MetricsResult) GetResultsPromQl(mQuery *structs.MetricsQuery, pqlQuerytype pql.ValueType) ([]*structs.MetricsQueryResponsePromQl, error) {
   318  	if r.State != AGGREGATED {
   319  		return nil, errors.New("results is not in aggregated state")
   320  	}
   321  	var pqldata structs.Data
   322  	var series pql.Series
   323  	var label structs.Label
   324  
   325  	retVal := make([]*structs.MetricsQueryResponsePromQl, len(r.Results))
   326  	idx := 0
   327  	uniqueTagKeys := make(map[string]bool)
   328  	tagKeys := make([]string, 0)
   329  	for _, tag := range mQuery.TagsFilters {
   330  		if _, ok := uniqueTagKeys[tag.TagKey]; !ok {
   331  			uniqueTagKeys[tag.TagKey] = true
   332  			tagKeys = append(tagKeys, tag.TagKey)
   333  		}
   334  	}
   335  	switch pqlQuerytype {
   336  	case pql.ValueTypeVector:
   337  		pqldata.ResultType = pql.ValueType("vector")
   338  		for grpId, results := range r.Results {
   339  			tags := make(map[string]string)
   340  			tagValues := strings.Split(grpId, tsidtracker.TAG_VALUE_DELIMITER_STR)
   341  
   342  			if len(tagKeys) != len(tagValues)-1 {
   343  				err := errors.New("GetResultsPromQl: the length of tag key and tag value pair must match")
   344  				return nil, err
   345  			}
   346  			for index, val := range tagValues[:len(tagValues)-1] {
   347  				tags[tagKeys[index]] = val
   348  				label.Name = tagKeys[index]
   349  				label.Value = val
   350  				series.Metric = append(series.Metric, labels.Label(label))
   351  			}
   352  			label.Name = "__name__"
   353  			label.Value = mQuery.MetricName
   354  			series.Metric = append(series.Metric, labels.Label(label))
   355  			for k, v := range results {
   356  				var point pql.Point
   357  				point.T = int64(k)
   358  				point.V = v
   359  				series.Points = append(series.Points, point)
   360  			}
   361  			pqldata.Result = append(pqldata.Result, series)
   362  
   363  			retVal[idx] = &structs.MetricsQueryResponsePromQl{
   364  				Status: "success",
   365  				Data:   pqldata,
   366  			}
   367  			pqldata.Result = nil
   368  			series = pql.Series{}
   369  			idx++
   370  		}
   371  	default:
   372  		return retVal, fmt.Errorf("GetResultsPromQl: Unsupported PromQL query result type")
   373  	}
   374  	return retVal, nil
   375  }
   376  
   377  func (r *MetricsResult) GetResultsPromQlForUi(mQuery *structs.MetricsQuery, pqlQuerytype pql.ValueType, startTime, endTime, interval uint32) (utils.MetricsStatsResponseInfo, error) {
   378  	var httpResp utils.MetricsStatsResponseInfo
   379  	httpResp.AggStats = make(map[string]map[string]interface{})
   380  	if r.State != AGGREGATED {
   381  		return utils.MetricsStatsResponseInfo{}, errors.New("results is not in aggregated state")
   382  	}
   383  	uniqueTagKeys := make(map[string]bool)
   384  	tagKeys := make([]string, 0)
   385  	for _, tag := range mQuery.TagsFilters {
   386  		if _, ok := uniqueTagKeys[tag.TagKey]; !ok {
   387  			uniqueTagKeys[tag.TagKey] = true
   388  			tagKeys = append(tagKeys, tag.TagKey)
   389  		}
   390  	}
   391  	for grpId, results := range r.Results {
   392  		tagValues := strings.Split(grpId, tsidtracker.TAG_VALUE_DELIMITER_STR)
   393  		if len(tagKeys) != len(tagValues)-1 { // Subtract 1 because grpId has a delimiter after the last value
   394  			err := errors.New("GetResults: the length of tag key and tag value pair must match")
   395  			return httpResp, err
   396  		}
   397  		groupId := mQuery.MetricName + "{"
   398  		for index, val := range tagValues[:len(tagValues)-1] {
   399  			groupId += fmt.Sprintf("%v=\"%v\",", tagKeys[index], val)
   400  		}
   401  		if last := len(groupId) - 1; last >= 0 && groupId[last] == ',' {
   402  			groupId = groupId[:last]
   403  		}
   404  		groupId += "}"
   405  		httpResp.AggStats[groupId] = make(map[string]interface{}, 1)
   406  		for ts, v := range results {
   407  			temp := time.Unix(int64(ts), 0)
   408  
   409  			bucketInterval := temp.Format("2006-01-02T15:04")
   410  
   411  			httpResp.AggStats[groupId][bucketInterval] = v
   412  		}
   413  	}
   414  	endEpoch := time.Unix(int64(endTime), 0)
   415  	startEpoch := time.Unix(int64(startTime), 0)
   416  	runningTs := startEpoch
   417  	var startDuration, endDuration int64
   418  
   419  	switch pqlQuerytype {
   420  	case pql.ValueTypeVector:
   421  		for groupId := range httpResp.AggStats {
   422  			startDuration = (startEpoch.UnixMilli() / segutils.MS_IN_MIN) * segutils.MS_IN_MIN
   423  			endDuration = (endEpoch.UnixMilli() / segutils.MS_IN_MIN) * segutils.MS_IN_MIN
   424  			runningTs = startEpoch
   425  
   426  			for endDuration > startDuration+segutils.MS_IN_MIN {
   427  				runningTs = runningTs.Add(1 * time.Minute)
   428  				startDuration = startDuration + segutils.MS_IN_MIN
   429  				bucketInterval := runningTs.Format("2006-01-02T15:04")
   430  				if _, ok := httpResp.AggStats[groupId][bucketInterval]; !ok {
   431  					httpResp.AggStats[groupId][bucketInterval] = 0
   432  				}
   433  			}
   434  
   435  		}
   436  	default:
   437  		return httpResp, fmt.Errorf("GetResultsPromQl: Unsupported PromQL query result type")
   438  	}
   439  
   440  	return httpResp, nil
   441  }