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

     1  package asPercent
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"math"
     7  	"sort"
     8  
     9  	"github.com/go-graphite/carbonapi/expr/helper"
    10  	"github.com/go-graphite/carbonapi/expr/interfaces"
    11  	"github.com/go-graphite/carbonapi/expr/types"
    12  	"github.com/go-graphite/carbonapi/pkg/parser"
    13  )
    14  
    15  type asPercent struct{}
    16  
    17  func GetOrder() interfaces.Order {
    18  	return interfaces.Any
    19  }
    20  
    21  func New(configFile string) []interfaces.FunctionMetadata {
    22  	res := make([]interfaces.FunctionMetadata, 0)
    23  	f := &asPercent{}
    24  	for _, n := range []string{"asPercent", "pct"} {
    25  		res = append(res, interfaces.FunctionMetadata{Name: n, F: f})
    26  	}
    27  	return res
    28  }
    29  
    30  func getTotal(arg []*types.MetricData, i int) float64 {
    31  	var t float64
    32  	var atLeastOne bool
    33  	for _, a := range arg {
    34  		if math.IsNaN(a.Values[i]) {
    35  			continue
    36  		}
    37  		atLeastOne = true
    38  		t += a.Values[i]
    39  	}
    40  	if atLeastOne {
    41  		return t
    42  	}
    43  	return math.NaN()
    44  }
    45  
    46  // sum aligned series
    47  func sumSeries(seriesList []*types.MetricData) *types.MetricData {
    48  	result := &types.MetricData{}
    49  	result.Values = make([]float64, len(seriesList[0].Values))
    50  
    51  	for _, s := range seriesList {
    52  		for i := range result.Values {
    53  			if !math.IsNaN(s.Values[i]) {
    54  				result.Values[i] += s.Values[i]
    55  			}
    56  		}
    57  	}
    58  
    59  	return result
    60  }
    61  
    62  func calculatePercentage(seriesValue, totalValue float64) float64 {
    63  	var value float64
    64  	if math.IsNaN(seriesValue) || math.IsNaN(totalValue) || totalValue == 0 {
    65  		value = math.NaN()
    66  	} else {
    67  		value = seriesValue * (100 / totalValue)
    68  	}
    69  	return value
    70  }
    71  
    72  // GetPercentages contains the logic to apply to the series in order to properly
    73  // calculate percentages. If the length of the values in series and totalSeries are
    74  // not equal, special handling is required. If the number of values in seriesList is
    75  // greater than the number of values in totalSeries, math.NaN() needs to be set to the
    76  // indices in series starting at the length of totalSeries.Values. If the number of values
    77  // in totalSeries is greater than the number of values in series, then math.NaN() needs
    78  // to be appended to series until its values have the same length as totalSeries.Values
    79  func getPercentages(series, totalSeries *types.MetricData) {
    80  	// If there are more series values than totalSeries values, set series value to math.NaN() for those indices
    81  	if len(series.Values) > len(totalSeries.Values) {
    82  		for i := 0; i < len(totalSeries.Values); i++ {
    83  			series.Values[i] = calculatePercentage(series.Values[i], totalSeries.Values[i])
    84  		}
    85  		for i := len(totalSeries.Values); i < len(series.Values); i++ {
    86  			series.Values[i] = math.NaN()
    87  		}
    88  	} else {
    89  		for i := range series.Values {
    90  			series.Values[i] = calculatePercentage(series.Values[i], totalSeries.Values[i])
    91  		}
    92  
    93  		// If there are more totalSeries values than series values, append math.NaN() to the series values
    94  		if lengthDiff := len(totalSeries.Values) - len(series.Values); lengthDiff > 0 {
    95  			for i := 0; i < lengthDiff; i++ {
    96  				series.Values = append(series.Values, math.NaN())
    97  			}
    98  		}
    99  	}
   100  }
   101  
   102  func groupByNodes(seriesList []*types.MetricData, nodesOrTags []parser.NodeOrTag) map[string][]*types.MetricData {
   103  	groups := make(map[string][]*types.MetricData)
   104  
   105  	for _, series := range seriesList {
   106  		key := helper.AggKey(series, nodesOrTags)
   107  		groups[key] = append(groups[key], series)
   108  	}
   109  
   110  	return groups
   111  }
   112  
   113  func seriesAsPercent(arg, total []*types.MetricData) []*types.MetricData {
   114  	if len(total) == 0 {
   115  		// asPercent(seriesList, MISSING)
   116  		for _, a := range arg {
   117  			for i := range a.Values {
   118  				a.Values[i] = math.NaN()
   119  			}
   120  
   121  			a.Name = "asPercent(" + a.Name + ",MISSING)"
   122  		}
   123  	} else if len(total) == 1 {
   124  		// asPercent(seriesList, totalSeries)
   125  		for _, a := range arg {
   126  			getPercentages(a, total[0])
   127  
   128  			a.Name = "asPercent(" + a.Name + "," + total[0].Name + ")"
   129  		}
   130  	} else {
   131  		// asPercent(seriesList, totalSeriesList)
   132  		sort.Sort(helper.ByName(arg))
   133  		sort.Sort(helper.ByName(total))
   134  		if len(arg) <= len(total) {
   135  			// asPercent(seriesList, totalSeriesList) for series with len(seriesList) <= len(totalSeriesList)
   136  			for n, a := range arg {
   137  				getPercentages(a, total[n])
   138  
   139  				a.Name = "asPercent(" + a.Name + "," + total[n].Name + ")"
   140  			}
   141  			if len(arg) < len(total) {
   142  				total = total[len(arg):]
   143  				for _, tot := range total {
   144  					for i := range tot.Values {
   145  						tot.Values[i] = math.NaN()
   146  					}
   147  					tot.Name = "asPercent(MISSING," + tot.Name + ")"
   148  					tot.Tags = map[string]string{"name": "MISSING"}
   149  				}
   150  				arg = append(arg, total...)
   151  			}
   152  		} else {
   153  			// asPercent(seriesList, totalSeriesList) for series with unaligned length
   154  			// len(seriesList) > len(totalSeriesList)
   155  			for n := range total {
   156  				a := arg[n]
   157  				getPercentages(a, total[n])
   158  
   159  				a.Name = "asPercent(" + a.Name + "," + total[n].Name + ")"
   160  			}
   161  			for n := len(total); n < len(arg); n++ {
   162  				a := arg[n]
   163  				for i := range a.Values {
   164  					a.Values[i] = math.NaN()
   165  				}
   166  
   167  				a.Name = "asPercent(" + a.Name + ",MISSING)"
   168  			}
   169  		}
   170  
   171  	}
   172  	return arg
   173  }
   174  
   175  func seriesGroupAsPercent(arg []*types.MetricData, nodesOrTags []parser.NodeOrTag) []*types.MetricData {
   176  	// asPercent(seriesList, None, *nodes)
   177  	argGroups := groupByNodes(arg, nodesOrTags)
   178  
   179  	keys := make([]string, len(argGroups))
   180  	i := 0
   181  	for key := range argGroups {
   182  		keys[i] = key
   183  		i++
   184  	}
   185  	sort.Strings(keys)
   186  
   187  	arg = make([]*types.MetricData, 0, len(arg))
   188  	for _, k := range keys {
   189  		argGroup := argGroups[k]
   190  		sum := sumSeries(argGroup)
   191  		start := len(arg)
   192  		for _, a := range argGroup {
   193  			for i := range sum.Values {
   194  				if math.IsNaN(sum.Values[i]) || sum.Values[i] == 0 {
   195  					if !math.IsNaN(a.Values[i]) {
   196  						a.Values[i] = math.NaN()
   197  					}
   198  				} else {
   199  					a.Values[i] *= 100 / sum.Values[i]
   200  				}
   201  			}
   202  			a.Name = "asPercent(" + a.Name + ",None)"
   203  			arg = append(arg, a)
   204  		}
   205  		end := len(arg)
   206  		sort.Sort(helper.ByName(arg[start:end]))
   207  	}
   208  	return arg
   209  }
   210  
   211  func seriesGroup2AsPercent(arg, total []*types.MetricData, nodesOrTags []parser.NodeOrTag) []*types.MetricData {
   212  	argGroups := groupByNodes(arg, nodesOrTags)
   213  	totalGroups := groupByNodes(total, nodesOrTags)
   214  
   215  	keys := make([]string, len(argGroups), len(argGroups)+4)
   216  	i := 0
   217  	for key := range argGroups {
   218  		keys[i] = key
   219  		i++
   220  	}
   221  	for key := range totalGroups {
   222  		if _, exist := argGroups[key]; !exist {
   223  			keys = append(keys, key)
   224  		}
   225  	}
   226  	sort.Strings(keys)
   227  
   228  	arg = make([]*types.MetricData, 0, len(arg))
   229  	for _, key := range keys {
   230  		if argGroup, exists := argGroups[key]; exists {
   231  			if totalGroup, exist := totalGroups[key]; exist {
   232  				if len(totalGroup) == 1 {
   233  					// asPercent(seriesList, totalSeries, *nodes)
   234  					start := len(arg)
   235  					for _, a := range argGroup {
   236  						getPercentages(a, totalGroup[0])
   237  
   238  						a.Name = "asPercent(" + a.Name + "," + totalGroup[0].Name + ")"
   239  						arg = append(arg, a)
   240  					}
   241  					end := len(arg)
   242  					sort.Sort(helper.ByName(arg[start:end]))
   243  				} else if len(argGroup) <= len(totalGroup) {
   244  					// asPercent(seriesList, totalSeriesList, *nodes)
   245  					// len(seriesGroupList) <= len(totalSeriesGroupList)
   246  
   247  					start := len(arg)
   248  					for n, a := range argGroup {
   249  						for i := range a.Values {
   250  							t := totalGroup[n].Values[i]
   251  							if math.IsNaN(a.Values[i]) || math.IsNaN(t) || t == 0 {
   252  								a.Values[i] = math.NaN()
   253  							} else {
   254  								a.Values[i] *= 100 / t
   255  							}
   256  						}
   257  						a.Name = "asPercent(" + a.Name + "," + totalGroup[n].Name + ")"
   258  						arg = append(arg, a)
   259  					}
   260  					if len(argGroup) < len(totalGroup) {
   261  						totalGroup = totalGroup[len(argGroup):]
   262  						for _, tot := range totalGroup {
   263  							for i := range tot.Values {
   264  								tot.Values[i] = math.NaN()
   265  							}
   266  							tot.Name = "asPercent(MISSING," + tot.Name + ")"
   267  							tot.Tags = map[string]string{"name": "MISSING"}
   268  						}
   269  						arg = append(arg, totalGroup...)
   270  					}
   271  					end := len(arg)
   272  					sort.Sort(helper.ByName(arg[start:end]))
   273  				} else {
   274  					// asPercent(seriesList, totalSeriesList, *nodes) for series with unaligned length
   275  					// len(seriesGroupList) > len(totalSeriesGroupList)
   276  
   277  					start := len(arg)
   278  					for n := range totalGroup {
   279  						a := argGroup[n]
   280  						for i := range a.Values {
   281  							t := total[n].Values[i]
   282  							if math.IsNaN(a.Values[i]) || math.IsNaN(t) || t == 0 {
   283  								a.Values[i] = math.NaN()
   284  							} else {
   285  								a.Values[i] *= 100 / t
   286  							}
   287  						}
   288  						a.Name = "asPercent(" + a.Name + "," + total[n].Name + ")"
   289  					}
   290  					for n := len(total); n < len(arg); n++ {
   291  						a := arg[n]
   292  						for i := range a.Values {
   293  							a.Values[i] = math.NaN()
   294  						}
   295  
   296  						a.Name = "asPercent(" + a.Name + ",MISSING)"
   297  						arg = append(arg, a)
   298  					}
   299  					end := len(arg)
   300  					sort.Sort(helper.ByName(arg[start:end]))
   301  				}
   302  			} else {
   303  				start := len(arg)
   304  				for _, a := range argGroup {
   305  					for i := range a.Values {
   306  						a.Values[i] = math.NaN()
   307  					}
   308  					a.Name = "asPercent(" + a.Name + ",MISSING)"
   309  					arg = append(arg, a)
   310  				}
   311  				end := len(arg)
   312  				sort.Sort(helper.ByName(arg[start:end]))
   313  			}
   314  		} else {
   315  			totalGroup := totalGroups[key]
   316  			if _, exist := argGroups[key]; !exist {
   317  				start := len(arg)
   318  				for _, t := range totalGroup {
   319  					for i := range t.Values {
   320  						t.Values[i] = math.NaN()
   321  					}
   322  					t.Name = "asPercent(MISSING," + t.Name + ")"
   323  					t.Tags = map[string]string{"name": "MISSING"}
   324  					arg = append(arg, t)
   325  				}
   326  				end := len(arg)
   327  				sort.Sort(helper.ByName(arg[start:end]))
   328  			}
   329  		}
   330  	}
   331  	return arg
   332  }
   333  
   334  // asPercent(seriesList, total=None, *nodes)
   335  func (f *asPercent) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
   336  	arg, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  	if len(arg) == 0 {
   341  		return nil, nil
   342  	}
   343  
   344  	if e.ArgsLen() == 1 {
   345  		// asPercent(seriesList)
   346  
   347  		// TODO (msaf1980): may be copy in before start eval (based on function pipeline descritptions (ValueChange field)) and avoid copy metrics in functions
   348  		arg = helper.AlignSeries(types.CopyMetricDataSlice(arg))
   349  		for i := range arg[0].Values {
   350  			total := getTotal(arg, i)
   351  
   352  			for _, a := range arg {
   353  				if math.IsNaN(a.Values[i]) || math.IsNaN(total) || total == 0 {
   354  					a.Values[i] = math.NaN()
   355  				} else {
   356  					a.Values[i] *= 100 / total
   357  				}
   358  			}
   359  		}
   360  
   361  		for _, a := range arg {
   362  			a.Name = "asPercent(" + a.Name + ")"
   363  		}
   364  		return arg, nil
   365  	} else if e.ArgsLen() == 2 && (e.Arg(1).IsConst() || e.Arg(1).IsString()) {
   366  		// asPercent(seriesList, N)
   367  
   368  		total, err := e.GetFloatArg(1)
   369  
   370  		if err != nil {
   371  			return nil, err
   372  		}
   373  
   374  		// TODO (msaf1980): may be copy in before start eval (based on function pipeline descritptions (ValueChange field)) and avoid copy metrics in functions
   375  		arg = helper.AlignSeries(types.CopyMetricDataSlice(arg))
   376  
   377  		for _, a := range arg {
   378  			for i := range a.Values {
   379  				if math.IsNaN(a.Values[i]) || math.IsNaN(total) || total == 0 {
   380  					a.Values[i] = math.NaN()
   381  				} else {
   382  					a.Values[i] *= 100 / total
   383  				}
   384  			}
   385  			a.Name = "asPercent(" + a.Name + "," + e.Arg(1).StringValue() + ")"
   386  		}
   387  		return arg, nil
   388  	} else if e.ArgsLen() == 2 && (e.Arg(1).IsName() || e.Arg(1).IsFunc()) {
   389  		// asPercent(seriesList, totalList)
   390  		total, err := helper.GetSeriesArg(ctx, eval, e.Arg(1), from, until, values)
   391  		if err != nil {
   392  			return nil, err
   393  		}
   394  
   395  		alignedSeries := helper.AlignSeries(types.CopyMetricDataSlice(append(arg, total...)))
   396  		arg = alignedSeries[0:len(arg)]
   397  		total = alignedSeries[len(arg):]
   398  
   399  		return seriesAsPercent(arg, total), nil
   400  
   401  	} else if e.ArgsLen() >= 3 && e.Arg(1).IsName() || e.Arg(1).IsFunc() {
   402  		// Group by
   403  		nodesOrTags, err := e.GetNodeOrTagArgs(2, false)
   404  		if err != nil {
   405  			return nil, err
   406  		}
   407  
   408  		if e.Arg(1).Target() == "None" {
   409  			// asPercent(seriesList, None, *nodes)
   410  			arg = helper.AlignSeries(types.CopyMetricDataSlice(arg))
   411  
   412  			return seriesGroupAsPercent(arg, nodesOrTags), nil
   413  		} else {
   414  			// asPercent(seriesList, totalSeriesList, *nodes)
   415  			total, err := helper.GetSeriesArg(ctx, eval, e.Arg(1), from, until, values)
   416  			if err != nil {
   417  				return nil, err
   418  			}
   419  
   420  			alignedSeries := helper.AlignSeries(types.CopyMetricDataSlice(append(arg, total...)))
   421  			arg = alignedSeries[0:len(arg)]
   422  			total = alignedSeries[len(arg):]
   423  
   424  			return seriesGroup2AsPercent(arg, total, nodesOrTags), nil
   425  		}
   426  	}
   427  
   428  	return nil, errors.New("total must be either a constant or a series")
   429  }
   430  
   431  // Description is auto-generated description, based on output of https://github.com/graphite-project/graphite-web
   432  func (f *asPercent) Description() map[string]types.FunctionDescription {
   433  	return map[string]types.FunctionDescription{
   434  		"asPercent": {
   435  			Description: "Calculates a percentage of the total of a wildcard series. If `total` is specified,\neach series will be calculated as a percentage of that total. If `total` is not specified,\nthe sum of all points in the wildcard series will be used instead.\n\nA list of nodes can optionally be provided, if so they will be used to match series with their\ncorresponding totals following the same logic as :py:func:`groupByNodes <groupByNodes>`.\n\nWhen passing `nodes` the `total` parameter may be a series list or `None`.  If it is `None` then\nfor each series in `seriesList` the percentage of the sum of series in that group will be returned.\n\nWhen not passing `nodes`, the `total` parameter may be a single series, reference the same number\nof series as `seriesList` or be a numeric value.\n\nExample:\n\n.. code-block:: none\n\n  # Server01 connections failed and succeeded as a percentage of Server01 connections attempted\n  &target=asPercent(Server01.connections.{failed,succeeded}, Server01.connections.attempted)\n\n  # For each server, its connections failed as a percentage of its connections attempted\n  &target=asPercent(Server*.connections.failed, Server*.connections.attempted)\n\n  # For each server, its connections failed and succeeded as a percentage of its connections attemped\n  &target=asPercent(Server*.connections.{failed,succeeded}, Server*.connections.attempted, 0)\n\n  # apache01.threads.busy as a percentage of 1500\n  &target=asPercent(apache01.threads.busy,1500)\n\n  # Server01 cpu stats as a percentage of its total\n  &target=asPercent(Server01.cpu.*.jiffies)\n\n  # cpu stats for each server as a percentage of its total\n  &target=asPercent(Server*.cpu.*.jiffies, None, 0)\n\nWhen using `nodes`, any series or totals that can't be matched will create output series with\nnames like ``asPercent(someSeries,MISSING)`` or ``asPercent(MISSING,someTotalSeries)`` and all\nvalues set to None. If desired these series can be filtered out by piping the result through\n``|exclude(\"MISSING\")`` as shown below:\n\n.. code-block:: none\n\n  &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)\n\n  # will produce 3 output series:\n  # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected}\n  # asPercent(Server2.memory.used,MISSING) [all values will be None}\n  # asPercent(MISSING,Server3.memory.total) [all values will be None}\n\n  &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)|exclude(\"MISSING\")\n\n  # will produce 1 output series:\n  # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected}\n\nEach node may be an integer referencing a node in the series name or a string identifying a tag.\n\n.. note::\n\n  When `total` is a seriesList, specifying `nodes` to match series with the corresponding total\n  series will increase reliability.",
   436  			Function:    "asPercent(seriesList, total=None, *nodes)",
   437  			Group:       "Combine",
   438  			Module:      "graphite.render.functions",
   439  			Name:        "asPercent",
   440  			Params: []types.FunctionParam{
   441  				{
   442  					Name:     "seriesList",
   443  					Required: true,
   444  					Type:     types.SeriesList,
   445  				},
   446  				{
   447  					Name: "total",
   448  					Type: types.SeriesList,
   449  				},
   450  				{
   451  					Multiple: true,
   452  					Name:     "nodes",
   453  					Type:     types.NodeOrTag,
   454  				},
   455  			},
   456  			SeriesChange: true, // function aggregate metrics or change series items count
   457  			NameChange:   true, // name changed
   458  			TagsChange:   true, // name tag changed
   459  			ValuesChange: true, // values changed
   460  		},
   461  		"pct": {
   462  			Description: "Calculates a percentage of the total of a wildcard series. If `total` is specified,\neach series will be calculated as a percentage of that total. If `total` is not specified,\nthe sum of all points in the wildcard series will be used instead.\n\nA list of nodes can optionally be provided, if so they will be used to match series with their\ncorresponding totals following the same logic as :py:func:`groupByNodes <groupByNodes>`.\n\nWhen passing `nodes` the `total` parameter may be a series list or `None`.  If it is `None` then\nfor each series in `seriesList` the percentage of the sum of series in that group will be returned.\n\nWhen not passing `nodes`, the `total` parameter may be a single series, reference the same number\nof series as `seriesList` or be a numeric value.\n\nExample:\n\n.. code-block:: none\n\n  # Server01 connections failed and succeeded as a percentage of Server01 connections attempted\n  &target=asPercent(Server01.connections.{failed,succeeded}, Server01.connections.attempted)\n\n  # For each server, its connections failed as a percentage of its connections attempted\n  &target=asPercent(Server*.connections.failed, Server*.connections.attempted)\n\n  # For each server, its connections failed and succeeded as a percentage of its connections attemped\n  &target=asPercent(Server*.connections.{failed,succeeded}, Server*.connections.attempted, 0)\n\n  # apache01.threads.busy as a percentage of 1500\n  &target=asPercent(apache01.threads.busy,1500)\n\n  # Server01 cpu stats as a percentage of its total\n  &target=asPercent(Server01.cpu.*.jiffies)\n\n  # cpu stats for each server as a percentage of its total\n  &target=asPercent(Server*.cpu.*.jiffies, None, 0)\n\nWhen using `nodes`, any series or totals that can't be matched will create output series with\nnames like ``asPercent(someSeries,MISSING)`` or ``asPercent(MISSING,someTotalSeries)`` and all\nvalues set to None. If desired these series can be filtered out by piping the result through\n``|exclude(\"MISSING\")`` as shown below:\n\n.. code-block:: none\n\n  &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)\n\n  # will produce 3 output series:\n  # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected}\n  # asPercent(Server2.memory.used,MISSING) [all values will be None}\n  # asPercent(MISSING,Server3.memory.total) [all values will be None}\n\n  &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)|exclude(\"MISSING\")\n\n  # will produce 1 output series:\n  # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected}\n\nEach node may be an integer referencing a node in the series name or a string identifying a tag.\n\n.. note::\n\n  When `total` is a seriesList, specifying `nodes` to match series with the corresponding total\n  series will increase reliability.",
   463  			Function:    "pct(seriesList, total=None, *nodes)",
   464  			Group:       "Combine",
   465  			Module:      "graphite.render.functions",
   466  			Name:        "pct",
   467  			Params: []types.FunctionParam{
   468  				{
   469  					Name:     "seriesList",
   470  					Required: true,
   471  					Type:     types.SeriesList,
   472  				},
   473  				{
   474  					Name: "total",
   475  					Type: types.SeriesList,
   476  				},
   477  				{
   478  					Multiple: true,
   479  					Name:     "nodes",
   480  					Type:     types.NodeOrTag,
   481  				},
   482  			},
   483  			SeriesChange: true, // function aggregate metrics or change series items count
   484  			NameChange:   true, // name changed
   485  			TagsChange:   true, // name tag changed
   486  			ValuesChange: true, // values changed
   487  		},
   488  	}
   489  }