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 }