github.com/go-graphite/carbonapi@v0.17.0/expr/functions/timeShiftByMetric/function.go (about)

     1  package timeShiftByMetric
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/ansel1/merry"
    10  
    11  	"github.com/go-graphite/carbonapi/expr/helper"
    12  	"github.com/go-graphite/carbonapi/expr/interfaces"
    13  	"github.com/go-graphite/carbonapi/expr/types"
    14  	"github.com/go-graphite/carbonapi/pkg/parser"
    15  )
    16  
    17  type offsetByVersion map[string]int64
    18  
    19  type timeShiftByMetric struct{}
    20  
    21  func GetOrder() interfaces.Order {
    22  	return interfaces.Any
    23  }
    24  
    25  func New(configFile string) []interfaces.FunctionMetadata {
    26  	return []interfaces.FunctionMetadata{{
    27  		F:    &timeShiftByMetric{},
    28  		Name: "timeShiftByMetric",
    29  	}}
    30  }
    31  
    32  // timeShiftByMetric(seriesList, markSource, versionRankIndex)
    33  func (f *timeShiftByMetric) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) (resultData []*types.MetricData, resultError error) {
    34  	params, err := f.extractCallParams(ctx, eval, e, from, until, values)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	latestMarks, err := f.locateLatestMarks(params)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	offsets := f.calculateOffsets(params, latestMarks)
    45  
    46  	result := f.applyShift(params, offsets)
    47  
    48  	return result, nil
    49  }
    50  
    51  // applyShift shifts timeline of those metrics which major version is less than top major version
    52  func (f *timeShiftByMetric) applyShift(params *callParams, offsets offsetByVersion) []*types.MetricData {
    53  	result := make([]*types.MetricData, 0, len(params.metrics))
    54  	for _, metric := range params.metrics {
    55  		offsetIsSet := false
    56  		offset := int64(0)
    57  		var possibleVersion string
    58  
    59  		name := metric.Tags["name"]
    60  		nameSplit := strings.Split(name, ".")
    61  
    62  		// make sure that there is desired rank at all
    63  		if params.versionRank >= len(nameSplit) {
    64  			continue
    65  		}
    66  		possibleVersion = nameSplit[params.versionRank]
    67  
    68  		if possibleOffset, ok := offsets[possibleVersion]; !ok {
    69  			for key, value := range offsets {
    70  				if strings.HasPrefix(key, possibleVersion) {
    71  					offset = value
    72  					offsetIsSet = true
    73  					offsets[possibleVersion] = value
    74  				}
    75  			}
    76  		} else {
    77  			offset = possibleOffset
    78  			offsetIsSet = true
    79  		}
    80  
    81  		// checking if it is some version after all, otherwise this series will be omitted
    82  		if offsetIsSet {
    83  			r := metric.CopyLinkTags()
    84  			r.Name = "timeShiftByMetric(" + r.Name + ")"
    85  			r.StopTime += offset
    86  			r.StartTime += offset
    87  
    88  			result = append(result, r)
    89  		}
    90  	}
    91  
    92  	return result
    93  }
    94  
    95  func (f *timeShiftByMetric) calculateOffsets(params *callParams, versions versionInfos) offsetByVersion {
    96  	result := make(offsetByVersion)
    97  	topPosition := versions[0].position
    98  
    99  	for _, version := range versions {
   100  		result[version.mark] = int64(topPosition-version.position) * params.stepTime
   101  	}
   102  
   103  	return result
   104  }
   105  
   106  // extractCallParams (preliminarily) validates and extracts parameters of timeShiftByMetric's call as structure
   107  func (f *timeShiftByMetric) extractCallParams(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) (*callParams, error) {
   108  	metrics, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	marks, err := helper.GetSeriesArg(ctx, eval, e.Arg(1), from, until, values)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	versionRank, err := e.GetIntArg(2)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	// validating data sets: both metrics and marks must have at least 2 series each
   124  	// also, all IsAbsent and Values lengths must be equal to each other
   125  	pointsQty := -1
   126  	stepTime := int64(-1)
   127  	var dataSets map[string][]*types.MetricData = map[string][]*types.MetricData{
   128  		"marks":   marks,
   129  		"metrics": metrics,
   130  	}
   131  	for name, dataSet := range dataSets {
   132  		if len(dataSet) < 2 {
   133  			return nil, merry.WithMessagef(errTooFewDatasets, "bad data: need at least 2 %s data sets to process, got %d", name, len(dataSet))
   134  		}
   135  
   136  		for _, series := range dataSet {
   137  			if pointsQty == -1 {
   138  				pointsQty = len(series.Values)
   139  				if pointsQty == 0 {
   140  					return nil, merry.WithMessagef(errEmptySeries, "bad data: empty series %s", series.Name)
   141  				}
   142  			} else if pointsQty != len(series.Values) {
   143  				return nil, merry.WithMessagef(errSeriesLengthMismatch, "bad data: length of Values for series %s differs from others", series.Name)
   144  			}
   145  
   146  			if stepTime == -1 {
   147  				stepTime = series.StepTime
   148  			}
   149  		}
   150  	}
   151  
   152  	result := &callParams{
   153  		metrics:     metrics,
   154  		marks:       marks,
   155  		versionRank: versionRank,
   156  		pointsQty:   pointsQty,
   157  		stepTime:    stepTime,
   158  	}
   159  	return result, nil
   160  }
   161  
   162  var reLocateMark *regexp.Regexp = regexp.MustCompile(`(\d+)_(\d+)`)
   163  
   164  // locateLatestMarks gets the series with marks those look like "65_4"
   165  // and looks for the latest ones by _major_ versions
   166  // e.g. among set [63_0, 64_0, 64_1, 64_2, 65_0, 65_1] it locates 63_0, 64_4 and 65_1
   167  // returns located elements
   168  func (f *timeShiftByMetric) locateLatestMarks(params *callParams) (versionInfos, error) {
   169  
   170  	versions := make(versionInfos, 0, len(params.marks))
   171  
   172  	// noinspection SpellCheckingInspection
   173  	for _, mark := range params.marks {
   174  		markSplit := strings.Split(mark.Tags["name"], ".")
   175  		markVersion := markSplit[len(markSplit)-1]
   176  
   177  		// for mark that matches pattern (\d+)_(\d+), this should return slice of 3 strings exactly
   178  		submatch := reLocateMark.FindStringSubmatch(markVersion)
   179  		if len(submatch) != 3 {
   180  			continue
   181  		}
   182  
   183  		position := -1
   184  		for i := params.pointsQty - 1; i >= 0; i-- {
   185  			if !math.IsNaN(mark.Values[i]) {
   186  				position = i
   187  				break
   188  			}
   189  		}
   190  
   191  		if position == -1 {
   192  			// weird, but mark series has no data in it - skipping
   193  			continue
   194  		}
   195  		// collecting all marks found
   196  		versions = append(versions, versionInfo{
   197  			mark:         markVersion,
   198  			position:     position,
   199  			versionMajor: mustAtoi(submatch[1]),
   200  			versionMinor: mustAtoi(submatch[2]),
   201  		})
   202  	}
   203  
   204  	// obtain top versions for each major version
   205  	result := versions.HighestVersions()
   206  	if len(result) < 2 {
   207  		return nil, merry.WithMessagef(errLessThan2Marks, "bad data: could not find 2 marks, only %d found", len(result))
   208  	} else {
   209  		return result, nil
   210  	}
   211  }
   212  
   213  func (f *timeShiftByMetric) Description() map[string]types.FunctionDescription {
   214  	return map[string]types.FunctionDescription{
   215  		"timeShiftByMetric": types.FunctionDescription{
   216  			Description: "Takes a seriesList with wildcard in versionRankIndex rank and applies shift to the closest version from markSource\n\n.. code-block:: none\n\n  &target=timeShiftByMetric(carbon.agents.graphite.creates)",
   217  			Function:    "timeShiftByMetric(seriesList, markSource, versionRankIndex)",
   218  			Group:       "Transform",
   219  			Module:      "graphite.render.functions",
   220  			Name:        "timeShiftByMetric",
   221  			Params: []types.FunctionParam{
   222  				types.FunctionParam{
   223  					Name:     "seriesList",
   224  					Required: true,
   225  					Type:     types.SeriesList,
   226  				},
   227  				types.FunctionParam{
   228  					Name:     "markSource",
   229  					Required: true,
   230  					Type:     types.SeriesList,
   231  				},
   232  				types.FunctionParam{
   233  					Name:     "versionRankIndex",
   234  					Required: true,
   235  					Type:     types.Integer,
   236  				},
   237  			},
   238  			NameChange:   true, // name changed
   239  			ValuesChange: true, // values changed
   240  		},
   241  	}
   242  }