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 }