github.com/m3db/m3@v1.5.0/src/query/graphite/native/aggregation_functions.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package native 22 23 import ( 24 "fmt" 25 "math" 26 "runtime" 27 "sort" 28 "strings" 29 "sync" 30 31 "github.com/m3db/m3/src/query/block" 32 "github.com/m3db/m3/src/query/graphite/common" 33 "github.com/m3db/m3/src/query/graphite/ts" 34 xerrors "github.com/m3db/m3/src/x/errors" 35 ) 36 37 func wrapPathExpr(wrapper string, series ts.SeriesList) string { 38 return fmt.Sprintf("%s(%s)", wrapper, joinPathExpr(series)) 39 } 40 41 // sumSeries adds metrics together and returns the sum at each datapoint. 42 // If the time series have different intervals, the coarsest interval will be used. 43 func sumSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 44 return combineSeries(ctx, series, wrapPathExpr(sumSeriesFnName, ts.SeriesList(series)), ts.Sum) 45 } 46 47 // diffSeries subtracts all but the first series from the first series. 48 // If the time series have different intervals, the coarsest interval will be used. 49 func diffSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 50 transformedSeries := series 51 numSeries := len(series.Values) 52 if numSeries > 1 { 53 transformedSeries.Values = make([]*ts.Series, numSeries) 54 transformedSeries.Values[0] = series.Values[0] 55 for i := 1; i < len(series.Values); i++ { 56 res, err := transform( 57 ctx, 58 singlePathSpec{Values: []*ts.Series{series.Values[i]}}, 59 func(n string) string { return n }, 60 common.MaintainNaNTransformer(func(v float64) float64 { return -v }), 61 ) 62 if err != nil { 63 return ts.NewSeriesList(), err 64 } 65 transformedSeries.Values[i] = res.Values[0] 66 } 67 } 68 69 return combineSeries(ctx, transformedSeries, wrapPathExpr(diffSeriesFnName, ts.SeriesList(series)), ts.Sum) 70 } 71 72 // multiplySeries multiplies metrics together and returns the product at each datapoint. 73 // If the time series have different intervals, the coarsest interval will be used. 74 func multiplySeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 75 return combineSeries(ctx, series, wrapPathExpr(multiplySeriesFnName, ts.SeriesList(series)), ts.Mul) 76 } 77 78 // averageSeries takes a list of series and returns a new series containing the 79 // average of all values at each datapoint. 80 func averageSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 81 return combineSeries(ctx, series, wrapPathExpr(averageSeriesFnName, ts.SeriesList(series)), ts.Avg) 82 } 83 84 // minSeries takes a list of series and returns a new series containing the 85 // minimum value across the series at each datapoint 86 func minSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 87 return combineSeries(ctx, series, wrapPathExpr(minSeriesFnName, ts.SeriesList(series)), ts.Min) 88 } 89 90 // powSeries takes a list of series and returns a new series containing the 91 // pow value across the series at each datapoint 92 // nolint: gocritic 93 func powSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 94 return combineSeries(ctx, series, wrapPathExpr(powSeriesFnName, ts.SeriesList(series)), ts.Pow) 95 } 96 97 // maxSeries takes a list of series and returns a new series containing the 98 // maximum value across the series at each datapoint 99 func maxSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 100 return combineSeries(ctx, series, wrapPathExpr(maxSeriesFnName, ts.SeriesList(series)), ts.Max) 101 } 102 103 // medianSeries takes a list of series and returns a new series containing the 104 // median value across the series at each datapoint 105 func medianSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 106 if len(series.Values) == 0 { 107 return ts.NewSeriesList(), nil 108 } 109 normalized, start, end, millisPerStep, err := common.Normalize(ctx, ts.SeriesList(series)) 110 if err != nil { 111 return ts.NewSeriesList(), err 112 } 113 numSteps := ts.NumSteps(start, end, millisPerStep) 114 values := ts.NewValues(ctx, millisPerStep, numSteps) 115 116 valuesAtTime := make([]float64, len(normalized.Values)) 117 for i := 0; i < numSteps; i++ { 118 for j, series := range normalized.Values { 119 valuesAtTime[j] = series.ValueAt(i) 120 } 121 values.SetValueAt(i, ts.Median(valuesAtTime, len(valuesAtTime))) 122 } 123 124 name := wrapPathExpr(medianSeriesFnName, ts.SeriesList(series)) 125 output := ts.NewSeries(ctx, name, start, values) 126 return ts.SeriesList{ 127 Values: []*ts.Series{output}, 128 Metadata: series.Metadata, 129 }, nil 130 } 131 132 // lastSeries takes a list of series and returns a new series containing the 133 // last value at each datapoint 134 func lastSeries(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) { 135 return combineSeries(ctx, series, joinPathExpr(ts.SeriesList(series)), ts.Last) 136 } 137 138 // standardDeviationHelper returns the standard deviation of a slice of a []float64 139 func standardDeviationHelper(values []float64) float64 { 140 var count, sum float64 141 142 for _, value := range values { 143 if !math.IsNaN(value) { 144 sum += value 145 count++ 146 } 147 } 148 if count == 0 { 149 return math.NaN() 150 } 151 avg := sum / count 152 153 m2 := float64(0) 154 for _, value := range values { 155 if !math.IsNaN(value) { 156 diff := value - avg 157 m2 += diff * diff 158 } 159 } 160 161 variance := m2 / count 162 163 return math.Sqrt(variance) 164 } 165 166 // stddevSeries takes a list of series and returns a new series containing the 167 // standard deviation at each datapoint 168 // At step n, stddevSeries will make a list of every series' nth value, 169 // and calculate the standard deviation of that list. 170 // The output is a seriesList containing 1 series 171 func stddevSeries(ctx *common.Context, seriesList multiplePathSpecs) (ts.SeriesList, error) { 172 if len(seriesList.Values) == 0 { 173 return ts.NewSeriesList(), nil 174 } 175 176 firstSeries := seriesList.Values[0] 177 numSteps := firstSeries.Len() 178 values := ts.NewValues(ctx, firstSeries.MillisPerStep(), numSteps) 179 valuesAtTime := make([]float64, 0, numSteps) 180 for i := 0; i < numSteps; i++ { 181 valuesAtTime = valuesAtTime[:0] 182 for _, series := range seriesList.Values { 183 if l := series.Len(); l != numSteps { 184 return ts.NewSeriesList(), fmt.Errorf("mismatched series length, expected %d, got %d", numSteps, l) 185 } 186 valuesAtTime = append(valuesAtTime, series.ValueAt(i)) 187 } 188 values.SetValueAt(i, standardDeviationHelper(valuesAtTime)) 189 } 190 191 name := wrapPathExpr(stddevSeriesFnName, ts.SeriesList(seriesList)) 192 output := ts.NewSeries(ctx, name, firstSeries.StartTime(), values) 193 return ts.SeriesList{ 194 Values: []*ts.Series{output}, 195 Metadata: seriesList.Metadata, 196 }, nil 197 } 198 199 func divideSeriesHelper(ctx *common.Context, dividendSeries, divisorSeries *ts.Series, metadata block.ResultMetadata) (*ts.Series, error) { 200 normalized, minBegin, _, lcmMillisPerStep, err := common.Normalize(ctx, ts.SeriesList{ 201 Values: []*ts.Series{dividendSeries, divisorSeries}, 202 Metadata: metadata, 203 }) 204 if err != nil { 205 return nil, err 206 } 207 208 // NB(bl): Normalized must give back exactly two series of the same length. 209 dividend, divisor := normalized.Values[0], normalized.Values[1] 210 numSteps := dividend.Len() 211 vals := ts.NewValues(ctx, lcmMillisPerStep, numSteps) 212 for i := 0; i < numSteps; i++ { 213 dividendVal := dividend.ValueAt(i) 214 divisorVal := divisor.ValueAt(i) 215 if !math.IsNaN(dividendVal) && !math.IsNaN(divisorVal) && divisorVal != 0 { 216 value := dividendVal / divisorVal 217 vals.SetValueAt(i, value) 218 } 219 } 220 221 // The individual series will be named divideSeries(X, X), even if it is generated by divideSeriesLists 222 // Based on Graphite source code (link below) 223 // https://github.com/graphite-project/graphite-web/blob/17a34e7966f7a46eded30c2362765c74eea899cb/webapp/graphite/render/functions.py#L901 224 name := fmt.Sprintf("divideSeries(%s,%s)", dividend.Name(), divisor.Name()) 225 quotientSeries := ts.NewSeries(ctx, name, minBegin, vals) 226 return quotientSeries, nil 227 } 228 229 // divideSeries divides one series list by another single series 230 func divideSeries(ctx *common.Context, dividendSeriesList, divisorSeriesList singlePathSpec) (ts.SeriesList, error) { 231 if len(dividendSeriesList.Values) == 0 || len(divisorSeriesList.Values) == 0 { 232 return ts.NewSeriesList(), nil 233 } 234 if len(divisorSeriesList.Values) != 1 { 235 err := xerrors.NewInvalidParamsError(fmt.Errorf( 236 "divideSeries second argument must reference exactly one series but instead has %d", 237 len(divisorSeriesList.Values))) 238 return ts.NewSeriesList(), err 239 } 240 241 divisorSeries := divisorSeriesList.Values[0] 242 results := make([]*ts.Series, len(dividendSeriesList.Values)) 243 for idx, dividendSeries := range dividendSeriesList.Values { 244 metadata := divisorSeriesList.Metadata.CombineMetadata(dividendSeriesList.Metadata) 245 quotientSeries, err := divideSeriesHelper(ctx, dividendSeries, divisorSeries, metadata) 246 if err != nil { 247 return ts.NewSeriesList(), err 248 } 249 results[idx] = quotientSeries 250 } 251 252 r := ts.SeriesList(dividendSeriesList) 253 r.Values = results 254 return r, nil 255 } 256 257 // divideSeriesLists divides one series list by another series list 258 func divideSeriesLists(ctx *common.Context, dividendSeriesList, divisorSeriesList singlePathSpec) (ts.SeriesList, error) { 259 if len(dividendSeriesList.Values) != len(divisorSeriesList.Values) { 260 err := xerrors.NewInvalidParamsError(fmt.Errorf( 261 "divideSeriesLists both SeriesLists must have exactly the same length")) 262 return ts.NewSeriesList(), err 263 } 264 265 // If either list is not sorted yet then apply a default sort for deterministic results. 266 if !dividendSeriesList.SortApplied { 267 // Use sort.Stable for deterministic output. 268 sort.Stable(ts.SeriesByName(dividendSeriesList.Values)) 269 dividendSeriesList.SortApplied = true 270 } 271 if !divisorSeriesList.SortApplied { 272 // Use sort.Stable for deterministic output. 273 sort.Stable(ts.SeriesByName(divisorSeriesList.Values)) 274 divisorSeriesList.SortApplied = true 275 } 276 277 results := make([]*ts.Series, len(dividendSeriesList.Values)) 278 for idx, dividendSeries := range dividendSeriesList.Values { 279 divisorSeries := divisorSeriesList.Values[idx] 280 metadata := divisorSeriesList.Metadata.CombineMetadata(dividendSeriesList.Metadata) 281 quotientSeries, err := divideSeriesHelper(ctx, dividendSeries, divisorSeries, metadata) 282 if err != nil { 283 return ts.NewSeriesList(), err 284 } 285 results[idx] = quotientSeries 286 } 287 288 r := ts.SeriesList(dividendSeriesList) 289 // Set sorted as we sorted any input that wasn't already sorted. 290 r.SortApplied = true 291 r.Values = results 292 return r, nil 293 } 294 295 // aggregate takes a list of series and returns a new series containing the 296 // value aggregated across the series at each datapoint using the specified function. 297 // This function can be used with aggregation functions average (or avg), avg_zero, 298 // median, sum (or total), min, max, diff, stddev, count, 299 // range (or rangeOf), multiply & last (or current). 300 func aggregate(ctx *common.Context, series singlePathSpec, fname string) (ts.SeriesList, error) { 301 switch fname { 302 case emptyFnName, sumFnName, sumSeriesFnName, totalFnName: 303 return sumSeries(ctx, multiplePathSpecs(series)) 304 case minFnName, minSeriesFnName: 305 return minSeries(ctx, multiplePathSpecs(series)) 306 case maxFnName, maxSeriesFnName: 307 return maxSeries(ctx, multiplePathSpecs(series)) 308 case medianFnName, medianSeriesFnName: 309 return medianSeries(ctx, multiplePathSpecs(series)) 310 case avgFnName, averageFnName, averageSeriesFnName: 311 return averageSeries(ctx, multiplePathSpecs(series)) 312 case multiplyFnName, multiplySeriesFnName: 313 return multiplySeries(ctx, multiplePathSpecs(series)) 314 case diffFnName, diffSeriesFnName: 315 return diffSeries(ctx, multiplePathSpecs(series)) 316 case countFnName, countSeriesFnName: 317 return countSeries(ctx, multiplePathSpecs(series)) 318 case rangeFnName, rangeOfFnName, rangeOfSeriesFnName: 319 return rangeOfSeries(ctx, series) 320 case lastFnName, currentFnName: 321 return lastSeries(ctx, multiplePathSpecs(series)) 322 case stddevFnName, stdevFnName, stddevSeriesFnName: 323 return stddevSeries(ctx, multiplePathSpecs(series)) 324 default: 325 // Median: the movingMedian() method already implemented is returning an series non compatible result. skip support for now. 326 // avg_zero is not implemented, skip support for now unless later identified actual use cases. 327 return ts.NewSeriesList(), xerrors.NewInvalidParamsError(fmt.Errorf("invalid func %s", fname)) 328 } 329 } 330 331 // averageSeriesWithWildcards splits the given set of series into sub-groupings 332 // based on wildcard matches in the hierarchy, then averages the values in each 333 // grouping 334 func averageSeriesWithWildcards( 335 ctx *common.Context, 336 series singlePathSpec, 337 positions ...int, 338 ) (ts.SeriesList, error) { 339 return combineSeriesWithWildcards(ctx, series, positions, averageSpecificationFunc, ts.Avg) 340 } 341 342 // sumSeriesWithWildcards splits the given set of series into sub-groupings 343 // based on wildcard matches in the hierarchy, then sums the values in each 344 // grouping 345 func sumSeriesWithWildcards( 346 ctx *common.Context, 347 series singlePathSpec, 348 positions ...int, 349 ) (ts.SeriesList, error) { 350 return combineSeriesWithWildcards(ctx, series, positions, sumSpecificationFunc, ts.Sum) 351 } 352 353 // multiplySeriesWithWildcards splits the given set of series into sub-groupings 354 // based on wildcard matches in the hierarchy, then multiply the values in each 355 // grouping 356 func multiplySeriesWithWildcards( 357 ctx *common.Context, 358 series singlePathSpec, 359 positions ...int, 360 ) (ts.SeriesList, error) { 361 return combineSeriesWithWildcards(ctx, series, positions, multiplyWithWildcardsSpecificationFunc, ts.Mul) 362 } 363 364 // aggregateWithWildcards splits the given set of series into sub-groupings 365 // based on wildcard matches in the hierarchy, then aggregate the values in 366 // each grouping based on the given function. 367 // Similar to combineSeriesWithWildcards function but more general, as it 368 // supports any aggregate functions while combineSeriesWithWildcards only 369 // support aggregation with existing ts.ConsolidationFunc. 370 func aggregateWithWildcards( 371 ctx *common.Context, 372 series singlePathSpec, 373 fname string, 374 positions ...int, 375 ) (ts.SeriesList, error) { 376 if len(series.Values) == 0 { 377 return ts.SeriesList(series), nil 378 } 379 380 toAggregate := splitSeriesIntoSubgroups(series, positions) 381 382 newSeries := make([]*ts.Series, 0, len(toAggregate)) 383 for name, toAggregateSeries := range toAggregate { 384 seriesList := ts.SeriesList{ 385 Values: toAggregateSeries, 386 Metadata: series.Metadata, 387 } 388 aggregated, err := aggregate(ctx, singlePathSpec(seriesList), fname) 389 if err != nil { 390 return ts.NewSeriesList(), err 391 } 392 renamedSeries := aggregated.Values[0].RenamedTo(name) 393 newSeries = append(newSeries, renamedSeries) 394 } 395 396 r := ts.SeriesList(series) 397 398 r.Values = newSeries 399 400 // Ranging over hash map to create results destroys 401 // any sort order on the incoming series list 402 r.SortApplied = false 403 404 return r, nil 405 } 406 407 // combineSeriesWithWildcards splits the given set of series into sub-groupings 408 // based on wildcard matches in the hierarchy, then combines the values in each 409 // sub-grouping according to the provided consolidation function 410 func combineSeriesWithWildcards( 411 ctx *common.Context, 412 series singlePathSpec, 413 positions []int, 414 sf specificationFunc, 415 f ts.ConsolidationFunc, 416 ) (ts.SeriesList, error) { 417 if len(series.Values) == 0 { 418 return ts.SeriesList(series), nil 419 } 420 421 toCombine := splitSeriesIntoSubgroups(series, positions) 422 423 newSeries := make([]*ts.Series, 0, len(toCombine)) 424 for name, toCombineSeries := range toCombine { 425 seriesList := ts.SeriesList{ 426 Values: toCombineSeries, 427 Metadata: series.Metadata, 428 } 429 combined, err := combineSeries(ctx, multiplePathSpecs(seriesList), name, f) 430 if err != nil { 431 return ts.NewSeriesList(), err 432 } 433 combined.Values[0].Specification = sf(seriesList) 434 newSeries = append(newSeries, combined.Values...) 435 } 436 437 r := ts.SeriesList(series) 438 439 r.Values = newSeries 440 441 // Ranging over hash map to create results destroys 442 // any sort order on the incoming series list 443 r.SortApplied = false 444 445 return r, nil 446 } 447 448 func splitSeriesIntoSubgroups(series singlePathSpec, positions []int) map[string][]*ts.Series { 449 var ( 450 toCombine = make(map[string][]*ts.Series) 451 wildcards = make(map[int]struct{}) 452 ) 453 454 for _, position := range positions { 455 wildcards[position] = struct{}{} 456 } 457 458 for _, series := range series.Values { 459 var ( 460 parts = strings.Split(series.Name(), ".") 461 newParts = make([]string, 0, len(parts)) 462 ) 463 for i, part := range parts { 464 if _, wildcard := wildcards[i]; !wildcard { 465 newParts = append(newParts, part) 466 } 467 } 468 469 newName := strings.Join(newParts, ".") 470 toCombine[newName] = append(toCombine[newName], series) 471 } 472 473 return toCombine 474 } 475 476 // splits a slice into chunks 477 func chunkArrayHelper(slice []string, numChunks int) [][]string { 478 divided := make([][]string, 0, numChunks) 479 480 chunkSize := (len(slice) + numChunks - 1) / numChunks 481 482 for i := 0; i < len(slice); i += chunkSize { 483 end := i + chunkSize 484 485 if end > len(slice) { 486 end = len(slice) 487 } 488 489 divided = append(divided, slice[i:end]) 490 } 491 492 return divided 493 } 494 495 func evaluateTarget(ctx *common.Context, target string) (ts.SeriesList, error) { 496 eng, ok := ctx.Engine.(*Engine) 497 if !ok { 498 return ts.NewSeriesList(), fmt.Errorf("engine not native engine") 499 } 500 expression, err := eng.Compile(target) 501 if err != nil { 502 return ts.NewSeriesList(), err 503 } 504 return expression.Execute(ctx) 505 } 506 507 /* 508 applyByNode takes a seriesList and applies some complicated function (described by a string), replacing templates with unique 509 prefixes of keys from the seriesList (the key is all nodes up to the index given as `nodeNum`). 510 511 If the `newName` parameter is provided, the name of the resulting series will be given by that parameter, with any 512 "%" characters replaced by the unique prefix. 513 514 Example: 515 516 `applyByNode(servers.*.disk.bytes_free,1,"divideSeries(%.disk.bytes_free,sumSeries(%.disk.bytes_*))")` 517 518 Would find all series which match `servers.*.disk.bytes_free`, then trim them down to unique series up to the node 519 given by nodeNum, then fill them into the template function provided (replacing % by the prefixes). 520 521 Additional Examples: 522 523 Given keys of 524 525 - `stats.counts.haproxy.web.2XX` 526 - `stats.counts.haproxy.web.3XX` 527 - `stats.counts.haproxy.web.5XX` 528 - `stats.counts.haproxy.microservice.2XX` 529 - `stats.counts.haproxy.microservice.3XX` 530 - `stats.counts.haproxy.microservice.5XX` 531 532 The following will return the rate of 5XX's per service: 533 534 `applyByNode(stats.counts.haproxy.*.*XX, 3, "asPercent(%.5XX, sumSeries(%.*XX))", "%.pct_5XX")` 535 536 The output series would have keys `stats.counts.haproxy.web.pct_5XX` and `stats.counts.haproxy.microservice.pct_5XX`. 537 */ 538 func applyByNode(ctx *common.Context, seriesList singlePathSpec, nodeNum int, templateFunction string, newName string) (ts.SeriesList, error) { 539 // using this as a set 540 prefixMap := map[string]struct{}{} 541 for _, series := range seriesList.Values { 542 var ( 543 name = series.Name() 544 545 partsSeen int 546 prefix string 547 ) 548 549 for i, c := range name { 550 if c == '.' { 551 partsSeen++ 552 if partsSeen == nodeNum+1 { 553 prefix = name[:i] 554 break 555 } 556 } 557 } 558 559 if len(prefix) == 0 { 560 continue 561 } 562 563 prefixMap[prefix] = struct{}{} 564 } 565 566 // transform to slice 567 var prefixes []string 568 for p := range prefixMap { 569 prefixes = append(prefixes, p) 570 } 571 sort.Strings(prefixes) 572 573 var ( 574 mu sync.Mutex 575 wg sync.WaitGroup 576 multiErr xerrors.MultiError 577 578 output = make([]*ts.Series, 0, len(prefixes)) 579 maxConcurrency = runtime.GOMAXPROCS(0) / 2 580 ) 581 for _, prefixChunk := range chunkArrayHelper(prefixes, maxConcurrency) { 582 if multiErr.LastError() != nil { 583 return ts.NewSeriesList(), multiErr.LastError() 584 } 585 586 for _, prefix := range prefixChunk { 587 prefix := prefix // Capture for lambda. 588 newTarget := strings.ReplaceAll(templateFunction, "%", prefix) 589 wg.Add(1) 590 go func() { 591 defer wg.Done() 592 resultSeriesList, err := evaluateTarget(ctx, newTarget) 593 if err != nil { 594 mu.Lock() 595 multiErr = multiErr.Add(err) 596 mu.Unlock() 597 return 598 } 599 600 mu.Lock() 601 for _, resultSeries := range resultSeriesList.Values { 602 if newName != "" { 603 resultSeries = resultSeries.RenamedTo(strings.ReplaceAll(newName, "%", prefix)) 604 } 605 resultSeries.Specification = prefix 606 output = append(output, resultSeries) 607 } 608 mu.Unlock() 609 }() 610 } 611 wg.Wait() 612 } 613 614 // Retain metadata but we definitely did not retain sort order. 615 r := ts.SeriesList(seriesList) 616 r.Values = output 617 r.SortApplied = false 618 return r, nil 619 } 620 621 // groupByNode takes a serieslist and maps a callback to subgroups within as defined by a common node 622 // 623 // &target=groupByNode(foo.by-function.*.*.cpu.load5,2,"sumSeries") 624 // 625 // Would return multiple series which are each the result of applying the "sumSeries" function 626 // to groups joined on the second node (0 indexed) resulting in a list of targets like 627 // 628 // sumSeries(foo.by-function.server1.*.cpu.load5),sumSeries(foo.by-function.server2.*.cpu.load5),... 629 func groupByNode(ctx *common.Context, series singlePathSpec, node int, fname string) (ts.SeriesList, error) { 630 return groupByNodes(ctx, series, fname, []int{node}...) 631 } 632 633 func getAggregationKey(series *ts.Series, nodes []int) (string, error) { 634 seriesName := series.Name() 635 metricsPath, err := getFirstPathExpression(seriesName) 636 if err != nil { 637 return "", err 638 } 639 seriesName = metricsPath 640 641 parts := strings.Split(seriesName, ".") 642 643 keys := make([]string, 0, len(nodes)) 644 for _, n := range nodes { 645 if n < 0 { 646 n = len(parts) + n 647 } 648 649 if n < len(parts) { 650 keys = append(keys, parts[n]) 651 } else { 652 keys = append(keys, "") 653 } 654 } 655 return strings.Join(keys, "."), nil 656 } 657 658 func getMetaSeriesGrouping(seriesList singlePathSpec, nodes []int) (map[string][]*ts.Series, error) { 659 metaSeries := make(map[string][]*ts.Series) 660 661 if len(nodes) > 0 { 662 for _, s := range seriesList.Values { 663 key, err := getAggregationKey(s, nodes) 664 if err != nil { 665 return metaSeries, err 666 } 667 metaSeries[key] = append(metaSeries[key], s) 668 } 669 } 670 671 return metaSeries, nil 672 } 673 674 // Takes a serieslist and maps a callback to subgroups within as defined by multiple nodes 675 // 676 // &target=groupByNodes(ganglia.server*.*.cpu.load*,"sum",1,4) 677 // 678 // Would return multiple series which are each the result of applying the “sum” aggregation to groups joined on the 679 // nodes’ list (0 indexed) resulting in a list of targets like 680 // 681 // sumSeries(ganglia.server1.*.cpu.load5),sumSeries(ganglia.server1.*.cpu.load10),sumSeries(ganglia.server1.*.cpu.load15), 682 // sumSeries(ganglia.server2.*.cpu.load5),sumSeries(ganglia.server2.*.cpu.load10),sumSeries(ganglia.server2.*.cpu.load15),... 683 // 684 // NOTE: if len(nodes) = 0, aggregate all series into 1 series. 685 func groupByNodes(ctx *common.Context, seriesList singlePathSpec, fname string, nodes ...int) (ts.SeriesList, error) { 686 metaSeries, err := getMetaSeriesGrouping(seriesList, nodes) 687 if err != nil { 688 return ts.NewSeriesList(), err 689 } 690 691 if len(metaSeries) == 0 { 692 // if nodes is an empty slice or every node in nodes exceeds the number 693 // of parts in each series, just treat it like aggregate 694 return aggregate(ctx, seriesList, fname) 695 } 696 697 return applyFnToMetaSeries(ctx, seriesList, metaSeries, fname) 698 } 699 700 func applyFnToMetaSeries(ctx *common.Context, series singlePathSpec, metaSeries map[string][]*ts.Series, fname string) (ts.SeriesList, error) { 701 newSeries := make([]*ts.Series, 0, len(metaSeries)) 702 for key, metaSeries := range metaSeries { 703 seriesList := ts.SeriesList{ 704 Values: metaSeries, 705 Metadata: series.Metadata, 706 } 707 output, err := aggregate(ctx, singlePathSpec(seriesList), fname) 708 if err != nil { 709 return ts.NewSeriesList(), err 710 } 711 712 if len(output.Values) > 0 { 713 series := output.Values[0].RenamedTo(key) 714 newSeries = append(newSeries, series) 715 } 716 } 717 718 r := ts.SeriesList(series) 719 720 r.Values = newSeries 721 722 // Ranging over hash map to create results destroys 723 // any sort order on the incoming series list 724 r.SortApplied = false 725 726 return r, nil 727 } 728 729 // combineSeries combines multiple series into a single series using a 730 // consolidation func. If the series use different time intervals, the 731 // coarsest time will apply. 732 func combineSeries(ctx *common.Context, 733 series multiplePathSpecs, 734 fname string, 735 f ts.ConsolidationFunc, 736 ) (ts.SeriesList, error) { 737 if len(series.Values) == 0 { // no data; no work 738 return ts.SeriesList(series), nil 739 } 740 741 normalized, start, end, millisPerStep, err := common.Normalize(ctx, ts.SeriesList(series)) 742 if err != nil { 743 err := xerrors.NewInvalidParamsError(fmt.Errorf("combine series error: %w", err)) 744 return ts.NewSeriesList(), err 745 } 746 747 consolidation := ts.NewConsolidation(ctx, start, end, millisPerStep, f) 748 for _, s := range normalized.Values { 749 consolidation.AddSeries(s, ts.Avg) 750 } 751 752 result := consolidation.BuildSeries(fname, ts.Finalize) 753 return ts.SeriesList{ 754 Values: []*ts.Series{result}, 755 Metadata: series.Metadata, 756 }, nil 757 } 758 759 // weightedAverage takes a series of values and a series of weights and produces a weighted 760 // average for all values. The corresponding values should share a node as defined by the 761 // node parameter, 0-indexed. 762 func weightedAverage( 763 ctx *common.Context, 764 input singlePathSpec, 765 weights singlePathSpec, 766 node int, 767 ) (ts.SeriesList, error) { 768 step := math.MaxInt32 769 if len(input.Values) > 0 { 770 step = input.Values[0].MillisPerStep() 771 } else if len(weights.Values) > 0 { 772 step = weights.Values[0].MillisPerStep() 773 } else { 774 return ts.SeriesList(input), nil 775 } 776 777 for _, series := range input.Values { 778 if step != series.MillisPerStep() { 779 err := xerrors.NewInvalidParamsError(fmt.Errorf("different step sizes in input series not supported")) 780 return ts.NewSeriesList(), err 781 } 782 } 783 784 for _, series := range weights.Values { 785 if step != series.MillisPerStep() { 786 err := xerrors.NewInvalidParamsError(fmt.Errorf("different step sizes in input series not supported")) 787 return ts.NewSeriesList(), err 788 } 789 } 790 791 valuesByKey, err := aliasByNode(ctx, input, node) 792 if err != nil { 793 return ts.NewSeriesList(), err 794 } 795 weightsByKey, err := aliasByNode(ctx, weights, node) 796 if err != nil { 797 return ts.NewSeriesList(), err 798 } 799 800 type pairedSeries struct { 801 values *ts.Series 802 weights *ts.Series 803 } 804 805 keys := make(map[string]*pairedSeries, len(valuesByKey.Values)) 806 807 for _, series := range valuesByKey.Values { 808 keys[series.Name()] = &pairedSeries{values: series} 809 } 810 811 for _, series := range weightsByKey.Values { 812 if tuple, ok := keys[series.Name()]; ok { 813 tuple.weights = series 814 } 815 } 816 817 productSeries := make([]*ts.Series, 0, len(keys)) 818 consideredWeights := make([]*ts.Series, 0, len(keys)) 819 820 for key, pair := range keys { 821 822 if pair.weights == nil { 823 continue // skip - no associated weight series 824 } 825 826 vals := ts.NewValues(ctx, pair.values.MillisPerStep(), pair.values.Len()) 827 for i := 0; i < pair.values.Len(); i++ { 828 v := pair.values.ValueAt(i) 829 w := pair.weights.ValueAt(i) 830 vals.SetValueAt(i, v*w) 831 } 832 series := ts.NewSeries(ctx, key, pair.values.StartTime(), vals) 833 productSeries = append(productSeries, series) 834 consideredWeights = append(consideredWeights, pair.weights) 835 } 836 837 meta := input.Metadata.CombineMetadata(weights.Metadata) 838 top, err := sumSeries(ctx, multiplePathSpecs(ts.SeriesList{ 839 Values: productSeries, 840 Metadata: meta, 841 })) 842 if err != nil { 843 return ts.NewSeriesList(), err 844 } 845 846 bottom, err := sumSeries(ctx, multiplePathSpecs(ts.SeriesList{ 847 Values: consideredWeights, 848 Metadata: meta, 849 })) 850 if err != nil { 851 return ts.NewSeriesList(), err 852 } 853 854 results, err := divideSeries(ctx, singlePathSpec(top), singlePathSpec(bottom)) 855 if err != nil { 856 return ts.NewSeriesList(), err 857 } 858 859 return alias(ctx, singlePathSpec(results), "weightedAverage") 860 } 861 862 // countSeries draws a horizontal line representing the number of nodes found in the seriesList. 863 func countSeries(ctx *common.Context, seriesList multiplePathSpecs) (ts.SeriesList, error) { 864 count, err := common.Count(ctx, ts.SeriesList(seriesList), func(series ts.SeriesList) string { 865 return wrapPathExpr(countSeriesFnName, series) 866 }) 867 if err != nil { 868 return ts.NewSeriesList(), err 869 } 870 871 r := ts.SeriesList(seriesList) 872 r.Values = count.Values 873 return r, nil 874 }