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

     1  package groupByNode
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  
     7  	"github.com/ansel1/merry"
     8  	"github.com/go-graphite/carbonapi/expr/consolidations"
     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 groupByNode 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 := &groupByNode{}
    24  	functions := []string{"groupByNode", "groupByNodes"}
    25  	for _, n := range functions {
    26  		res = append(res, interfaces.FunctionMetadata{Name: n, F: f})
    27  	}
    28  	return res
    29  }
    30  
    31  // groupByNode(seriesList, nodeNum, callback)
    32  // groupByNodes(seriesList, callback, *nodes)
    33  func (f *groupByNode) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
    34  	if e.ArgsLen() < 2 {
    35  		return nil, parser.ErrMissingArgument
    36  	}
    37  
    38  	args, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  	var callback string
    43  	var nodes []parser.NodeOrTag
    44  
    45  	target := e.Target()
    46  	if target == "groupByNode" {
    47  		nodes, err = e.GetNodeOrTagArgs(1, true)
    48  		if err != nil {
    49  			return nil, err
    50  		}
    51  		callback, err = e.GetStringArgDefault(2, "avg")
    52  		if err != nil {
    53  			return nil, err
    54  		}
    55  	} else {
    56  		callback, err = e.GetStringArg(1)
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  
    61  		nodes, err = e.GetNodeOrTagArgs(2, false)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  	}
    66  
    67  	var results []*types.MetricData
    68  
    69  	groups := make(map[string][]*types.MetricData)
    70  	nodeList := make([]string, 0, 4)
    71  
    72  	// This is done to preserve the order
    73  	for _, a := range args {
    74  		key := helper.AggKey(a, nodes)
    75  		if len(groups[key]) == 0 {
    76  			nodeList = append(nodeList, key)
    77  		}
    78  		groups[key] = append(groups[key], a)
    79  	}
    80  
    81  	for _, k := range nodeList {
    82  		k := k // k's reference is used later, so it's important to make it unique per loop
    83  		v := groups[k]
    84  
    85  		// Ensure that names won't be parsed as consts, appending stub to them
    86  		expr := callback + "(stub_" + k + ")"
    87  
    88  		// create a stub context to evaluate the callback in
    89  		nexpr, _, err := parser.ParseExpr(expr)
    90  		if err != nil {
    91  			return nil, err
    92  		} else if nexpr.Type() != parser.EtFunc {
    93  			err = merry.WithMessagef(parser.ErrInvalidArg, "unsupported "+target+" callback function")
    94  			return nil, err
    95  		}
    96  		// remove all stub_ prefixes we've prepended before
    97  		nexpr.SetRawArgs(strings.Replace(nexpr.RawArgs(), "stub_", "", 1))
    98  		for argIdx := range nexpr.Args() {
    99  			nexpr.Args()[argIdx].SetTarget(strings.Replace(nexpr.Args()[0].Target(), "stub_", "", 1))
   100  		}
   101  
   102  		nvalues := values
   103  		if e.Target() == "groupByNode" || e.Target() == "groupByNodes" {
   104  			nvalues = map[parser.MetricRequest][]*types.MetricData{
   105  				{Metric: k, From: from, Until: until}: v,
   106  			}
   107  		}
   108  
   109  		r, _ := eval.Eval(ctx, nexpr, from, until, nvalues)
   110  		if r != nil {
   111  			var res []*types.MetricData
   112  			if len(r) > 0 {
   113  				// Only the first result is used. See implementation in Graphite-web:
   114  				// https://github.com/graphite-project/graphite-web/blob/master/webapp/graphite/render/functions.py
   115  				res = []*types.MetricData{r[0]}
   116  			} else {
   117  				res = r
   118  			}
   119  			// avoid overwriting, do copy-on-write
   120  			rg := types.CopyMetricDataSliceWithName(res, k)
   121  			results = append(results, rg...)
   122  		}
   123  	}
   124  
   125  	return results, nil
   126  }
   127  
   128  // Description is auto-generated description, based on output of https://github.com/graphite-project/graphite-web
   129  func (f *groupByNode) Description() map[string]types.FunctionDescription {
   130  	return map[string]types.FunctionDescription{
   131  		"groupByNode": {
   132  			Description: "Takes a serieslist and maps a callback to subgroups within as defined by a common node\n\n.. code-block:: none\n\n  &target=groupByNode(ganglia.by-function.*.*.cpu.load5,2,\"sumSeries\")\n\nWould return multiple series which are each the result of applying the \"sumSeries\" function\nto groups joined on the second node (0 indexed) resulting in a list of targets like\n\n.. code-block :: none\n\n  sumSeries(ganglia.by-function.server1.*.cpu.load5),sumSeries(ganglia.by-function.server2.*.cpu.load5),...\n\nNode may be an integer referencing a node in the series name or a string identifying a tag.\n\nThis is an alias for using :py:func:`groupByNodes <groupByNodes>` with a single node.",
   133  			Function:    "groupByNode(seriesList, nodeNum, callback='average')",
   134  			Group:       "Combine",
   135  			Module:      "graphite.render.functions",
   136  			Name:        "groupByNode",
   137  			Params: []types.FunctionParam{
   138  				{
   139  					Name:     "seriesList",
   140  					Required: true,
   141  					Type:     types.SeriesList,
   142  				},
   143  				{
   144  					Name:     "nodeNum",
   145  					Required: true,
   146  					Type:     types.NodeOrTag,
   147  				},
   148  				{
   149  					Default:  types.NewSuggestion("average"),
   150  					Name:     "callback",
   151  					Options:  types.StringsToSuggestionList(consolidations.AvailableSummarizers),
   152  					Required: false,
   153  					Type:     types.AggFunc,
   154  				},
   155  			},
   156  			SeriesChange: true, // function aggregate metrics or change series items count
   157  			NameChange:   true, // name changed
   158  			TagsChange:   true, // name tag changed
   159  			ValuesChange: true, // values changed
   160  		},
   161  		"groupByNodes": {
   162  			Description: "Takes a serieslist and maps a callback to subgroups within as defined by multiple nodes\n\n.. code-block:: none\n\n  &target=groupByNodes(ganglia.server*.*.cpu.load*,\"sum\",1,4)\n\nWould return multiple series which are each the result of applying the \"sum\" aggregation\nto groups joined on the nodes' list (0 indexed) resulting in a list of targets like\n\n.. code-block :: none\n\n  sumSeries(ganglia.server1.*.cpu.load5),sumSeries(ganglia.server1.*.cpu.load10),sumSeries(ganglia.server1.*.cpu.load15),sumSeries(ganglia.server2.*.cpu.load5),sumSeries(ganglia.server2.*.cpu.load10),sumSeries(ganglia.server2.*.cpu.load15),...\n\nThis function can be used with all aggregation functions supported by\n:py:func:`aggregate <aggregate>`: ``average``, ``median``, ``sum``, ``min``, ``max``, ``diff``,\n``stddev``, ``range`` & ``multiply``.\n\nEach node may be an integer referencing a node in the series name or a string identifying a tag.\n\n.. code-block :: none\n\n  &target=seriesByTag(\"name=~cpu.load.*\", \"server=~server[1-9}+\", \"datacenter=~dc[1-9}+\")|groupByNodes(\"average\", \"datacenter\", 1)\n\n  # will produce output series like\n  # dc1.load5, dc2.load5, dc1.load10, dc2.load10\n\nThis complements :py:func:`aggregateWithWildcards <aggregateWithWildcards>` which takes a list of wildcard nodes.",
   163  			Function:    "groupByNodes(seriesList, callback, *nodes)",
   164  			Group:       "Combine",
   165  			Module:      "graphite.render.functions",
   166  			Name:        "groupByNodes",
   167  			Params: []types.FunctionParam{
   168  				{
   169  					Name:     "seriesList",
   170  					Required: true,
   171  					Type:     types.SeriesList,
   172  				},
   173  				{
   174  					Name:     "callback",
   175  					Options:  types.StringsToSuggestionList(consolidations.AvailableSummarizers),
   176  					Required: false,
   177  					Type:     types.AggFunc,
   178  				},
   179  				{
   180  					Multiple: true,
   181  					Name:     "nodes",
   182  					Required: true,
   183  					Type:     types.NodeOrTag,
   184  				},
   185  			},
   186  			SeriesChange: true, // function aggregate metrics or change series items count
   187  			NameChange:   true, // name changed
   188  			TagsChange:   true, // name tag changed
   189  			ValuesChange: true, // values changed
   190  		},
   191  	}
   192  }