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

     1  package slo
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  
     8  	pbv3 "github.com/go-graphite/protocol/carbonapi_v3_pb"
     9  
    10  	"github.com/go-graphite/carbonapi/expr/helper"
    11  	"github.com/go-graphite/carbonapi/expr/interfaces"
    12  	"github.com/go-graphite/carbonapi/expr/types"
    13  	"github.com/go-graphite/carbonapi/pkg/parser"
    14  )
    15  
    16  type slo struct{}
    17  
    18  func GetOrder() interfaces.Order {
    19  	return interfaces.Any
    20  }
    21  
    22  func New(string) []interfaces.FunctionMetadata {
    23  	return []interfaces.FunctionMetadata{
    24  		{F: &slo{}, Name: "slo"},
    25  		{F: &slo{}, Name: "sloErrorBudget"},
    26  	}
    27  }
    28  
    29  func (f *slo) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
    30  	var (
    31  		argsExtended, argsWindowed []*types.MetricData
    32  		bucketSize32               int32
    33  		windowSize                 int64
    34  		delta                      int64
    35  		err                        error
    36  	)
    37  
    38  	// requested data points' window
    39  	argsWindowed, err = helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values)
    40  	if len(argsWindowed) == 0 || err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	bucketSize32, err = e.GetIntervalArg(1, 1)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	bucketSize := int64(bucketSize32)
    49  	intervalStringValue := e.Arg(1).StringValue()
    50  
    51  	// there is an opportunity that requested data points' window is smaller than slo interval
    52  	// e.g.: requesting slo(some.data.series, '30days', above, 0) with window of 6 hours
    53  	// this means that we're gonna need 2 sets of data points:
    54  	// - the first one with range [from, until] - for 6 hours
    55  	// - the second one with range [from - delta, until] - for 30 days
    56  	// the result's time range will be 6 hours anyway
    57  	windowSize = until - from
    58  	if bucketSize > windowSize && !(from == 0 && until == 1) {
    59  		delta = bucketSize - windowSize
    60  		argsExtended, err = helper.GetSeriesArg(ctx, eval, e.Arg(0), from-delta, until, values)
    61  
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  
    66  		if len(argsExtended) != len(argsWindowed) {
    67  			return nil, fmt.Errorf(
    68  				"MetricData quantity differs: there is %d for [%d, %d] and %d for [%d, %d]",
    69  				len(argsExtended), from-delta, until,
    70  				len(argsWindowed), from, until,
    71  			)
    72  		}
    73  	} else {
    74  		argsExtended = argsWindowed
    75  	}
    76  
    77  	value, err := e.GetFloatArg(3)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	valueStr := e.Arg(3).StringValue()
    82  
    83  	methodFoo, methodName, err := f.buildMethod(e, 2, value)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	var (
    89  		isErrorBudget bool
    90  		objective     float64
    91  		objectiveStr  string
    92  	)
    93  
    94  	isErrorBudget = e.Target() == "sloErrorBudget"
    95  	if isErrorBudget {
    96  		objective, err = e.GetFloatArg(4)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  		objectiveStr = e.Arg(4).StringValue()
   101  	}
   102  
   103  	results := make([]*types.MetricData, 0, len(argsWindowed))
   104  
   105  	for i, argWnd := range argsWindowed {
   106  		var (
   107  			argExt     *types.MetricData
   108  			resultName string
   109  		)
   110  
   111  		if isErrorBudget {
   112  			resultName = "sloErrorBudget(" + argWnd.Name + ", " + intervalStringValue + ", " + methodName + ", " + valueStr + ", " + objectiveStr + ")"
   113  		} else {
   114  			resultName = "slo(" + argWnd.Name + ", " + intervalStringValue + ", " + methodName + ", " + valueStr + ")"
   115  		}
   116  
   117  		// buckets qty is calculated based on requested window
   118  		bucketsQty := helper.GetBuckets(argWnd.StartTime, argWnd.StopTime, bucketSize)
   119  
   120  		// result for the given series (argWnd)
   121  		r := &types.MetricData{
   122  			FetchResponse: pbv3.FetchResponse{
   123  				Name:      resultName,
   124  				Values:    make([]float64, 0, bucketsQty+1),
   125  				StepTime:  bucketSize,
   126  				StartTime: argWnd.StartTime,
   127  				StopTime:  argWnd.StartTime + (bucketsQty)*bucketSize,
   128  			},
   129  			Tags: argWnd.Tags,
   130  		}
   131  		// it's ok to place new element to result and modify it later since it's the pointer
   132  		results = append(results, r)
   133  
   134  		// if the granularity of series exceeds bucket size then
   135  		// there are not enough data to do the math
   136  		if argWnd.StepTime > bucketSize {
   137  			for i := int64(0); i < bucketsQty; i++ {
   138  				r.Values = append(r.Values, math.NaN())
   139  			}
   140  			continue
   141  		}
   142  
   143  		// extended data points set will be used for calculating matched items
   144  		argExt = argsExtended[i]
   145  
   146  		// calculate SLO using moving window
   147  		qtyMatched := 0 // bucket matched items quantity
   148  		qtyNotNull := 0 // bucket not-null items quantity
   149  		qtyTotal := 0
   150  
   151  		timeCurrent := argExt.StartTime
   152  		timeStop := argExt.StopTime
   153  		timeBucketStarts := timeCurrent
   154  		timeBucketEnds := timeCurrent + bucketSize
   155  
   156  		// process full buckets
   157  		for i, argValue := range argExt.Values {
   158  			qtyTotal++
   159  
   160  			if !math.IsNaN(argExt.Values[i]) {
   161  				qtyNotNull++
   162  				if methodFoo(argValue) {
   163  					qtyMatched++
   164  				}
   165  			}
   166  
   167  			timeCurrent += argExt.StepTime
   168  			if timeCurrent > timeStop {
   169  				break
   170  			}
   171  
   172  			if timeCurrent >= timeBucketEnds { // the bucket ends
   173  				newIsAbsent, newValue := f.buildDataPoint(qtyMatched, qtyNotNull)
   174  				if isErrorBudget && !newIsAbsent {
   175  					newValue = (newValue - objective) * float64(bucketSize)
   176  				}
   177  
   178  				r.Values = append(r.Values, newValue)
   179  
   180  				// init the next bucket
   181  				qtyMatched = 0
   182  				qtyNotNull = 0
   183  				qtyTotal = 0
   184  				timeBucketStarts = timeCurrent
   185  				timeBucketEnds += bucketSize
   186  			}
   187  		}
   188  
   189  		// partial bucket might remain
   190  		if qtyTotal > 0 {
   191  			newIsAbsent, newValue := f.buildDataPoint(qtyMatched, qtyNotNull)
   192  			if isErrorBudget && !newIsAbsent {
   193  				newValue = (newValue - objective) * float64(timeCurrent-timeBucketStarts)
   194  			}
   195  
   196  			r.Values = append(r.Values, newValue)
   197  		}
   198  	}
   199  
   200  	return results, nil
   201  }
   202  
   203  func (f *slo) Description() map[string]types.FunctionDescription {
   204  	return map[string]types.FunctionDescription{
   205  		"slo": {
   206  			Description: "Returns ratio of points which are in `interval` range and are above/below (`method`) than `value`.\n\nExample:\n\n.. code-block:: none\n\n  &target=slo(some.data.series, \"1hour\", \"above\", 117)",
   207  			Function:    "slo(seriesList, interval, method, value)",
   208  			Group:       "Transform",
   209  			Module:      "graphite.render.functions",
   210  			Name:        "slo",
   211  			Params: []types.FunctionParam{
   212  				{
   213  					Name:     "seriesList",
   214  					Required: true,
   215  					Type:     types.SeriesList,
   216  				},
   217  				{
   218  					Name:     "interval",
   219  					Required: true,
   220  					Suggestions: types.NewSuggestions(
   221  						"10min",
   222  						"1h",
   223  						"1d",
   224  					),
   225  					Type: types.Interval,
   226  				},
   227  				{
   228  					Default: types.NewSuggestion("above"),
   229  					Name:    "method",
   230  					Options: types.StringsToSuggestionList([]string{
   231  						"above",
   232  						"aboveOrEqual",
   233  						"below",
   234  						"belowOrEqual",
   235  					}),
   236  					Required: true,
   237  					Type:     types.String,
   238  				},
   239  				{
   240  					Default:  types.NewSuggestion(0.0),
   241  					Name:     "value",
   242  					Required: true,
   243  					Type:     types.Float,
   244  				},
   245  			},
   246  			SeriesChange: true, // function aggregate metrics or change series items count
   247  			NameChange:   true, // name changed
   248  			TagsChange:   true, // name tag changed
   249  			ValuesChange: true, // values changed
   250  		},
   251  		"sloErrorBudget": {
   252  			Description: "Returns rest failure/error budget for this time interval\n\nExample:\n\n.. code-block:: none\n\n  &target=sloErrorBudget(some.data.series, \"1hour\", \"above\", 117, 9999e-4)",
   253  			Group:       "Transform",
   254  			Function:    "sloErrorBudget(seriesList, interval, method, value, objective)",
   255  			Module:      "graphite.render.functions",
   256  			Name:        "sloErrorBudget",
   257  			Params: []types.FunctionParam{
   258  				{
   259  					Name:     "seriesList",
   260  					Required: true,
   261  					Type:     types.SeriesList,
   262  				},
   263  				{
   264  					Name:     "interval",
   265  					Required: true,
   266  					Suggestions: types.NewSuggestions(
   267  						"10min",
   268  						"1h",
   269  						"1d",
   270  					),
   271  					Type: types.Interval,
   272  				},
   273  				{
   274  					Default: types.NewSuggestion("above"),
   275  					Name:    "method",
   276  					Options: types.StringsToSuggestionList([]string{
   277  						"above",
   278  						"aboveOrEqual",
   279  						"below",
   280  						"belowOrEqual",
   281  					}),
   282  					Required: true,
   283  					Type:     types.String,
   284  				},
   285  				{
   286  					Default:  types.NewSuggestion(0.0),
   287  					Name:     "value",
   288  					Required: true,
   289  					Type:     types.Float,
   290  				},
   291  				{
   292  					Default:  types.NewSuggestion(9999e-4),
   293  					Name:     "objective",
   294  					Required: true,
   295  					Type:     types.Float,
   296  				},
   297  			},
   298  			SeriesChange: true, // function aggregate metrics or change series items count
   299  			NameChange:   true, // name changed
   300  			TagsChange:   true, // name tag changed
   301  			ValuesChange: true, // values changed
   302  		},
   303  	}
   304  }