mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-25 06:17:36 +01:00
d27dfac5c6
Previously it incorrectly applied xFilesFactor, if it's value equal to 0. This commit properly handles this case and returns result according to the graphite documentation: `xFilesFactor follows the same semantics as in Whisper storage schemas. Setting it to 0 (the default) means that only a single value in the series needs to be non-null for it to be considered non-empty, setting it to 1 means that all values in the series must be non-null. A setting of 0.5 means that at least half the values in the series must be non-null.` Signed-off-by: f41gh7 <nik@victoriametrics.com> Co-authored-by: Evgeniy Negriy <einegriy@avito.ru>
5685 lines
167 KiB
Go
5685 lines
167 KiB
Go
package graphite
|
|
|
|
import (
|
|
"container/heap"
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphiteql"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
|
)
|
|
|
|
// nextSeriesFunc must return the next series to process.
|
|
//
|
|
// nextSeriesFunc must release all the occupied resources before returning non-nil error.
|
|
// drainAllSeries can be used for releasing occupied resources.
|
|
//
|
|
// When there are no more series to return, (nil, nil) must be returned.
|
|
type nextSeriesFunc func() (*series, error)
|
|
|
|
type transformFunc func(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error)
|
|
|
|
var transformFuncs = map[string]transformFunc{}
|
|
|
|
func init() {
|
|
// A workaround for https://github.com/golang/go/issues/43741
|
|
transformFuncs = map[string]transformFunc{
|
|
"absolute": transformAbsolute,
|
|
"add": transformAdd,
|
|
"aggregate": transformAggregate,
|
|
"aggregateLine": transformAggregateLine,
|
|
"aggregateSeriesLists": transformAggregateSeriesLists,
|
|
"aggregateWithWildcards": transformAggregateWithWildcards,
|
|
"alias": transformAlias,
|
|
"aliasByMetric": transformAliasByMetric,
|
|
"aliasByNode": transformAliasByNode,
|
|
"aliasByTags": transformAliasByNode,
|
|
"aliasQuery": transformAliasQuery,
|
|
"aliasSub": transformAliasSub,
|
|
"alpha": transformAlpha,
|
|
"applyByNode": transformApplyByNode,
|
|
"areaBetween": transformAreaBetween,
|
|
"asPercent": transformAsPercent,
|
|
"averageAbove": transformAverageAbove,
|
|
"averageBelow": transformAverageBelow,
|
|
"averageOutsidePercentile": transformAverageOutsidePercentile,
|
|
"averageSeries": transformAverageSeries,
|
|
"averageSeriesWithWildcards": transformAverageSeriesWithWildcards,
|
|
"avg": transformAverageSeries,
|
|
"cactiStyle": transformTODO,
|
|
"changed": transformChanged,
|
|
"color": transformColor,
|
|
"consolidateBy": transformConsolidateBy,
|
|
"constantLine": transformConstantLine,
|
|
"countSeries": transformCountSeries,
|
|
"cumulative": transformCumulative,
|
|
"currentAbove": transformCurrentAbove,
|
|
"currentBelow": transformCurrentBelow,
|
|
"dashed": transformDashed,
|
|
"delay": transformDelay,
|
|
"derivative": transformDerivative,
|
|
"diffSeries": transformDiffSeries,
|
|
"diffSeriesLists": transformDiffSeriesLists,
|
|
"divideSeries": transformDivideSeries,
|
|
"divideSeriesLists": transformDivideSeriesLists,
|
|
"drawAsInfinite": transformDrawAsInfinite,
|
|
"events": transformEvents,
|
|
"exclude": transformExclude,
|
|
"exp": transformExp,
|
|
"exponentialMovingAverage": transformExponentialMovingAverage,
|
|
"fallbackSeries": transformFallbackSeries,
|
|
"filterSeries": transformFilterSeries,
|
|
"grep": transformGrep,
|
|
"group": transformGroup,
|
|
"groupByNode": transformGroupByNode,
|
|
"groupByNodes": transformGroupByNodes,
|
|
"groupByTags": transformGroupByTags,
|
|
"highest": transformHighest,
|
|
"highestAverage": transformHighestAverage,
|
|
"highestCurrent": transformHighestCurrent,
|
|
"highestMax": transformHighestMax,
|
|
"hitcount": transformHitcount,
|
|
"holtWintersAberration": transformHoltWintersAberration,
|
|
"holtWintersConfidenceArea": transformHoltWintersConfidenceArea,
|
|
"holtWintersConfidenceBands": transformHoltWintersConfidenceBands,
|
|
"holtWintersForecast": transformHoltWintersForecast,
|
|
"identity": transformIdentity,
|
|
"integral": transformIntegral,
|
|
"integralByInterval": transformIntegralByInterval,
|
|
"interpolate": transformInterpolate,
|
|
"invert": transformInvert,
|
|
"isNonNull": transformIsNonNull,
|
|
"keepLastValue": transformKeepLastValue,
|
|
"legendValue": transformTODO,
|
|
"limit": transformLimit,
|
|
"lineWidth": transformLineWidth,
|
|
"linearRegression": transformLinearRegression,
|
|
"log": transformLogarithm,
|
|
"logarithm": transformLogarithm,
|
|
"logit": transformLogit,
|
|
"lowest": transformLowest,
|
|
"lowestAverage": transformLowestAverage,
|
|
"lowestCurrent": transformLowestCurrent,
|
|
"map": transformTODO,
|
|
"mapSeries": transformTODO,
|
|
"max": transformMaxSeries,
|
|
"maxSeries": transformMaxSeries,
|
|
"maximumAbove": transformMaximumAbove,
|
|
"maximumBelow": transformMaximumBelow,
|
|
"minMax": transformMinMax,
|
|
"min": transformMinSeries,
|
|
"minSeries": transformMinSeries,
|
|
"minimumAbove": transformMinimumAbove,
|
|
"minimumBelow": transformMinimumBelow,
|
|
"mostDeviant": transformMostDeviant,
|
|
"movingAverage": transformMovingAverage,
|
|
"movingMax": transformMovingMax,
|
|
"movingMedian": transformMovingMedian,
|
|
"movingMin": transformMovingMin,
|
|
"movingSum": transformMovingSum,
|
|
"movingWindow": transformMovingWindow,
|
|
"multiplySeries": transformMultiplySeries,
|
|
"multiplySeriesLists": transformMultiplySeriesLists,
|
|
"multiplySeriesWithWildcards": transformMultiplySeriesWithWildcards,
|
|
"nPercentile": transformNPercentile,
|
|
"nonNegativeDerivative": transformNonNegativeDerivative,
|
|
"offset": transformOffset,
|
|
"offsetToZero": transformOffsetToZero,
|
|
"perSecond": transformPerSecond,
|
|
"percentileOfSeries": transformPercentileOfSeries,
|
|
// It looks like pie* functions aren't needed for Graphite render API
|
|
// "pieAverage": transformTODO,
|
|
// "pieMaximum": transformTODO,
|
|
// "pieMinimum": transformTODO,
|
|
"pow": transformPow,
|
|
"powSeries": transformPowSeries,
|
|
"randomWalk": transformRandomWalk,
|
|
"randomWalkFunction": transformRandomWalk,
|
|
"rangeOfSeries": transformRangeOfSeries,
|
|
"reduce": transformTODO,
|
|
"reduceSeries": transformTODO,
|
|
"removeAbovePercentile": transformRemoveAbovePercentile,
|
|
"removeAboveValue": transformRemoveAboveValue,
|
|
"removeBelowPercentile": transformRemoveBelowPercentile,
|
|
"removeBelowValue": transformRemoveBelowValue,
|
|
"removeBetweenPercentile": transformRemoveBetweenPercentile,
|
|
"removeEmptySeries": transformRemoveEmptySeries,
|
|
"round": transformRoundFunction,
|
|
"roundFunction": transformRoundFunction,
|
|
"scale": transformScale,
|
|
"scaleToSeconds": transformScaleToSeconds,
|
|
"secondYAxis": transformSecondYAxis,
|
|
"seriesByTag": transformSeriesByTag,
|
|
"setXFilesFactor": transformSetXFilesFactor,
|
|
"sigmoid": transformSigmoid,
|
|
"sin": transformSinFunction,
|
|
"sinFunction": transformSinFunction,
|
|
"smartSummarize": transformSmartSummarize,
|
|
"sortBy": transformSortBy,
|
|
"sortByMaxima": transformSortByMaxima,
|
|
"sortByMinima": transformSortByMinima,
|
|
"sortByName": transformSortByName,
|
|
"sortByTotal": transformSortByTotal,
|
|
"squareRoot": transformSquareRoot,
|
|
"stacked": transformStacked,
|
|
"stddevSeries": transformStddevSeries,
|
|
"stdev": transformStdev,
|
|
"substr": transformSubstr,
|
|
"sum": transformSumSeries,
|
|
"sumSeries": transformSumSeries,
|
|
"sumSeriesLists": transformSumSeriesLists,
|
|
"sumSeriesWithWildcards": transformSumSeriesWithWildcards,
|
|
"summarize": transformSummarize,
|
|
"threshold": transformThreshold,
|
|
"time": transformTimeFunction,
|
|
"timeFunction": transformTimeFunction,
|
|
"timeShift": transformTimeShift,
|
|
"timeSlice": transformTimeSlice,
|
|
"timeStack": transformTimeStack,
|
|
"transformNull": transformTransformNull,
|
|
"unique": transformUnique,
|
|
"useSeriesAbove": transformUseSeriesAbove,
|
|
"verticalLine": transformVerticalLine,
|
|
"weightedAverage": transformWeightedAverage,
|
|
"xFilesFactor": transformSetXFilesFactor,
|
|
}
|
|
}
|
|
|
|
func transformTODO(_ *evalConfig, _ *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return nil, fmt.Errorf("TODO: implement this function")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.absolute
|
|
func transformAbsolute(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = math.Abs(v)
|
|
}
|
|
s.Name = fmt.Sprintf("absolute(%s)", s.Name)
|
|
s.Tags["absolute"] = "1"
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.add
|
|
func transformAdd(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "constant", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nString := fmt.Sprintf("%g", n)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i := range values {
|
|
values[i] += n
|
|
}
|
|
s.Tags["add"] = nString
|
|
s.Name = fmt.Sprintf("add(%s,%s)", s.Name, nString)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.aggregate
|
|
func transformAggregate(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 && len(args) != 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2 or 3", len(args))
|
|
}
|
|
funcName, err := getString(args, "func", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
funcName = strings.TrimSuffix(funcName, "Series")
|
|
xFilesFactor, err := getOptionalNumber(args, "xFilesFactor", 2, ec.xFilesFactor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return aggregateSeries(ec, fe, nextSeries, funcName, xFilesFactor)
|
|
}
|
|
|
|
func aggregateSeries(ec *evalConfig, expr graphiteql.Expr, nextSeries nextSeriesFunc, funcName string, xFilesFactor float64) (nextSeriesFunc, error) {
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
as, err := newAggrState(ec.pointsLen(step), funcName)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
nextSeriesWrapper := getNextSeriesWrapperForAggregateFunc(funcName)
|
|
var seriesTags []map[string]string
|
|
var seriesExpressions []string
|
|
var mu sync.Mutex
|
|
f := nextSeriesWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
mu.Lock()
|
|
as.Update(s.Values)
|
|
seriesTags = append(seriesTags, s.Tags)
|
|
seriesExpressions = append(seriesExpressions, s.pathExpression)
|
|
mu.Unlock()
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(seriesTags) == 0 {
|
|
return newZeroSeriesFunc(), nil
|
|
}
|
|
tags := seriesTags[0]
|
|
for _, m := range seriesTags[1:] {
|
|
for k, v := range tags {
|
|
if m[k] != v {
|
|
delete(tags, k)
|
|
}
|
|
}
|
|
}
|
|
name := formatAggrFuncForSeriesNames(funcName, seriesExpressions)
|
|
tags["aggregatedBy"] = funcName
|
|
if tags["name"] == "" {
|
|
tags["name"] = name
|
|
}
|
|
s := &series{
|
|
Name: name,
|
|
Tags: tags,
|
|
Timestamps: ec.newTimestamps(step),
|
|
Values: as.Finalize(xFilesFactor),
|
|
pathExpression: name,
|
|
expr: expr,
|
|
step: step,
|
|
}
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
func aggregateSeriesGeneric(ec *evalConfig, fe *graphiteql.FuncExpr, funcName string) (nextSeriesFunc, error) {
|
|
nextSeries, err := groupSeriesLists(ec, fe.Args, fe)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return aggregateSeries(ec, fe, nextSeries, funcName, ec.xFilesFactor)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.aggregateLine
|
|
func transformAggregateLine(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1, 2 or 3", len(args))
|
|
}
|
|
funcName, err := getOptionalString(args, "func", 1, "avg")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keepStep, err := getOptionalBool(args, "keepStep", 2, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
v := aggrFunc(values)
|
|
if keepStep {
|
|
for i := range values {
|
|
values[i] = v
|
|
}
|
|
} else {
|
|
s.Timestamps = []int64{ec.startTime, (ec.endTime + ec.startTime) / 2, ec.endTime}
|
|
s.Values = []float64{v, v, v}
|
|
}
|
|
vString := "None"
|
|
if !math.IsNaN(v) {
|
|
vString = fmt.Sprintf("%g", v)
|
|
}
|
|
s.Name = fmt.Sprintf("aggregateLine(%s,%s)", s.Name, vString)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.aggregateWithWildcards
|
|
func transformAggregateWithWildcards(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want at least 2", len(args))
|
|
}
|
|
funcName, err := getString(args, "func", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
positions, err := getInts(args[2:], "positions")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return aggregateSeriesWithWildcards(ec, fe, nextSeries, funcName, positions)
|
|
}
|
|
|
|
func aggregateSeriesWithWildcards(ec *evalConfig, expr graphiteql.Expr, nextSeries nextSeriesFunc, funcName string, positions []int) (nextSeriesFunc, error) {
|
|
positionsMap := make(map[int]struct{})
|
|
for _, pos := range positions {
|
|
positionsMap[pos] = struct{}{}
|
|
}
|
|
keyFunc := func(name string, _ map[string]string) string {
|
|
parts := strings.Split(getPathFromName(name), ".")
|
|
dstParts := parts[:0]
|
|
for i, part := range parts {
|
|
if _, ok := positionsMap[i]; ok {
|
|
continue
|
|
}
|
|
dstParts = append(dstParts, part)
|
|
}
|
|
return strings.Join(dstParts, ".")
|
|
}
|
|
return groupByKeyFunc(ec, expr, nextSeries, funcName, keyFunc)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.alias
|
|
func transformAlias(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
newName, err := getString(args, "newName", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.Name = newName
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.aliasByMetric
|
|
func transformAliasByMetric(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
path := getPathFromName(s.Name)
|
|
n := strings.LastIndexByte(path, '.')
|
|
if n > 0 {
|
|
path = path[n+1:]
|
|
}
|
|
s.Name = path
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.aliasByNode
|
|
func transformAliasByNode(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want at least 1", len(args))
|
|
}
|
|
nodes, err := getNodes(args[1:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.Name = getNameFromNodes(s.Name, s.Tags, nodes)
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.aliasQuery
|
|
func transformAliasQuery(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 4 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 4", len(args))
|
|
}
|
|
re, err := getRegexp(args, "search", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replace, err := getRegexpReplacement(args, "replace", 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newName, err := getString(args, "newName", 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
query := re.ReplaceAllString(s.Name, replace)
|
|
next, err := execExpr(ec, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot evaluate query %q: %w", query, err)
|
|
}
|
|
ss, err := fetchAllSeries(next)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot fetch series for query %q: %w", query, err)
|
|
}
|
|
if len(ss) == 0 {
|
|
return nil, fmt.Errorf("cannot find series for query %q", query)
|
|
}
|
|
v := aggrLast(ss[0].Values)
|
|
if math.IsNaN(v) {
|
|
return nil, fmt.Errorf("cannot find values for query %q", query)
|
|
}
|
|
name := strings.ReplaceAll(newName, "%d", fmt.Sprintf("%d", int(v)))
|
|
name = strings.ReplaceAll(name, "%g", fmt.Sprintf("%g", v))
|
|
name = strings.ReplaceAll(name, "%f", fmt.Sprintf("%f", v))
|
|
s.Name = name
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.aliasSub
|
|
func transformAliasSub(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 3", len(args))
|
|
}
|
|
re, err := getRegexp(args, "search", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replace, err := getRegexpReplacement(args, "replace", 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.Name = re.ReplaceAllString(s.Name, replace)
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.alpha
|
|
func transformAlpha(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
_, err := getNumber(args, "alpha", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.applyByNode
|
|
func transformApplyByNode(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 3 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want from 3 to 4", len(args))
|
|
}
|
|
nn, err := getNumber(args, "nodeNum", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodeNum := int(nn)
|
|
templateFunction, err := getString(args, "templateFunction", 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newName, err := getOptionalString(args, "newName", 3, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextTemplateSeries := newZeroSeriesFunc()
|
|
prefix := ""
|
|
visitedPrefixes := make(map[string]struct{})
|
|
f := func() (*series, error) {
|
|
for {
|
|
ts, err := nextTemplateSeries()
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
if ts != nil {
|
|
if newName != "" {
|
|
ts.Name = strings.ReplaceAll(newName, "%", prefix)
|
|
}
|
|
ts.expr = fe
|
|
ts.pathExpression = prefix
|
|
return ts, nil
|
|
}
|
|
for {
|
|
s, err := nextSeries()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s == nil {
|
|
return nil, nil
|
|
}
|
|
prefix = getPathFromName(s.Name)
|
|
nodes := strings.Split(prefix, ".")
|
|
if nodeNum >= 0 && nodeNum < len(nodes) {
|
|
prefix = strings.Join(nodes[:nodeNum+1], ".")
|
|
}
|
|
if _, ok := visitedPrefixes[prefix]; !ok {
|
|
visitedPrefixes[prefix] = struct{}{}
|
|
break
|
|
}
|
|
}
|
|
query := strings.ReplaceAll(templateFunction, "%", prefix)
|
|
next, err := execExpr(ec, query)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
nextTemplateSeries = next
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.areaBetween
|
|
func transformAreaBetween(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seriesFound := 0
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
seriesFound++
|
|
if seriesFound > 2 {
|
|
return nil, fmt.Errorf("expecting exactly two series; got more series")
|
|
}
|
|
s.Tags["areaBetween"] = "1"
|
|
s.Name = fmt.Sprintf("areaBetween(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.asPercent
|
|
func transformAsPercent(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want at least 1", len(args))
|
|
}
|
|
totalArg := getOptionalArg(args, "total", 1)
|
|
if totalArg == nil {
|
|
totalArg = &graphiteql.ArgExpr{
|
|
Expr: &graphiteql.NoneExpr{},
|
|
}
|
|
}
|
|
var nodes []graphiteql.Expr
|
|
if len(args) > 2 {
|
|
ns, err := getNodes(args[2:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes = ns
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch t := totalArg.Expr.(type) {
|
|
case *graphiteql.NoneExpr:
|
|
if len(nodes) == 0 {
|
|
ss, step, err := fetchNormalizedSeries(ec, nextSeries, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inplacePercentForMultiSeries(ec, fe, ss, step)
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
m, step, err := fetchNormalizedSeriesByNodes(ec, nextSeries, nodes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var ssAll []*series
|
|
for _, ss := range m {
|
|
inplacePercentForMultiSeries(ec, fe, ss, step)
|
|
ssAll = append(ssAll, ss...)
|
|
}
|
|
return multiSeriesFunc(ssAll), nil
|
|
case *graphiteql.NumberExpr:
|
|
if len(nodes) > 0 {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, fmt.Errorf("unexpected non-empty nodes for numeric total")
|
|
}
|
|
total := t.N
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = v / total * 100
|
|
}
|
|
s.Name = fmt.Sprintf("asPercent(%s,%g)", s.Name, total)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
default:
|
|
nextTotal, err := evalExpr(ec, t)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
if len(nodes) == 0 {
|
|
// Fetch series serially in order to preserve the original order of series returned by nextTotal,
|
|
// so the returned series could be matched against series returned by nextSeries.
|
|
ssTotal, stepTotal, err := fetchNormalizedSeries(ec, nextTotal, false)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
if len(ssTotal) == 0 {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
// The `total` expression matches zero series. Return empty response in this case.
|
|
return multiSeriesFunc(nil), nil
|
|
}
|
|
if len(ssTotal) == 1 {
|
|
sTotal := ssTotal[0]
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, stepTotal)
|
|
inplacePercentForSingleSeries(fe, s, sTotal)
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
// Fetch series serially in order to preserve the original order of series returned by nextSeries
|
|
// and match these series to ssTotal
|
|
ss, step, err := fetchNormalizedSeries(ec, nextSeries, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ss) != len(ssTotal) {
|
|
return nil, fmt.Errorf("unexpected number of series returned by total expression; got %d; want %d", len(ssTotal), len(ss))
|
|
}
|
|
if step != stepTotal {
|
|
return nil, fmt.Errorf("step mismatch for series and total series: %d vs %d", step, stepTotal)
|
|
}
|
|
for i, s := range ss {
|
|
inplacePercentForSingleSeries(fe, s, ssTotal[i])
|
|
}
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
m, step, err := fetchNormalizedSeriesByNodes(ec, nextSeries, nodes)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextTotal)
|
|
return nil, err
|
|
}
|
|
mTotal, stepTotal, err := fetchNormalizedSeriesByNodes(ec, nextTotal, nodes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if step != stepTotal {
|
|
return nil, fmt.Errorf("step mismatch for series and total series: %d vs %d", step, stepTotal)
|
|
}
|
|
var ssAll []*series
|
|
for key, ssTotal := range mTotal {
|
|
seriesExpressions := make([]string, 0, len(ssTotal))
|
|
as := newAggrStateSum(ec.pointsLen(step))
|
|
for _, s := range ssTotal {
|
|
seriesExpressions = append(seriesExpressions, s.pathExpression)
|
|
as.Update(s.Values)
|
|
}
|
|
totalValues := as.Finalize(ec.xFilesFactor)
|
|
totalName := formatAggrFuncForPercentSeriesNames("sum", seriesExpressions)
|
|
ss := m[key]
|
|
if ss == nil {
|
|
s := newNaNSeries(ec, step)
|
|
newName := fmt.Sprintf("asPercent(MISSING,%s)", totalName)
|
|
s.Name = newName
|
|
s.Tags["name"] = newName
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
ssAll = append(ssAll, s)
|
|
continue
|
|
}
|
|
for _, s := range ss {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = v / totalValues[i] * 100
|
|
}
|
|
newName := fmt.Sprintf("asPercent(%s,%s)", s.Name, totalName)
|
|
s.Name = newName
|
|
s.Tags["name"] = newName
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
ssAll = append(ssAll, s)
|
|
}
|
|
}
|
|
for key, ss := range m {
|
|
ssTotal := mTotal[key]
|
|
if ssTotal != nil {
|
|
continue
|
|
}
|
|
for _, s := range ss {
|
|
values := s.Values
|
|
for i := range values {
|
|
values[i] = nan
|
|
}
|
|
newName := fmt.Sprintf("asPercent(%s,MISSING)", s.Name)
|
|
s.Name = newName
|
|
s.Tags["name"] = newName
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
ssAll = append(ssAll, s)
|
|
}
|
|
}
|
|
return multiSeriesFunc(ssAll), nil
|
|
}
|
|
}
|
|
|
|
func inplacePercentForSingleSeries(expr graphiteql.Expr, s, sTotal *series) {
|
|
values := s.Values
|
|
totalValues := sTotal.Values
|
|
for i, v := range values {
|
|
values[i] = v / totalValues[i] * 100
|
|
}
|
|
newName := fmt.Sprintf("asPercent(%s,%s)", s.Name, sTotal.Name)
|
|
s.Name = newName
|
|
s.Tags["name"] = newName
|
|
s.expr = expr
|
|
s.pathExpression = s.Name
|
|
}
|
|
|
|
func inplacePercentForMultiSeries(ec *evalConfig, expr graphiteql.Expr, ss []*series, step int64) {
|
|
seriesExpressions := make([]string, 0, len(ss))
|
|
as := newAggrStateSum(ec.pointsLen(step))
|
|
for _, s := range ss {
|
|
seriesExpressions = append(seriesExpressions, s.pathExpression)
|
|
as.Update(s.Values)
|
|
}
|
|
totalValues := as.Finalize(ec.xFilesFactor)
|
|
totalName := formatAggrFuncForPercentSeriesNames("sum", seriesExpressions)
|
|
for _, s := range ss {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = v / totalValues[i] * 100
|
|
}
|
|
newName := fmt.Sprintf("asPercent(%s,%s)", s.Name, totalName)
|
|
s.Name = newName
|
|
s.Tags["name"] = newName
|
|
s.expr = expr
|
|
s.pathExpression = s.Name
|
|
}
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.averageAbove
|
|
func transformAverageAbove(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "average", ">", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.averageBelow
|
|
func transformAverageBelow(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "average", "<", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.averageOutsidePercentile
|
|
func transformAverageOutsidePercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var sws []seriesWithWeight
|
|
var lock sync.Mutex
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
avg := aggrAvg(s.Values)
|
|
lock.Lock()
|
|
sws = append(sws, seriesWithWeight{
|
|
s: s,
|
|
v: avg,
|
|
})
|
|
lock.Unlock()
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
avgs := make([]float64, len(sws))
|
|
for i, sw := range sws {
|
|
avgs[i] = sw.v
|
|
}
|
|
if n > 50 {
|
|
n = 100 - n
|
|
}
|
|
lowPercentile := n
|
|
highPercentile := 100 - n
|
|
lowValue := newAggrFuncPercentile(lowPercentile)(avgs)
|
|
highValue := newAggrFuncPercentile(highPercentile)(avgs)
|
|
var ss []*series
|
|
for _, sw := range sws {
|
|
if sw.v < lowValue || sw.v > highValue {
|
|
s := sw.s
|
|
s.expr = fe
|
|
ss = append(ss, s)
|
|
}
|
|
}
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.averageSeries
|
|
func transformAverageSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "average")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.averageSeriesWithWildcards
|
|
func transformAverageSeriesWithWildcards(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesWithWildcardsGeneric(ec, fe, "average")
|
|
}
|
|
|
|
func aggregateSeriesWithWildcardsGeneric(ec *evalConfig, fe *graphiteql.FuncExpr, funcName string) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; must be at least 1", len(args))
|
|
}
|
|
positions, err := getInts(args[1:], "position")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return aggregateSeriesWithWildcards(ec, fe, nextSeries, funcName, positions)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.changed
|
|
func transformChanged(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("expecting a single arg; got %d args", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
prevValue := nan
|
|
for i, v := range values {
|
|
if math.IsNaN(prevValue) {
|
|
prevValue = v
|
|
values[i] = 0
|
|
} else if !math.IsNaN(v) && prevValue != v {
|
|
prevValue = v
|
|
values[i] = 1
|
|
} else {
|
|
values[i] = 0
|
|
}
|
|
}
|
|
s.Name = fmt.Sprintf("changed(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.color
|
|
func transformColor(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
_, err := getString(args, "theColor", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.countSeries
|
|
func transformCountSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "count")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.cumulative
|
|
func transformCumulative(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return consolidateBy(fe, nextSeries, "sum")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.consolidateBy
|
|
func transformConsolidateBy(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
funcName, err := getString(args, "consolidationFunc", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return consolidateBy(fe, nextSeries, funcName)
|
|
}
|
|
|
|
func consolidateBy(expr graphiteql.Expr, nextSeries nextSeriesFunc, funcName string) (nextSeriesFunc, error) {
|
|
consolidateFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidateFunc = consolidateFunc
|
|
s.Name = fmt.Sprintf("consolidateBy(%s,%s)", s.Name, graphiteql.QuoteString(funcName))
|
|
s.Tags["consolidateBy"] = funcName
|
|
s.expr = expr
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.constantLine
|
|
func transformConstantLine(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("expecting a single arg; got %d", len(args))
|
|
}
|
|
n, err := getNumber(args, "value", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return constantLine(ec, fe, n), nil
|
|
}
|
|
|
|
func constantLine(ec *evalConfig, expr graphiteql.Expr, n float64) nextSeriesFunc {
|
|
name := fmt.Sprintf("%g", n)
|
|
step := (ec.endTime - ec.startTime) / 2
|
|
s := &series{
|
|
Name: name,
|
|
Tags: unmarshalTags(name),
|
|
Timestamps: []int64{ec.startTime, ec.startTime + step, ec.startTime + 2*step},
|
|
Values: []float64{n, n, n},
|
|
expr: expr,
|
|
pathExpression: string(expr.AppendString(nil)),
|
|
step: step,
|
|
}
|
|
return singleSeriesFunc(s)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.currentAbove
|
|
func transformCurrentAbove(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "current", ">", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.currentBelow
|
|
func transformCurrentBelow(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "current", "<", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.dashed
|
|
func transformDashed(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1 or 2", len(args))
|
|
}
|
|
dashLength, err := getOptionalNumber(args, "dashLength", 1, 5)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dashLengthStr := fmt.Sprintf("%g", dashLength)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.Name = fmt.Sprintf("dashed(%s,%s)", s.Name, dashLengthStr)
|
|
s.Tags["dashed"] = dashLengthStr
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.delay
|
|
func transformDelay(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
stepsFloat, err := getNumber(args, "steps", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
steps := int(stepsFloat)
|
|
stepsStr := fmt.Sprintf("%d", steps)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
stepsLocal := steps
|
|
if stepsLocal < 0 {
|
|
stepsLocal = -stepsLocal
|
|
if stepsLocal > len(values) {
|
|
stepsLocal = len(values)
|
|
}
|
|
copy(values, values[stepsLocal:])
|
|
for i := len(values) - 1; i >= len(values)-stepsLocal; i-- {
|
|
values[i] = nan
|
|
}
|
|
} else {
|
|
if stepsLocal > len(values) {
|
|
stepsLocal = len(values)
|
|
}
|
|
copy(values[stepsLocal:], values[:len(values)-stepsLocal])
|
|
for i := 0; i < stepsLocal; i++ {
|
|
values[i] = nan
|
|
}
|
|
}
|
|
s.Tags["delay"] = stepsStr
|
|
s.Name = fmt.Sprintf("delay(%s,%d)", s.Name, steps)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.derivative
|
|
func transformDerivative(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
prevValue := nan
|
|
for i, v := range values {
|
|
if math.IsNaN(prevValue) || math.IsNaN(v) {
|
|
values[i] = nan
|
|
} else {
|
|
values[i] = v - prevValue
|
|
}
|
|
prevValue = v
|
|
}
|
|
s.Tags["derivative"] = "1"
|
|
s.Name = fmt.Sprintf("derivative(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.diffSeries
|
|
func transformDiffSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "diff")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.divideSeries
|
|
func transformDivideSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
nextDivisor, err := evalSeriesList(ec, args, "divisorSeries", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ssDivisors, stepDivisor, err := fetchNormalizedSeries(ec, nextDivisor, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ssDivisors) > 1 {
|
|
return nil, fmt.Errorf("unexpected number of divisorSeries; got %d; want 1", len(ssDivisors))
|
|
}
|
|
nextDividend, err := evalSeriesList(ec, args, "dividendSeriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ssDivisors) == 0 {
|
|
f := nextSeriesConcurrentWrapper(nextDividend, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i := range values {
|
|
values[i] = nan
|
|
}
|
|
s.Name = fmt.Sprintf("divideSeries(%s,MISSING)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
sDivisor := ssDivisors[0]
|
|
divisorName := sDivisor.Name
|
|
divisorValues := sDivisor.Values
|
|
f := nextSeriesSerialWrapper(nextDividend, func(s *series) (*series, error) {
|
|
s.consolidate(ec, stepDivisor)
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = v / divisorValues[i]
|
|
}
|
|
s.Name = fmt.Sprintf("divideSeries(%s,%s)", s.Name, divisorName)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
func aggregateSeriesListsGeneric(ec *evalConfig, fe *graphiteql.FuncExpr, funcName string) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
agg, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeriesFirst, err := evalSeriesList(ec, args, "seriesListFirstPos", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeriesSecond, err := evalSeriesList(ec, args, "seriesListSecondPos", 1)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeriesFirst)
|
|
return nil, err
|
|
}
|
|
return aggregateSeriesList(ec, fe, nextSeriesFirst, nextSeriesSecond, agg, funcName)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/latest/functions.html#graphite.render.functions.aggregateSeriesLists
|
|
func transformAggregateSeriesLists(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 3 && len(args) != 4 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 3 or 4", len(args))
|
|
}
|
|
|
|
funcName, err := getString(args, "func", 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return aggregateSeriesListsGeneric(ec, fe, funcName)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/latest/functions.html#graphite.render.functions.sumSeriesLists
|
|
func transformSumSeriesLists(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesListsGeneric(ec, fe, "sum")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/latest/functions.html#graphite.render.functions.multiplySeriesLists
|
|
func transformMultiplySeriesLists(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesListsGeneric(ec, fe, "multiply")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/latest/functions.html#graphite.render.functions.diffSeriesLists
|
|
func transformDiffSeriesLists(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesListsGeneric(ec, fe, "diff")
|
|
}
|
|
|
|
func aggregateSeriesList(ec *evalConfig, fe *graphiteql.FuncExpr, nextSeriesFirst, nextSeriesSecond nextSeriesFunc, agg aggrFunc, funcName string) (nextSeriesFunc, error) {
|
|
ssFirst, stepFirst, err := fetchNormalizedSeries(ec, nextSeriesFirst, false)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeriesSecond)
|
|
return nil, err
|
|
}
|
|
ssSecond, stepSecond, err := fetchNormalizedSeries(ec, nextSeriesSecond, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(ssFirst) != len(ssSecond) {
|
|
return nil, fmt.Errorf("First and second lists must have equal number of series; got %d vs %d series", len(ssFirst), len(ssSecond))
|
|
}
|
|
if stepFirst != stepSecond {
|
|
return nil, fmt.Errorf("step mismatch for first and second: %d vs %d", stepFirst, stepSecond)
|
|
}
|
|
|
|
valuePair := make([]float64, 2)
|
|
for i, s := range ssFirst {
|
|
sSecond := ssSecond[i]
|
|
values := s.Values
|
|
secondValues := sSecond.Values
|
|
for j, v := range values {
|
|
valuePair[0], valuePair[1] = v, secondValues[j]
|
|
values[j] = agg(valuePair)
|
|
}
|
|
s.Name = fmt.Sprintf("%sSeries(%s,%s)", funcName, s.Name, sSecond.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
}
|
|
return multiSeriesFunc(ssFirst), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.divideSeriesLists
|
|
func transformDivideSeriesLists(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
nextDividend, err := evalSeriesList(ec, args, "dividendSeriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextDivisor, err := evalSeriesList(ec, args, "divisorSeriesList", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return aggregateSeriesList(ec, fe, nextDividend, nextDivisor, func(values []float64) float64 {
|
|
return values[0] / values[1]
|
|
}, "divide")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.drawAsInfinite
|
|
func transformDrawAsInfinite(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.Tags["drawAsInfinite"] = "1"
|
|
s.Name = fmt.Sprintf("drawAsInfinite(%s)", s.Name)
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.events
|
|
func transformEvents(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
var tags []string
|
|
for _, arg := range args {
|
|
se, ok := arg.Expr.(*graphiteql.StringExpr)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expecting string tag; got %T", arg.Expr)
|
|
}
|
|
tags = append(tags, graphiteql.QuoteString(se.S))
|
|
}
|
|
s := newNaNSeries(ec, ec.storageStep)
|
|
events := fmt.Sprintf("events(%s)", strings.Join(tags, ","))
|
|
s.Name = events
|
|
s.Tags = map[string]string{"name": events}
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.exclude
|
|
func transformExclude(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("expecting two args; got %d args", len(args))
|
|
}
|
|
pattern, err := getRegexp(args, "pattern", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
if pattern.MatchString(s.Name) {
|
|
return nil, nil
|
|
}
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.exp
|
|
func transformExp(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("expecting one arg; got %d args", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = math.Exp(v)
|
|
}
|
|
s.Tags["exp"] = "e"
|
|
s.Name = fmt.Sprintf("exp(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.exponentialMovingAverage
|
|
func transformExponentialMovingAverage(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
windowSizeArg, err := getArg(args, "windowSize", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
windowSizeStr := string(windowSizeArg.Expr.AppendString(nil))
|
|
var c float64
|
|
var windowSize int64
|
|
switch t := windowSizeArg.Expr.(type) {
|
|
case *graphiteql.StringExpr:
|
|
ws, err := parseInterval(t.S)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse windowSize: %w", err)
|
|
}
|
|
c = 2 / (float64(ws)/1000 + 1)
|
|
windowSize = ws
|
|
case *graphiteql.NumberExpr:
|
|
c = 2 / (t.N + 1)
|
|
windowSize = int64(t.N * float64(ec.storageStep))
|
|
default:
|
|
return nil, fmt.Errorf("windowSize must be either string or number; got %T", t)
|
|
}
|
|
if windowSize < 0 {
|
|
windowSize = -windowSize
|
|
}
|
|
|
|
ecCopy := *ec
|
|
ecCopy.startTime -= windowSize
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
timestamps := s.Timestamps
|
|
i := 0
|
|
for i < len(timestamps) && timestamps[i] < ec.startTime {
|
|
i++
|
|
}
|
|
ema := aggrAvg(s.Values[:i])
|
|
if math.IsNaN(ema) {
|
|
ema = 0
|
|
}
|
|
values := s.Values[i:]
|
|
timestamps = timestamps[i:]
|
|
for i, v := range values {
|
|
ema = c*v + (1-c)*ema
|
|
values[i] = ema
|
|
}
|
|
s.Timestamps = append([]int64{}, timestamps...)
|
|
s.Values = append([]float64{}, values...)
|
|
s.Tags["exponentialMovingAverage"] = windowSizeStr
|
|
s.Name = fmt.Sprintf("exponentialMovingAverage(%s,%s)", s.Name, windowSizeStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.fallbackSeries
|
|
func transformFallbackSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of arg; got %d; want 2", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seriesFetched := 0
|
|
fallbackUsed := false
|
|
f := func() (*series, error) {
|
|
for {
|
|
s, err := nextSeries()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s != nil {
|
|
seriesFetched++
|
|
s.expr = fe
|
|
return s, nil
|
|
}
|
|
if fallbackUsed || seriesFetched > 0 {
|
|
return nil, nil
|
|
}
|
|
fallback, err := evalSeriesList(ec, args, "fallback", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries = fallback
|
|
fallbackUsed = true
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.filterSeries
|
|
func transformFilterSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 4 {
|
|
return nil, fmt.Errorf("unexpected number of arg; got %d; want 4", len(args))
|
|
}
|
|
funcName, err := getString(args, "func", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
operator, err := getString(args, "operator", 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
threshold, err := getNumber(args, "threshold", 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, funcName, operator, threshold)
|
|
}
|
|
|
|
func filterSeriesGeneric(expr graphiteql.Expr, nextSeries nextSeriesFunc, funcName, operator string, threshold float64) (nextSeriesFunc, error) {
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
operatorFunc, err := getOperatorFunc(operator)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
v := aggrFunc(s.Values)
|
|
if !operatorFunc(v, threshold) {
|
|
return nil, nil
|
|
}
|
|
s.expr = expr
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
func getOperatorFunc(operator string) (operatorFunc, error) {
|
|
switch operator {
|
|
case "=":
|
|
return operatorFuncEqual, nil
|
|
case "!=":
|
|
return operatorFuncNotEqual, nil
|
|
case ">":
|
|
return operatorFuncAbove, nil
|
|
case ">=":
|
|
return operatorFuncAboveEqual, nil
|
|
case "<":
|
|
return operatorFuncBelow, nil
|
|
case "<=":
|
|
return operatorFuncBelowEqual, nil
|
|
default:
|
|
return nil, fmt.Errorf("unknown operator %q", operator)
|
|
}
|
|
}
|
|
|
|
type operatorFunc func(v, threshold float64) bool
|
|
|
|
func operatorFuncEqual(v, threshold float64) bool {
|
|
return v == threshold
|
|
}
|
|
|
|
func operatorFuncNotEqual(v, threshold float64) bool {
|
|
return v != threshold
|
|
}
|
|
|
|
func operatorFuncAbove(v, threshold float64) bool {
|
|
return v > threshold
|
|
}
|
|
|
|
func operatorFuncAboveEqual(v, threshold float64) bool {
|
|
return v >= threshold
|
|
}
|
|
|
|
func operatorFuncBelow(v, threshold float64) bool {
|
|
return v < threshold
|
|
}
|
|
|
|
func operatorFuncBelowEqual(v, threshold float64) bool {
|
|
return v <= threshold
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.grep
|
|
func transformGrep(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("expecting two args; got %d args", len(args))
|
|
}
|
|
pattern, err := getRegexp(args, "pattern", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
if !pattern.MatchString(s.Name) {
|
|
return nil, nil
|
|
}
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.group
|
|
func transformGroup(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return groupSeriesLists(ec, fe.Args, fe)
|
|
}
|
|
|
|
func groupSeriesLists(ec *evalConfig, args []*graphiteql.ArgExpr, expr graphiteql.Expr) (nextSeriesFunc, error) {
|
|
var nextSeriess []nextSeriesFunc
|
|
for i := 0; i < len(args); i++ {
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", i)
|
|
if err != nil {
|
|
for _, f := range nextSeriess {
|
|
_, _ = drainAllSeries(f)
|
|
}
|
|
return nil, err
|
|
}
|
|
nextSeriess = append(nextSeriess, nextSeries)
|
|
}
|
|
return nextSeriesGroup(nextSeriess, expr), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.groupByNode
|
|
func transformGroupByNode(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2 or 3", len(args))
|
|
}
|
|
nodes, err := getNodes(args[1:2])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
callback, err := getOptionalString(args, "callback", 2, "average")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return groupByNodesGeneric(ec, fe, nextSeries, nodes, callback)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.groupByNodes
|
|
func transformGroupByNodes(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want at least 2", len(args))
|
|
}
|
|
callback, err := getString(args, "callback", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes, err := getNodes(args[2:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return groupByNodesGeneric(ec, fe, nextSeries, nodes, callback)
|
|
}
|
|
|
|
func groupByNodesGeneric(ec *evalConfig, expr graphiteql.Expr, nextSeries nextSeriesFunc, nodes []graphiteql.Expr, callback string) (nextSeriesFunc, error) {
|
|
keyFunc := func(name string, tags map[string]string) string {
|
|
return getNameFromNodes(name, tags, nodes)
|
|
}
|
|
return groupByKeyFunc(ec, expr, nextSeries, callback, keyFunc)
|
|
}
|
|
|
|
func groupByKeyFunc(ec *evalConfig, expr graphiteql.Expr, nextSeries nextSeriesFunc, aggrFuncName string,
|
|
keyFunc func(name string, tags map[string]string) string) (nextSeriesFunc, error) {
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeriesWrapper := getNextSeriesWrapperForAggregateFunc(aggrFuncName)
|
|
type x struct {
|
|
as aggrState
|
|
tags map[string]string
|
|
seriesExpressions []string
|
|
}
|
|
m := make(map[string]*x)
|
|
var mLock sync.Mutex
|
|
f := nextSeriesWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
key := keyFunc(s.Name, s.Tags)
|
|
mLock.Lock()
|
|
defer mLock.Unlock()
|
|
e := m[key]
|
|
if e == nil {
|
|
as, err := newAggrState(ec.pointsLen(step), aggrFuncName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e = &x{
|
|
as: as,
|
|
tags: s.Tags,
|
|
}
|
|
m[key] = e
|
|
} else {
|
|
for k, v := range e.tags {
|
|
if v != s.Tags[k] {
|
|
delete(e.tags, k)
|
|
}
|
|
}
|
|
}
|
|
e.as.Update(s.Values)
|
|
e.seriesExpressions = append(e.seriesExpressions, s.pathExpression)
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
var ss []*series
|
|
for key, e := range m {
|
|
tags := e.tags
|
|
if tags["name"] == "" {
|
|
funcName := strings.TrimSuffix(aggrFuncName, "Series")
|
|
tags["name"] = fmt.Sprintf("%sSeries(%s)", funcName, formatPathsFromSeriesExpressions(e.seriesExpressions, true))
|
|
}
|
|
tags["aggregatedBy"] = aggrFuncName
|
|
s := &series{
|
|
Name: key,
|
|
Tags: tags,
|
|
Timestamps: ec.newTimestamps(step),
|
|
Values: e.as.Finalize(ec.xFilesFactor),
|
|
expr: expr,
|
|
pathExpression: tags["name"],
|
|
step: step,
|
|
}
|
|
ss = append(ss, s)
|
|
}
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.groupByTags
|
|
func transformGroupByTags(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want at least 2", len(args))
|
|
}
|
|
callback, err := getString(args, "callback", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagKeys := make(map[string]struct{})
|
|
for _, arg := range args[2:] {
|
|
se, ok := arg.Expr.(*graphiteql.StringExpr)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected tag type: %T; expecting string", arg.Expr)
|
|
}
|
|
tagKeys[se.S] = struct{}{}
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keyFunc := func(_ string, tags map[string]string) string {
|
|
return formatKeyFromTags(tags, tagKeys, callback)
|
|
}
|
|
return groupByKeyFunc(ec, fe, nextSeries, callback, keyFunc)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.highest
|
|
func transformHighest(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want from 1 to 3", len(args))
|
|
}
|
|
n, err := getOptionalNumber(args, "n", 1, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
funcName, err := getOptionalString(args, "func", 2, "average")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return highestGeneric(fe, nextSeries, n, funcName)
|
|
}
|
|
|
|
func highestGeneric(expr graphiteql.Expr, nextSeries nextSeriesFunc, n float64, funcName string) (nextSeriesFunc, error) {
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
nextSeriesWrapper := getNextSeriesWrapperForAggregateFunc(funcName)
|
|
var topSeries maxSeriesHeap
|
|
var topSeriesLock sync.Mutex
|
|
f := nextSeriesWrapper(nextSeries, func(s *series) (*series, error) {
|
|
v := aggrFunc(s.Values)
|
|
topSeriesLock.Lock()
|
|
defer topSeriesLock.Unlock()
|
|
if len(topSeries) < int(n) {
|
|
heap.Push(&topSeries, &seriesWithWeight{
|
|
v: v,
|
|
s: s,
|
|
})
|
|
} else if v > topSeries[0].v {
|
|
topSeries[0] = &seriesWithWeight{
|
|
v: v,
|
|
s: s,
|
|
}
|
|
heap.Fix(&topSeries, 0)
|
|
}
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Slice(topSeries, func(i, j int) bool {
|
|
return topSeries[i].v < topSeries[j].v
|
|
})
|
|
var ss []*series
|
|
for _, x := range topSeries {
|
|
s := x.s
|
|
s.expr = expr
|
|
ss = append(ss, s)
|
|
}
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
|
|
type seriesWithWeight struct {
|
|
v float64
|
|
s *series
|
|
}
|
|
|
|
type minSeriesHeap []*seriesWithWeight
|
|
|
|
func (h *minSeriesHeap) Len() int { return len(*h) }
|
|
func (h *minSeriesHeap) Less(i, j int) bool {
|
|
a := *h
|
|
return a[i].v > a[j].v
|
|
}
|
|
func (h *minSeriesHeap) Swap(i, j int) {
|
|
a := *h
|
|
a[i], a[j] = a[j], a[i]
|
|
}
|
|
func (h *minSeriesHeap) Push(x any) {
|
|
*h = append(*h, x.(*seriesWithWeight))
|
|
}
|
|
func (h *minSeriesHeap) Pop() any {
|
|
a := *h
|
|
x := a[len(a)-1]
|
|
*h = a[:len(a)-1]
|
|
return x
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.highestAverage
|
|
func transformHighestAverage(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return highestGeneric(fe, nextSeries, n, "average")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.highestCurrent
|
|
func transformHighestCurrent(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return highestGeneric(fe, nextSeries, n, "current")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.highestMax
|
|
func transformHighestMax(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return highestGeneric(fe, nextSeries, n, "max")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.hitcount
|
|
func transformHitcount(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2 or 3", len(args))
|
|
}
|
|
intervalString, err := getString(args, "intervalString", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
interval, err := parseInterval(intervalString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if interval <= 0 {
|
|
return nil, fmt.Errorf("interval must be positive; got %dms", interval)
|
|
}
|
|
alignToInterval, err := getOptionalBool(args, "alignToInterval", 2, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecCopy := *ec
|
|
if alignToInterval {
|
|
startTime := ecCopy.startTime
|
|
tz := ecCopy.currentTime.Location()
|
|
t := time.Unix(startTime/1e3, (startTime%1000)*1e6).In(tz)
|
|
if interval >= 24*3600*1000 {
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, tz)
|
|
} else if interval >= 3600*1000 {
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, tz)
|
|
} else if interval >= 60*1000 {
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, tz)
|
|
}
|
|
ecCopy.startTime = t.UnixNano() / 1e6
|
|
}
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
ts := ecCopy.startTime
|
|
timestamps := s.Timestamps
|
|
values := s.Values
|
|
var dstTimestamps []int64
|
|
var dstValues []float64
|
|
i := 0
|
|
vPrev := float64(0)
|
|
for ts < ecCopy.endTime {
|
|
tsPrev := ts
|
|
hitcount := float64(0)
|
|
if i < len(timestamps) && !math.IsNaN(vPrev) {
|
|
hitcount = vPrev * float64(timestamps[i]-tsPrev) / 1000
|
|
}
|
|
tsEnd := ts + interval
|
|
for i < len(timestamps) {
|
|
tsCurr := timestamps[i]
|
|
if tsCurr >= tsEnd {
|
|
break
|
|
}
|
|
v := values[i]
|
|
if !math.IsNaN(v) {
|
|
hitcount += v * (float64(tsCurr-tsPrev) / 1000)
|
|
}
|
|
tsPrev = tsCurr
|
|
vPrev = v
|
|
i++
|
|
}
|
|
if hitcount == 0 {
|
|
hitcount = nan
|
|
}
|
|
dstValues = append(dstValues, hitcount)
|
|
dstTimestamps = append(dstTimestamps, ts)
|
|
ts = tsEnd
|
|
}
|
|
s.Timestamps = dstTimestamps
|
|
s.Values = dstValues
|
|
s.Tags["hitcount"] = intervalString
|
|
if alignToInterval {
|
|
s.Name = fmt.Sprintf("hitcount(%s,%s,true)", s.Name, graphiteql.QuoteString(intervalString))
|
|
} else {
|
|
s.Name = fmt.Sprintf("hitcount(%s,%s)", s.Name, graphiteql.QuoteString(intervalString))
|
|
}
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.identity
|
|
func transformIdentity(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
name, err := getString(args, "name", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
const step = 60e3
|
|
var dstValues []float64
|
|
var dstTimestamps []int64
|
|
ts := ec.startTime
|
|
for ts < ec.endTime {
|
|
dstValues = append(dstValues, float64(ts)/1000)
|
|
dstTimestamps = append(dstTimestamps, ts)
|
|
ts += step
|
|
}
|
|
s := &series{
|
|
Name: name,
|
|
Tags: unmarshalTags(name),
|
|
Timestamps: dstTimestamps,
|
|
Values: dstValues,
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: step,
|
|
}
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.integral
|
|
func transformIntegral(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
sum := float64(0)
|
|
for i, v := range values {
|
|
if math.IsNaN(v) {
|
|
continue
|
|
}
|
|
sum += v
|
|
values[i] = sum
|
|
}
|
|
s.Tags["integral"] = "1"
|
|
s.Name = fmt.Sprintf("integral(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.integralByInterval
|
|
func transformIntegralByInterval(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
intervalUnit, err := getString(args, "intervalUnit", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
interval, err := parseInterval(intervalUnit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
timestamps := s.Timestamps
|
|
sum := float64(0)
|
|
dtPrev := int64(0)
|
|
for i, v := range values {
|
|
if math.IsNaN(v) {
|
|
continue
|
|
}
|
|
dt := timestamps[i] / interval
|
|
if dt != dtPrev {
|
|
sum = 0
|
|
dtPrev = dt
|
|
}
|
|
sum += v
|
|
values[i] = sum
|
|
}
|
|
s.Tags["integralByInterval"] = "1"
|
|
s.Name = fmt.Sprintf("integralByInterval(%s,%s)", s.Name, graphiteql.QuoteString(intervalUnit))
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.interpolate
|
|
func transformInterpolate(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1 or 2", len(args))
|
|
}
|
|
limit, err := getOptionalNumber(args, "limit", 1, math.Inf(1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
nansCount := float64(0)
|
|
prevValue := nan
|
|
for i, v := range values {
|
|
if math.IsNaN(v) {
|
|
nansCount++
|
|
continue
|
|
}
|
|
if nansCount > 0 && nansCount <= limit {
|
|
delta := (v - prevValue) / (nansCount + 1)
|
|
for j := i - int(nansCount); j < i; j++ {
|
|
prevValue += delta
|
|
values[j] = prevValue
|
|
}
|
|
}
|
|
nansCount = 0
|
|
prevValue = v
|
|
}
|
|
s.Name = fmt.Sprintf("interpolate(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.invert
|
|
func transformInvert(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = 1 / v
|
|
}
|
|
s.Tags["invert"] = "1"
|
|
s.Name = fmt.Sprintf("invert(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.keepLastValue
|
|
func transformKeepLastValue(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1 or 2", len(args))
|
|
}
|
|
limit, err := getOptionalNumber(args, "limit", 1, math.Inf(1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "serieslList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
nansCount := float64(0)
|
|
prevValue := nan
|
|
for i, v := range values {
|
|
if !math.IsNaN(v) {
|
|
nansCount = 0
|
|
prevValue = v
|
|
continue
|
|
}
|
|
nansCount++
|
|
if nansCount <= limit {
|
|
values[i] = prevValue
|
|
}
|
|
}
|
|
s.Name = fmt.Sprintf("keepLastValue(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.limit
|
|
func transformLimit(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seriesFetched := 0
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
if seriesFetched >= int(n) {
|
|
return nil, nil
|
|
}
|
|
seriesFetched++
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.lineWidth
|
|
func transformLineWidth(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
_, err := getNumber(args, "width", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.logarithm
|
|
func transformLogarithm(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1 or 2", len(args))
|
|
}
|
|
base, err := getOptionalNumber(args, "base", 1, 10)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseStr := fmt.Sprintf("%g", base)
|
|
baseLog := math.Log(base)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = math.Log(v) / baseLog
|
|
}
|
|
s.Tags["log"] = baseStr
|
|
s.Name = fmt.Sprintf("log(%s,%s)", s.Name, baseStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.logit
|
|
func transformLogit(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = math.Log(v / (1 - v))
|
|
}
|
|
s.Tags["logit"] = "logit"
|
|
s.Name = fmt.Sprintf("logit(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.lowest
|
|
func transformLowest(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want from 1 to 3", len(args))
|
|
}
|
|
n, err := getOptionalNumber(args, "n", 1, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
funcName, err := getOptionalString(args, "func", 2, "average")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return lowestGeneric(fe, nextSeries, n, funcName)
|
|
}
|
|
|
|
func lowestGeneric(expr graphiteql.Expr, nextSeries nextSeriesFunc, n float64, funcName string) (nextSeriesFunc, error) {
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
nextSeriesWrapper := getNextSeriesWrapperForAggregateFunc(funcName)
|
|
var minSeries minSeriesHeap
|
|
var minSeriesLock sync.Mutex
|
|
f := nextSeriesWrapper(nextSeries, func(s *series) (*series, error) {
|
|
v := aggrFunc(s.Values)
|
|
minSeriesLock.Lock()
|
|
defer minSeriesLock.Unlock()
|
|
if len(minSeries) < int(n) {
|
|
heap.Push(&minSeries, &seriesWithWeight{
|
|
v: v,
|
|
s: s,
|
|
})
|
|
} else if v < minSeries[0].v {
|
|
minSeries[0] = &seriesWithWeight{
|
|
v: v,
|
|
s: s,
|
|
}
|
|
heap.Fix(&minSeries, 0)
|
|
}
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Slice(minSeries, func(i, j int) bool {
|
|
return minSeries[i].v > minSeries[j].v
|
|
})
|
|
var ss []*series
|
|
for _, x := range minSeries {
|
|
s := x.s
|
|
s.expr = expr
|
|
ss = append(ss, s)
|
|
}
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
|
|
type maxSeriesHeap []*seriesWithWeight
|
|
|
|
func (h *maxSeriesHeap) Len() int { return len(*h) }
|
|
func (h *maxSeriesHeap) Less(i, j int) bool {
|
|
a := *h
|
|
return a[i].v < a[j].v
|
|
}
|
|
func (h *maxSeriesHeap) Swap(i, j int) {
|
|
a := *h
|
|
a[i], a[j] = a[j], a[i]
|
|
}
|
|
func (h *maxSeriesHeap) Push(x any) {
|
|
*h = append(*h, x.(*seriesWithWeight))
|
|
}
|
|
func (h *maxSeriesHeap) Pop() any {
|
|
a := *h
|
|
x := a[len(a)-1]
|
|
*h = a[:len(a)-1]
|
|
return x
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.lowestAverage
|
|
func transformLowestAverage(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return lowestGeneric(fe, nextSeries, n, "average")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.lowestCurrent
|
|
func transformLowestCurrent(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return lowestGeneric(fe, nextSeries, n, "current")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.maxSeries
|
|
func transformMaxSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "max")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.maximumAbove
|
|
func transformMaximumAbove(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "max", ">", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.maximumBelow
|
|
func transformMaximumBelow(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "max", "<", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.minMax
|
|
func transformMinMax(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
min := aggrMin(values)
|
|
if math.IsNaN(min) {
|
|
min = 0
|
|
}
|
|
max := aggrMax(values)
|
|
if math.IsNaN(max) {
|
|
max = 0
|
|
}
|
|
vRange := max - min
|
|
for i, v := range values {
|
|
v = (v - min) / vRange
|
|
if math.IsInf(v, 0) {
|
|
v = 0
|
|
}
|
|
values[i] = v
|
|
}
|
|
s.Name = fmt.Sprintf("minMax(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.minSeries
|
|
func transformMinSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "min")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.minimumAbove
|
|
func transformMinimumAbove(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "min", ">", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.minimumBelow
|
|
func transformMinimumBelow(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSeriesGeneric(fe, nextSeries, "min", "<", n)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.mostDeviant
|
|
func transformMostDeviant(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return highestGeneric(fe, nextSeries, n, "stddev")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.movingAverage
|
|
func transformMovingAverage(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return movingWindowGeneric(ec, fe, "average")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.movingMax
|
|
func transformMovingMax(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return movingWindowGeneric(ec, fe, "max")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.movingMedian
|
|
func transformMovingMedian(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return movingWindowGeneric(ec, fe, "median")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.movingMin
|
|
func transformMovingMin(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return movingWindowGeneric(ec, fe, "min")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.movingSum
|
|
func transformMovingSum(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return movingWindowGeneric(ec, fe, "sum")
|
|
}
|
|
|
|
func movingWindowGeneric(ec *evalConfig, fe *graphiteql.FuncExpr, funcName string) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2 or 3", len(args))
|
|
}
|
|
windowSizeArg, err := getArg(args, "windowSize", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
xFilesFactor, err := getOptionalNumber(args, "xFilesFactor", 2, ec.xFilesFactor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seriesListArg, err := getArg(args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return movingWindow(ec, fe, seriesListArg, windowSizeArg, funcName, xFilesFactor)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.movingWindow
|
|
func transformMovingWindow(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want from 2 to 4", len(args))
|
|
}
|
|
windowSizeArg, err := getArg(args, "windowSize", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
funcName, err := getOptionalString(args, "func", 2, "avg")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
xFilesFactor, err := getOptionalNumber(args, "xFilesFactor", 3, ec.xFilesFactor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seriesListArg, err := getArg(args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return movingWindow(ec, fe, seriesListArg, windowSizeArg, funcName, xFilesFactor)
|
|
}
|
|
|
|
func movingWindow(ec *evalConfig, fe *graphiteql.FuncExpr, seriesListArg, windowSizeArg *graphiteql.ArgExpr, funcName string, xFilesFactor float64) (nextSeriesFunc, error) {
|
|
windowSize, stepsCount, err := getWindowSize(ec, windowSizeArg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
windowSizeStr := string(windowSizeArg.Expr.AppendString(nil))
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecCopy := *ec
|
|
ecCopy.startTime -= windowSize
|
|
nextSeries, err := evalExpr(&ecCopy, seriesListArg.Expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if stepsCount > 0 && step != ec.storageStep {
|
|
// The inner function call changes the step and the moving* function refers to it.
|
|
// Adjust the startTime and re-calculate the inner function on the adjusted time range.
|
|
if _, err := drainAllSeries(nextSeries); err != nil {
|
|
return nil, err
|
|
}
|
|
windowSize = int64(stepsCount * float64(step))
|
|
ecCopy = *ec
|
|
ecCopy.startTime -= windowSize
|
|
nextSeries, err = evalExpr(&ecCopy, seriesListArg.Expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
tagName := "moving" + strings.Title(funcName)
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
timestamps := s.Timestamps
|
|
values := s.Values
|
|
var dstTimestamps []int64
|
|
var dstValues []float64
|
|
tsEnd := ecCopy.startTime + windowSize
|
|
i := 0
|
|
j := 0
|
|
for tsEnd <= ecCopy.endTime {
|
|
tsStart := tsEnd - windowSize
|
|
for i < len(timestamps) && timestamps[i] < tsStart {
|
|
i++
|
|
}
|
|
if i > j {
|
|
j = i
|
|
}
|
|
for j < len(timestamps) && timestamps[j] < tsEnd {
|
|
j++
|
|
}
|
|
v := aggrFunc.apply(xFilesFactor, values[i:j])
|
|
dstTimestamps = append(dstTimestamps, tsEnd)
|
|
dstValues = append(dstValues, v)
|
|
tsEnd += step
|
|
}
|
|
s.Timestamps = dstTimestamps
|
|
s.Values = dstValues
|
|
s.Tags[tagName] = windowSizeStr
|
|
s.Name = fmt.Sprintf("%s(%s,%s)", tagName, s.Name, windowSizeStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.multiplySeries
|
|
func transformMultiplySeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "multiply")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.multiplySeriesWithWildcards
|
|
func transformMultiplySeriesWithWildcards(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesWithWildcardsGeneric(ec, fe, "multiply")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.percentileOfSeries
|
|
func transformPercentileOfSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2 or 3", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO: properly use interpolate
|
|
if _, err := getOptionalBool(args, "interpolate", 2, false); err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
as := newAggrStatePercentile(ec.pointsLen(step), n)
|
|
var lock sync.Mutex
|
|
var seriesExpressions []string
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
lock.Lock()
|
|
as.Update(s.Values)
|
|
|
|
seriesExpressions = append(seriesExpressions, s.pathExpression)
|
|
lock.Unlock()
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(seriesExpressions) == 0 {
|
|
return multiSeriesFunc(nil), nil
|
|
}
|
|
// peek first expr as graphite does.
|
|
sort.Strings(seriesExpressions)
|
|
name := fmt.Sprintf("percentileOfSeries(%s,%g)", seriesExpressions[0], n)
|
|
s := &series{
|
|
Name: name,
|
|
Tags: map[string]string{"name": name},
|
|
Timestamps: ec.newTimestamps(step),
|
|
Values: as.Finalize(ec.xFilesFactor),
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: step,
|
|
}
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.pow
|
|
func transformPow(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
factor, err := getNumber(args, "factor", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
factorStr := fmt.Sprintf("%g", factor)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = math.Pow(v, factor)
|
|
}
|
|
s.Tags["pow"] = factorStr
|
|
s.Name = fmt.Sprintf("pow(%s,%s)", s.Name, factorStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.powSeries
|
|
func transformPowSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "pow")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.randomWalk
|
|
func transformRandomWalk(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1 or 2", len(args))
|
|
}
|
|
name, err := getString(args, "name", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := getOptionalNumber(args, "step", 1, 60)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if step <= 0 {
|
|
return nil, fmt.Errorf("step must be positive; got %g", step)
|
|
}
|
|
stepMsecs := int64(step * 1000)
|
|
var dstValues []float64
|
|
var dstTimestamps []int64
|
|
ts := ec.startTime
|
|
v := float64(0)
|
|
for ts < ec.endTime {
|
|
dstValues = append(dstValues, v)
|
|
dstTimestamps = append(dstTimestamps, ts)
|
|
v += rand.Float64() - 0.5
|
|
ts += stepMsecs
|
|
}
|
|
s := &series{
|
|
Name: name,
|
|
Tags: unmarshalTags(name),
|
|
Timestamps: dstTimestamps,
|
|
Values: dstValues,
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: stepMsecs,
|
|
}
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.rangeOfSeries
|
|
func transformRangeOfSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "rangeOf")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.removeAbovePercentile
|
|
func transformRemoveAbovePercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aggrFunc := newAggrFuncPercentile(n)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
max := aggrFunc(values)
|
|
for i, v := range values {
|
|
if v > max {
|
|
values[i] = nan
|
|
}
|
|
}
|
|
s.Name = fmt.Sprintf("removeAbovePercentile(%s,%g)", s.Name, n)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.removeAboveValue
|
|
func transformRemoveAboveValue(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if v > n {
|
|
values[i] = nan
|
|
}
|
|
}
|
|
s.Name = fmt.Sprintf("removeAboveValue(%s,%g)", s.Name, n)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.removeBelowPercentile
|
|
func transformRemoveBelowPercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aggrFunc := newAggrFuncPercentile(n)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
min := aggrFunc(values)
|
|
for i, v := range values {
|
|
if v < min {
|
|
values[i] = nan
|
|
}
|
|
}
|
|
s.Name = fmt.Sprintf("removeBelowPercentile(%s,%g)", s.Name, n)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.removeBelowValue
|
|
func transformRemoveBelowValue(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if v < n {
|
|
values[i] = nan
|
|
}
|
|
}
|
|
s.Name = fmt.Sprintf("removeBelowValue(%s,%g)", s.Name, n)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.removeBetweenPercentile
|
|
func transformRemoveBetweenPercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n > 50 {
|
|
n = 100 - n
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var ss []*series
|
|
asLow := newAggrStatePercentile(ec.pointsLen(step), n)
|
|
asHigh := newAggrStatePercentile(ec.pointsLen(step), 100-n)
|
|
var lock sync.Mutex
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
lock.Lock()
|
|
asLow.Update(s.Values)
|
|
asHigh.Update(s.Values)
|
|
ss = append(ss, s)
|
|
lock.Unlock()
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
lows := asLow.Finalize(ec.xFilesFactor)
|
|
highs := asHigh.Finalize(ec.xFilesFactor)
|
|
var ssDst []*series
|
|
for _, s := range ss {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if v < lows[i] || v > highs[i] {
|
|
s.expr = fe
|
|
ssDst = append(ssDst, s)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return multiSeriesFunc(ssDst), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.removeEmptySeries
|
|
func transformRemoveEmptySeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1 or 2", len(args))
|
|
}
|
|
xFilesFactor, err := getOptionalNumber(args, "xFilesFactor", 1, ec.xFilesFactor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
xff := s.xFilesFactor
|
|
if xff == 0 {
|
|
xff = xFilesFactor
|
|
}
|
|
n := aggrCount(s.Values)
|
|
if n/float64(len(s.Values)) <= xff {
|
|
return nil, nil
|
|
}
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.roundFunction
|
|
func transformRoundFunction(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1 or 2", len(args))
|
|
}
|
|
precision, err := getOptionalNumber(args, "precision", 1, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
precisionProduct := math.Pow10(int(precision))
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = math.Round(v*precisionProduct) / precisionProduct
|
|
}
|
|
if precision == 0 {
|
|
s.Name = fmt.Sprintf("round(%s)", s.Name)
|
|
} else {
|
|
s.Name = fmt.Sprintf("round(%s,%g)", s.Name, precision)
|
|
}
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.scale
|
|
func transformScale(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
factor, err := getNumber(args, "factor", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = v * factor
|
|
}
|
|
s.Name = fmt.Sprintf("scale(%s,%g)", s.Name, factor)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.seriesByTag
|
|
func transformSeriesByTag(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) == 0 {
|
|
return nil, fmt.Errorf("at least one tagExpression must be passed to seriesByTag")
|
|
}
|
|
var tagExpressions []string
|
|
for i := 0; i < len(args); i++ {
|
|
te, err := getString(args, "tagExpressions", i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tagExpressions = append(tagExpressions, te)
|
|
}
|
|
sq, err := getSearchQueryForExprs(ec.currentTime, ec.etfs, tagExpressions, *maxGraphiteSeries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sq.MinTimestamp = ec.startTime
|
|
sq.MaxTimestamp = ec.endTime
|
|
return newNextSeriesForSearchQuery(ec, sq, fe)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.setXFilesFactor
|
|
func transformSetXFilesFactor(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
xFilesFactor, err := getNumber(args, "xFilesFactor", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecCopy := *ec
|
|
ecCopy.xFilesFactor = xFilesFactor
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
xFilesFactorStr := fmt.Sprintf("%g", xFilesFactor)
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.xFilesFactor = xFilesFactor
|
|
s.Tags["xFilesFactor"] = xFilesFactorStr
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sumSeriesWithWildcards
|
|
func transformSumSeriesWithWildcards(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesWithWildcardsGeneric(ec, fe, "sum")
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.summarize
|
|
func transformSummarize(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want from 2 to 4", len(args))
|
|
}
|
|
intervalString, err := getString(args, "intervalString", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
interval, err := parseInterval(intervalString)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse intervalString: %w", err)
|
|
}
|
|
if interval <= 0 {
|
|
return nil, fmt.Errorf("interval must be positive; got %dms", interval)
|
|
}
|
|
funcName, err := getOptionalString(args, "func", 2, "sum")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alignToFrom, err := getOptionalBool(args, "alignToFrom", 3, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecCopy := *ec
|
|
if !alignToFrom {
|
|
ecCopy.startTime -= ecCopy.startTime % interval
|
|
ecCopy.endTime += interval - ecCopy.endTime%interval
|
|
}
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.summarize(aggrFunc, ecCopy.startTime, ecCopy.endTime, interval, s.xFilesFactor)
|
|
s.Tags["summarize"] = intervalString
|
|
s.Tags["summarizeFunction"] = funcName
|
|
if alignToFrom {
|
|
s.Name = fmt.Sprintf("summarize(%s,%s,%s,true)", s.Name, graphiteql.QuoteString(intervalString), graphiteql.QuoteString(funcName))
|
|
} else {
|
|
s.Name = fmt.Sprintf("summarize(%s,%s,%s)", s.Name, graphiteql.QuoteString(intervalString), graphiteql.QuoteString(funcName))
|
|
}
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.weightedAverage
|
|
func transformWeightedAverage(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2 at least", len(args))
|
|
}
|
|
nodes, err := getNodes(args[2:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
avgSeries, err := evalSeriesList(ec, args, "seriesListAvg", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ss, stepAvg, err := fetchNormalizedSeries(ec, avgSeries, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
weightSeries, err := evalSeriesList(ec, args, "seriesListWeight", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ssWeight, stepWeight, err := fetchNormalizedSeries(ec, weightSeries, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ss) != len(ssWeight) {
|
|
return nil, fmt.Errorf("series len mismatch, got seriesListAvg: %d,seriesListWeight: %d ", len(ss), len(ssWeight))
|
|
}
|
|
if stepAvg != stepWeight {
|
|
return nil, fmt.Errorf("step mismatch for seriesListAvg and seriesListWeight: %d vs %d", stepAvg, stepWeight)
|
|
}
|
|
mAvg := groupSeriesByNodes(ss, nodes)
|
|
mWeight := groupSeriesByNodes(ssWeight, nodes)
|
|
var ssProduct []*series
|
|
for k, ss := range mAvg {
|
|
wss := mWeight[k]
|
|
if len(wss) == 0 {
|
|
continue
|
|
}
|
|
s := ss[len(ss)-1]
|
|
ws := wss[len(wss)-1]
|
|
values := s.Values
|
|
valuesWeight := ws.Values
|
|
for i, v := range values {
|
|
values[i] = v * valuesWeight[i]
|
|
}
|
|
ssProduct = append(ssProduct, s)
|
|
}
|
|
if len(ssProduct) == 0 {
|
|
return multiSeriesFunc(nil), nil
|
|
}
|
|
|
|
step := stepAvg
|
|
as := newAggrStateSum(ec.pointsLen(step))
|
|
for _, s := range ssProduct {
|
|
as.Update(s.Values)
|
|
}
|
|
values := as.Finalize(ec.xFilesFactor)
|
|
|
|
asWeight := newAggrStateSum(ec.pointsLen(step))
|
|
for _, s := range ssWeight {
|
|
asWeight.Update(s.Values)
|
|
}
|
|
valuesWeight := asWeight.Finalize(ec.xFilesFactor)
|
|
|
|
for i, v := range values {
|
|
values[i] = v / valuesWeight[i]
|
|
}
|
|
|
|
var nodesStr []string
|
|
for _, node := range nodes {
|
|
nodesStr = append(nodesStr, string(node.AppendString(nil)))
|
|
}
|
|
name := fmt.Sprintf("weightedAverage(%s,%s,%s)",
|
|
formatPathsFromSeries(ss),
|
|
formatPathsFromSeries(ssWeight),
|
|
strings.Join(nodesStr, ","),
|
|
)
|
|
sResult := &series{
|
|
Name: name,
|
|
Tags: map[string]string{"name": name},
|
|
Timestamps: ec.newTimestamps(step),
|
|
Values: values,
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: step,
|
|
}
|
|
return singleSeriesFunc(sResult), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.timeFunction
|
|
func transformTimeFunction(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1 or 2 args", len(args))
|
|
}
|
|
name, err := getString(args, "name", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := getOptionalNumber(args, "step", 1, 60)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepMsecs := int64(step * 1000)
|
|
var values []float64
|
|
var timestamps []int64
|
|
ts := ec.startTime
|
|
for ts <= ec.endTime {
|
|
timestamps = append(timestamps, ts)
|
|
values = append(values, float64(ts/1000))
|
|
ts += stepMsecs
|
|
}
|
|
s := &series{
|
|
Name: name,
|
|
Tags: unmarshalTags(name),
|
|
Timestamps: timestamps,
|
|
Values: values,
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: stepMsecs,
|
|
}
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
func getWindowSize(ec *evalConfig, windowSizeArg *graphiteql.ArgExpr) (windowSize int64, stepsCount float64, err error) {
|
|
switch t := windowSizeArg.Expr.(type) {
|
|
case *graphiteql.NumberExpr:
|
|
stepsCount = t.N
|
|
windowSize = int64(t.N * float64(ec.storageStep))
|
|
case *graphiteql.StringExpr:
|
|
ws, err := parseInterval(t.S)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("cannot parse windowSize: %w", err)
|
|
}
|
|
windowSize = ws
|
|
default:
|
|
return 0, 0, fmt.Errorf("unexpected type for windowSize arg: %T; expecting number or string", windowSizeArg.Expr)
|
|
}
|
|
if windowSize <= 0 {
|
|
return 0, 0, fmt.Errorf("windowSize must be positive; got %dms", windowSize)
|
|
}
|
|
return windowSize, stepsCount, nil
|
|
}
|
|
|
|
func getArg(args []*graphiteql.ArgExpr, name string, index int) (*graphiteql.ArgExpr, error) {
|
|
for _, arg := range args {
|
|
if arg.Name == name {
|
|
return arg, nil
|
|
}
|
|
}
|
|
if index >= len(args) {
|
|
return nil, fmt.Errorf("missing arg %q at position %d", name, index)
|
|
}
|
|
arg := args[index]
|
|
if arg.Name != "" {
|
|
return nil, fmt.Errorf("unexpected named arg at position %d: %q", index, arg.Name)
|
|
}
|
|
return arg, nil
|
|
}
|
|
|
|
func getOptionalArg(args []*graphiteql.ArgExpr, name string, index int) *graphiteql.ArgExpr {
|
|
for _, arg := range args {
|
|
if arg.Name == name {
|
|
return arg
|
|
}
|
|
}
|
|
if index >= len(args) {
|
|
return nil
|
|
}
|
|
arg := args[index]
|
|
if arg.Name != "" {
|
|
return nil
|
|
}
|
|
return arg
|
|
}
|
|
|
|
func evalSeriesList(ec *evalConfig, args []*graphiteql.ArgExpr, name string, index int) (nextSeriesFunc, error) {
|
|
arg, err := getArg(args, name, index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalExpr(ec, arg.Expr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot evaluate arg %q at position %d: %w", name, index, err)
|
|
}
|
|
return nextSeries, nil
|
|
}
|
|
|
|
func getInts(args []*graphiteql.ArgExpr, name string) ([]int, error) {
|
|
var ns []int
|
|
for i := range args {
|
|
n, err := getNumber(args, name, i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ns = append(ns, int(n))
|
|
}
|
|
return ns, nil
|
|
}
|
|
|
|
func getNumber(args []*graphiteql.ArgExpr, name string, index int) (float64, error) {
|
|
arg, err := getArg(args, name, index)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
ne, ok := arg.Expr.(*graphiteql.NumberExpr)
|
|
if !ok {
|
|
return 0, fmt.Errorf("arg %q at position %d must be a number; got %T", name, index, arg.Expr)
|
|
}
|
|
return ne.N, nil
|
|
}
|
|
|
|
func getOptionalNumber(args []*graphiteql.ArgExpr, name string, index int, defaultValue float64) (float64, error) {
|
|
arg := getOptionalArg(args, name, index)
|
|
if arg == nil {
|
|
return defaultValue, nil
|
|
}
|
|
if _, ok := arg.Expr.(*graphiteql.NoneExpr); ok {
|
|
return defaultValue, nil
|
|
}
|
|
ne, ok := arg.Expr.(*graphiteql.NumberExpr)
|
|
if !ok {
|
|
return 0, fmt.Errorf("arg %q at position %d must be a number; got %T", name, index, arg.Expr)
|
|
}
|
|
return ne.N, nil
|
|
}
|
|
|
|
func getString(args []*graphiteql.ArgExpr, name string, index int) (string, error) {
|
|
arg, err := getArg(args, name, index)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
se, ok := arg.Expr.(*graphiteql.StringExpr)
|
|
if !ok {
|
|
return "", fmt.Errorf("arg %q at position %d must be a string; got %T", name, index, arg.Expr)
|
|
}
|
|
return se.S, nil
|
|
}
|
|
|
|
func getOptionalString(args []*graphiteql.ArgExpr, name string, index int, defaultValue string) (string, error) {
|
|
arg := getOptionalArg(args, name, index)
|
|
if arg == nil {
|
|
return defaultValue, nil
|
|
}
|
|
if _, ok := arg.Expr.(*graphiteql.NoneExpr); ok {
|
|
return defaultValue, nil
|
|
}
|
|
se, ok := arg.Expr.(*graphiteql.StringExpr)
|
|
if !ok {
|
|
return "", fmt.Errorf("arg %q at position %d must be a string; got %T", name, index, arg.Expr)
|
|
}
|
|
return se.S, nil
|
|
}
|
|
|
|
func getOptionalBool(args []*graphiteql.ArgExpr, name string, index int, defaultValue bool) (bool, error) {
|
|
arg := getOptionalArg(args, name, index)
|
|
if arg == nil {
|
|
return defaultValue, nil
|
|
}
|
|
if _, ok := arg.Expr.(*graphiteql.NoneExpr); ok {
|
|
return defaultValue, nil
|
|
}
|
|
be, ok := arg.Expr.(*graphiteql.BoolExpr)
|
|
if !ok {
|
|
return false, fmt.Errorf("arg %q at position %d must be a bool; got %T", name, index, arg.Expr)
|
|
}
|
|
return be.B, nil
|
|
}
|
|
|
|
func getRegexp(args []*graphiteql.ArgExpr, name string, index int) (*regexp.Regexp, error) {
|
|
search, err := getString(args, name, index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
re, err := regexp.Compile(search)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot compile search regexp %q: %w", search, err)
|
|
}
|
|
return re, nil
|
|
}
|
|
|
|
func getRegexpReplacement(args []*graphiteql.ArgExpr, name string, index int) (string, error) {
|
|
replace, err := getString(args, name, index)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return graphiteToGolangRegexpReplace(replace), nil
|
|
}
|
|
|
|
func graphiteToGolangRegexpReplace(replace string) string {
|
|
return graphiteToGolangRe.ReplaceAllString(replace, "$$${1}")
|
|
}
|
|
|
|
var graphiteToGolangRe = regexp.MustCompile(`\\(\d+)`)
|
|
|
|
func getNodes(args []*graphiteql.ArgExpr) ([]graphiteql.Expr, error) {
|
|
var nodes []graphiteql.Expr
|
|
for i := 0; i < len(args); i++ {
|
|
expr := args[i].Expr
|
|
switch expr.(type) {
|
|
case *graphiteql.NumberExpr, *graphiteql.StringExpr:
|
|
default:
|
|
return nil, fmt.Errorf("unexpected arg type for `nodes`; got %T; expecting number or string", expr)
|
|
}
|
|
nodes = append(nodes, expr)
|
|
}
|
|
return nodes, nil
|
|
}
|
|
|
|
func fetchNormalizedSeriesByNodes(ec *evalConfig, nextSeries nextSeriesFunc, nodes []graphiteql.Expr) (map[string][]*series, int64, error) {
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
return s, nil
|
|
})
|
|
ss, err := fetchAllSeries(f)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return groupSeriesByNodes(ss, nodes), step, nil
|
|
}
|
|
|
|
func groupSeriesByNodes(ss []*series, nodes []graphiteql.Expr) map[string][]*series {
|
|
m := make(map[string][]*series)
|
|
for _, s := range ss {
|
|
key := getNameFromNodes(s.Name, s.Tags, nodes)
|
|
m[key] = append(m[key], s)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func getAbsoluteNodeIndex(index, size int) int {
|
|
// Handle the negative index case as Python does
|
|
if index < 0 {
|
|
index = size + index
|
|
}
|
|
if index < 0 || index >= size {
|
|
return -1
|
|
}
|
|
return index
|
|
}
|
|
|
|
func getNameFromNodes(name string, tags map[string]string, nodes []graphiteql.Expr) string {
|
|
if len(nodes) == 0 {
|
|
return ""
|
|
}
|
|
path := getPathFromName(name)
|
|
parts := strings.Split(path, ".")
|
|
var dstParts []string
|
|
for _, node := range nodes {
|
|
switch t := node.(type) {
|
|
case *graphiteql.NumberExpr:
|
|
if n := getAbsoluteNodeIndex(int(t.N), len(parts)); n >= 0 {
|
|
dstParts = append(dstParts, parts[n])
|
|
}
|
|
case *graphiteql.StringExpr:
|
|
if v := tags[t.S]; v != "" {
|
|
dstParts = append(dstParts, v)
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(dstParts, ".")
|
|
}
|
|
|
|
func getPathFromName(s string) string {
|
|
expr, err := graphiteql.Parse(s)
|
|
if err != nil {
|
|
return s
|
|
}
|
|
for {
|
|
switch t := expr.(type) {
|
|
case *graphiteql.MetricExpr:
|
|
return t.Query
|
|
case *graphiteql.FuncExpr:
|
|
for _, arg := range t.Args {
|
|
if me, ok := arg.Expr.(*graphiteql.MetricExpr); ok {
|
|
return me.Query
|
|
}
|
|
}
|
|
if len(t.Args) == 0 {
|
|
return s
|
|
}
|
|
expr = t.Args[0].Expr
|
|
case *graphiteql.StringExpr:
|
|
return t.S
|
|
case *graphiteql.NumberExpr:
|
|
return string(t.AppendString(nil))
|
|
case *graphiteql.BoolExpr:
|
|
return strconv.FormatBool(t.B)
|
|
default:
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
|
|
func fetchNormalizedSeries(ec *evalConfig, nextSeries nextSeriesFunc, isConcurrent bool) ([]*series, int64, error) {
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
nextSeriesWrapper := getNextSeriesWrapper(isConcurrent)
|
|
f := nextSeriesWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
return s, nil
|
|
})
|
|
ss, err := fetchAllSeries(f)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return ss, step, nil
|
|
}
|
|
|
|
func fetchAllSeries(nextSeries nextSeriesFunc) ([]*series, error) {
|
|
var ss []*series
|
|
for {
|
|
s, err := nextSeries()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s == nil {
|
|
return ss, nil
|
|
}
|
|
ss = append(ss, s)
|
|
}
|
|
}
|
|
|
|
func drainAllSeries(nextSeries nextSeriesFunc) (int, error) {
|
|
seriesCount := 0
|
|
for {
|
|
s, err := nextSeries()
|
|
if err != nil {
|
|
return seriesCount, err
|
|
}
|
|
if s == nil {
|
|
return seriesCount, nil
|
|
}
|
|
seriesCount++
|
|
}
|
|
}
|
|
|
|
func singleSeriesFunc(s *series) nextSeriesFunc {
|
|
return multiSeriesFunc([]*series{s})
|
|
}
|
|
|
|
func multiSeriesFunc(ss []*series) nextSeriesFunc {
|
|
for _, s := range ss {
|
|
if s == nil {
|
|
panic(fmt.Errorf("BUG: all the series passed to multiSeriesFunc must be non-nil"))
|
|
}
|
|
}
|
|
f := func() (*series, error) {
|
|
if len(ss) == 0 {
|
|
return nil, nil
|
|
}
|
|
s := ss[0]
|
|
ss = ss[1:]
|
|
return s, nil
|
|
}
|
|
return f
|
|
}
|
|
|
|
func nextSeriesGroup(nextSeriess []nextSeriesFunc, expr graphiteql.Expr) nextSeriesFunc {
|
|
f := func() (*series, error) {
|
|
for {
|
|
if len(nextSeriess) == 0 {
|
|
return nil, nil
|
|
}
|
|
nextSeries := nextSeriess[0]
|
|
s, err := nextSeries()
|
|
if err != nil {
|
|
for _, f := range nextSeriess[1:] {
|
|
_, _ = drainAllSeries(f)
|
|
}
|
|
nextSeriess = nil
|
|
return nil, err
|
|
}
|
|
if s != nil {
|
|
if expr != nil {
|
|
s.expr = expr
|
|
}
|
|
return s, nil
|
|
}
|
|
nextSeriess = nextSeriess[1:]
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
|
|
func getNextSeriesWrapperForAggregateFunc(funcName string) func(nextSeriesFunc, func(s *series) (*series, error)) nextSeriesFunc {
|
|
isConcurrent := !isSerialFunc(funcName)
|
|
return getNextSeriesWrapper(isConcurrent)
|
|
}
|
|
|
|
func isSerialFunc(funcName string) bool {
|
|
switch funcName {
|
|
case "diff", "first", "last", "current", "pow":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getNextSeriesWrapper(isConcurrent bool) func(nextSeriesFunc, func(s *series) (*series, error)) nextSeriesFunc {
|
|
if isConcurrent {
|
|
return nextSeriesConcurrentWrapper
|
|
}
|
|
return nextSeriesSerialWrapper
|
|
}
|
|
|
|
// nextSeriesSerialWrapper serially fetches series from nextSeries and passes them to f.
|
|
//
|
|
// see nextSeriesConcurrentWrapper for CPU-bound f.
|
|
//
|
|
// If f returns (nil, nil), then the current series is skipped.
|
|
// If f returns non-nil error, then nextSeries is drained with drainAllSeries.
|
|
func nextSeriesSerialWrapper(nextSeries nextSeriesFunc, f func(s *series) (*series, error)) nextSeriesFunc {
|
|
wrapper := func() (*series, error) {
|
|
for {
|
|
s, err := nextSeries()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s == nil {
|
|
return nil, nil
|
|
}
|
|
sNew, err := f(s)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
if sNew != nil {
|
|
return sNew, nil
|
|
}
|
|
}
|
|
}
|
|
return wrapper
|
|
}
|
|
|
|
// nextSeriesConcurrentWrapper fetches multiple series from nextSeries and calls f on these series from concurrent goroutines.
|
|
//
|
|
// This function is useful for parallelizing CPU-bound f across available CPU cores.
|
|
// f must be goroutine-safe, since it is called from multiple concurrent goroutines.
|
|
// See nextSeriesSerialWrapper for serial calls to f.
|
|
//
|
|
// If f returns (nil, nil), then the current series is skipped.
|
|
// If f returns non-nil error, then nextSeries is drained.
|
|
//
|
|
// nextSeries is called serially.
|
|
func nextSeriesConcurrentWrapper(nextSeries nextSeriesFunc, f func(s *series) (*series, error)) nextSeriesFunc {
|
|
goroutines := cgroup.AvailableCPUs()
|
|
type result struct {
|
|
s *series
|
|
err error
|
|
}
|
|
resultCh := make(chan *result, goroutines)
|
|
seriesCh := make(chan *series, goroutines)
|
|
errCh := make(chan error, 1)
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
go func() {
|
|
var err error
|
|
for {
|
|
s, e := nextSeries()
|
|
if e != nil || s == nil {
|
|
err = e
|
|
break
|
|
}
|
|
seriesCh <- s
|
|
}
|
|
close(seriesCh)
|
|
wg.Wait()
|
|
close(resultCh)
|
|
errCh <- err
|
|
close(errCh)
|
|
}()
|
|
var skipProcessing atomic.Bool
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for s := range seriesCh {
|
|
if skipProcessing.Load() {
|
|
continue
|
|
}
|
|
sNew, err := f(s)
|
|
if err != nil {
|
|
// Drain the rest of series and do not call f for them in order to conserve CPU time.
|
|
skipProcessing.Store(true)
|
|
resultCh <- &result{
|
|
err: err,
|
|
}
|
|
} else if sNew != nil {
|
|
resultCh <- &result{
|
|
s: sNew,
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
wrapper := func() (*series, error) {
|
|
r := <-resultCh
|
|
if r == nil {
|
|
err := <-errCh
|
|
return nil, err
|
|
}
|
|
if r.err != nil {
|
|
// Drain the rest of series before returning the error.
|
|
for {
|
|
_, ok := <-resultCh
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
<-errCh
|
|
return nil, r.err
|
|
}
|
|
if r.s == nil {
|
|
panic(fmt.Errorf("BUG: r.s must be non-nil"))
|
|
}
|
|
return r.s, nil
|
|
}
|
|
return wrapper
|
|
}
|
|
|
|
func newZeroSeriesFunc() nextSeriesFunc {
|
|
f := func() (*series, error) {
|
|
return nil, nil
|
|
}
|
|
return f
|
|
}
|
|
|
|
func unmarshalTags(s string) map[string]string {
|
|
if len(s) == 0 {
|
|
return make(map[string]string)
|
|
}
|
|
tmp := strings.Split(s, ";")
|
|
m := make(map[string]string, len(tmp))
|
|
m["name"] = tmp[0]
|
|
for _, x := range tmp[1:] {
|
|
kv := strings.SplitN(x, "=", 2)
|
|
if len(kv) == 2 {
|
|
m[kv[0]] = kv[1]
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func marshalTags(m map[string]string) string {
|
|
parts := make([]string, 0, len(m))
|
|
parts = append(parts, m["name"])
|
|
for k, v := range m {
|
|
if k != "name" {
|
|
parts = append(parts, k+"="+v)
|
|
}
|
|
}
|
|
sort.Strings(parts[1:])
|
|
return strings.Join(parts, ";")
|
|
}
|
|
|
|
func formatKeyFromTags(tags map[string]string, tagKeys map[string]struct{}, defaultName string) string {
|
|
newTags := make(map[string]string)
|
|
for key := range tagKeys {
|
|
newTags[key] = tags[key]
|
|
}
|
|
if _, ok := tagKeys["name"]; !ok {
|
|
newTags["name"] = defaultName
|
|
}
|
|
return marshalTags(newTags)
|
|
}
|
|
|
|
func formatPathsFromSeries(ss []*series) string {
|
|
seriesExpressions := make([]string, len(ss))
|
|
for i, s := range ss {
|
|
seriesExpressions[i] = s.pathExpression
|
|
}
|
|
return formatPathsFromSeriesExpressions(seriesExpressions, true)
|
|
}
|
|
|
|
func formatAggrFuncForPercentSeriesNames(funcName string, seriesNames []string) string {
|
|
if len(seriesNames) == 0 {
|
|
return "None"
|
|
}
|
|
if len(seriesNames) == 1 {
|
|
return seriesNames[0]
|
|
}
|
|
return formatAggrFuncForSeriesNames(funcName, seriesNames)
|
|
}
|
|
|
|
func formatAggrFuncForSeriesNames(funcName string, seriesNames []string) string {
|
|
if len(seriesNames) == 0 {
|
|
return "None"
|
|
}
|
|
sortPaths := !isSerialFunc(funcName)
|
|
return fmt.Sprintf("%sSeries(%s)", funcName, formatPathsFromSeriesExpressions(seriesNames, sortPaths))
|
|
}
|
|
|
|
func formatPathsFromSeriesExpressions(seriesExpressions []string, sortPaths bool) string {
|
|
if len(seriesExpressions) == 0 {
|
|
return ""
|
|
}
|
|
paths := make([]string, 0, len(seriesExpressions))
|
|
visitedPaths := make(map[string]struct{})
|
|
for _, path := range seriesExpressions {
|
|
if _, ok := visitedPaths[path]; ok {
|
|
continue
|
|
}
|
|
visitedPaths[path] = struct{}{}
|
|
paths = append(paths, path)
|
|
}
|
|
if sortPaths {
|
|
sort.Strings(paths)
|
|
}
|
|
return strings.Join(paths, ",")
|
|
}
|
|
|
|
func newNaNSeries(ec *evalConfig, step int64) *series {
|
|
values := make([]float64, ec.pointsLen(step))
|
|
for i := 0; i < len(values); i++ {
|
|
values[i] = nan
|
|
}
|
|
return &series{
|
|
Tags: map[string]string{},
|
|
Timestamps: ec.newTimestamps(step),
|
|
Values: values,
|
|
step: step,
|
|
}
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.verticalLine
|
|
func transformVerticalLine(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1, 2 or 3 args", len(args))
|
|
}
|
|
tsArg, err := getString(args, "ts", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ts, err := parseTime(ec.currentTime, tsArg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name, err := getOptionalString(args, "label", 1, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
start := ec.startTime
|
|
if ts < start {
|
|
return nil, fmt.Errorf("verticalLine(): timestamp %d exists before start of range: %d", ts, start)
|
|
}
|
|
end := ec.endTime
|
|
if ts > end {
|
|
return nil, fmt.Errorf("verticalLine(): timestamp %d exists after end of range: %d", ts, end)
|
|
}
|
|
s := &series{
|
|
Name: name,
|
|
Tags: unmarshalTags(name),
|
|
Timestamps: []int64{ts, ts},
|
|
Values: []float64{1.0, 1.0},
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: ec.endTime - ec.startTime,
|
|
}
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.useSeriesAbove
|
|
func transformUseSeriesAbove(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 4 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 4 args", len(args))
|
|
}
|
|
value, err := getNumber(args, "value", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
searchRe, err := getRegexp(args, "search", 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replace, err := getRegexpReplacement(args, "replace", 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ss, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var seriesNames []string
|
|
var lock sync.Mutex
|
|
f := nextSeriesConcurrentWrapper(ss, func(s *series) (*series, error) {
|
|
for _, v := range s.Values {
|
|
if v <= value {
|
|
continue
|
|
}
|
|
newName := searchRe.ReplaceAllString(s.Name, replace)
|
|
lock.Lock()
|
|
seriesNames = append(seriesNames, newName)
|
|
lock.Unlock()
|
|
break
|
|
}
|
|
return s, nil
|
|
})
|
|
if _, err = drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
query := fmt.Sprintf("group(%s)", strings.Join(seriesNames, ","))
|
|
return execExpr(ec, query)
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.unique
|
|
func transformUnique(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
var uniqSeries []nextSeriesFunc
|
|
uniq := make(map[string]struct{})
|
|
for i := range args {
|
|
nextSS, err := evalSeriesList(ec, args, "seriesList", i)
|
|
if err != nil {
|
|
for _, s := range uniqSeries {
|
|
_, _ = drainAllSeries(s)
|
|
}
|
|
return nil, err
|
|
}
|
|
// Use nextSeriesSerialWrapper in order to guarantee that the first series among duplicate series is returned.
|
|
nextUniq := nextSeriesSerialWrapper(nextSS, func(s *series) (*series, error) {
|
|
name := s.Name
|
|
if _, ok := uniq[name]; !ok {
|
|
uniq[name] = struct{}{}
|
|
return s, nil
|
|
}
|
|
return nil, nil
|
|
})
|
|
uniqSeries = append(uniqSeries, nextUniq)
|
|
}
|
|
return nextSeriesGroup(uniqSeries, fe), nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.transformNull
|
|
func transformTransformNull(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1,2 or 3 args", len(args))
|
|
}
|
|
defaultValue, err := getOptionalNumber(args, "default", 1, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defaultStr := fmt.Sprintf("%g", defaultValue)
|
|
referenceSeries := getOptionalArg(args, "referenceSeries", 2)
|
|
if referenceSeries == nil {
|
|
// referenceSeries isn't set. Replace all NaNs with defaultValue.
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if math.IsNaN(v) {
|
|
values[i] = defaultValue
|
|
}
|
|
}
|
|
s.Tags["transformNull"] = defaultStr
|
|
s.Name = fmt.Sprintf("transformNull(%s,%s)", s.Name, defaultStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// referenceSeries is set. Replace NaNs with defaultValue only if referenceSeries has non-NaN value at the given point.
|
|
// Series must be normalized in order to match referenceSeries points.
|
|
nextRefSeries, err := evalExpr(ec, referenceSeries.Expr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot evaluate referenceSeries: %w", err)
|
|
}
|
|
ssRef, step, err := fetchNormalizedSeries(ec, nextRefSeries, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replaceNan := make([]bool, ec.pointsLen(step))
|
|
for i := range replaceNan {
|
|
for _, sRef := range ssRef {
|
|
if !math.IsNaN(sRef.Values[i]) {
|
|
replaceNan[i] = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if replaceNan[i] && math.IsNaN(v) {
|
|
values[i] = defaultValue
|
|
}
|
|
}
|
|
s.Tags["transformNull"] = defaultStr
|
|
s.Tags["referenceSeries"] = "1"
|
|
s.Name = fmt.Sprintf("transformNull(%s,%s,referenceSeries)", s.Name, defaultStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.timeStack
|
|
func transformTimeStack(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 4 args", len(args))
|
|
}
|
|
timeShiftUnit, err := getOptionalString(args, "timeShiftUnit", 1, "1d")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delta, err := parseInterval(timeShiftUnit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if delta > 0 && !strings.HasPrefix(timeShiftUnit, "+") {
|
|
delta = -delta
|
|
}
|
|
start, err := getOptionalNumber(args, "timeShiftStart", 2, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
end, err := getOptionalNumber(args, "timeShiftEnd", 3, 7)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if end < start {
|
|
return nil, fmt.Errorf("timeShiftEnd=%g cannot be smaller than timeShiftStart=%g", end, start)
|
|
}
|
|
var allSeries []nextSeriesFunc
|
|
for shift := int64(start); shift <= int64(end); shift++ {
|
|
innerDelta := delta * shift
|
|
ecCopy := *ec
|
|
ecCopy.startTime = ecCopy.startTime + innerDelta
|
|
ecCopy.endTime = ecCopy.endTime + innerDelta
|
|
nextSS, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
for _, f := range allSeries {
|
|
_, _ = drainAllSeries(f)
|
|
}
|
|
return nil, err
|
|
}
|
|
shiftStr := fmt.Sprintf("%d", shift)
|
|
f := nextSeriesConcurrentWrapper(nextSS, func(s *series) (*series, error) {
|
|
timestamps := s.Timestamps
|
|
for i := range timestamps {
|
|
timestamps[i] -= innerDelta
|
|
}
|
|
s.Tags["timeShiftUnit"] = timeShiftUnit
|
|
s.Tags["timeShift"] = shiftStr
|
|
s.Name = fmt.Sprintf("timeShift(%s,%s,%s)", s.Name, timeShiftUnit, shiftStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
|
|
return s, nil
|
|
})
|
|
allSeries = append(allSeries, f)
|
|
}
|
|
return nextSeriesGroup(allSeries, fe), nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.timeSlice
|
|
func transformTimeSlice(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 2 or 3 args", len(args))
|
|
}
|
|
startStr, err := getString(args, "startSliceAt", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
start, err := parseTime(ec.currentTime, startStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endStr, err := getOptionalString(args, "endSliceAt", 2, "now")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
end, err := parseTime(ec.currentTime, endStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
startSecsStr := fmt.Sprintf("%d", start/1000)
|
|
endSecsStr := fmt.Sprintf("%d", end/1000)
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
timestamps := s.Timestamps
|
|
for i := range values {
|
|
if timestamps[i] < start || timestamps[i] > end {
|
|
values[i] = nan
|
|
}
|
|
}
|
|
s.Tags["timeSliceStart"] = startSecsStr
|
|
s.Tags["timeSliceEnd"] = endSecsStr
|
|
s.Name = fmt.Sprintf("timeSlice(%s,%s,%s)", s.Name, startSecsStr, endSecsStr)
|
|
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.timeShift
|
|
func transformTimeShift(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 2 to 4 args", len(args))
|
|
}
|
|
timeShiftStr, err := getString(args, "timeShift", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timeShift, err := parseInterval(timeShiftStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if timeShift > 0 && !strings.HasPrefix(timeShiftStr, "+") {
|
|
timeShift = -timeShift
|
|
}
|
|
resetEnd, err := getOptionalBool(args, "resetEnd", 2, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = getOptionalBool(args, "alignDST", 3, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO: properly use alignDST
|
|
|
|
ecCopy := *ec
|
|
ecCopy.startTime += timeShift
|
|
ecCopy.endTime += timeShift
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
if resetEnd {
|
|
for i, ts := range s.Timestamps {
|
|
if ts > ec.endTime {
|
|
s.Timestamps = s.Timestamps[:i]
|
|
s.Values = s.Values[:i]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
timestamps := s.Timestamps
|
|
for i := range timestamps {
|
|
timestamps[i] -= timeShift
|
|
}
|
|
s.Tags["timeShift"] = timeShiftStr
|
|
s.Name = fmt.Sprintf(`timeShift(%s,%s)`, s.Name, graphiteql.QuoteString(timeShiftStr))
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.nPercentile
|
|
func transformNPercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
n, err := getNumber(args, "n", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nStr := fmt.Sprintf("%g", n)
|
|
aggrFunc := newAggrFuncPercentile(n)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
percentile := aggrFunc(values)
|
|
for i := range values {
|
|
values[i] = percentile
|
|
}
|
|
s.Tags["nPercentile"] = nStr
|
|
s.Name = fmt.Sprintf("nPercentile(%s,%s)", s.Name, nStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.nonNegativeDerivative
|
|
func transformNonNegativeDerivative(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want from 1 to 3", len(args))
|
|
}
|
|
maxValue, err := getOptionalNumber(args, "maxValue", 1, nan)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
minValue, err := getOptionalNumber(args, "minValue", 2, nan)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
prev := nan
|
|
var delta float64
|
|
values := s.Values
|
|
for i, v := range values {
|
|
delta, prev = nonNegativeDelta(v, prev, maxValue, minValue)
|
|
values[i] = delta
|
|
}
|
|
s.Tags["nonNegativeDerivative"] = "1"
|
|
s.Name = fmt.Sprintf(`nonNegativeDerivative(%s)`, s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.offset
|
|
func transformOffset(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
factor, err := getNumber(args, "factor", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
factorStr := fmt.Sprintf("%g", factor)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if !math.IsNaN(v) {
|
|
values[i] = v + factor
|
|
}
|
|
}
|
|
s.Tags["offset"] = factorStr
|
|
s.Name = fmt.Sprintf("offset(%s,%s)", s.Name, factorStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.offsetToZero
|
|
func transformOffsetToZero(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
min := aggrMin(values)
|
|
for i, v := range values {
|
|
values[i] = v - min
|
|
}
|
|
s.Tags["offsetToZero"] = fmt.Sprintf("%g", min)
|
|
s.Name = fmt.Sprintf("offsetToZero(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.perSecond
|
|
func transformPerSecond(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
maxValue, err := getOptionalNumber(args, "maxValue", 1, nan)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
minValue, err := getOptionalNumber(args, "minValue", 2, nan)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
prev := nan
|
|
var delta float64
|
|
values := s.Values
|
|
timestamps := s.Timestamps
|
|
for i, v := range values {
|
|
delta, prev = nonNegativeDelta(v, prev, maxValue, minValue)
|
|
stepSecs := nan
|
|
if i > 0 {
|
|
stepSecs = float64(timestamps[i]-timestamps[i-1]) / 1000
|
|
}
|
|
values[i] = delta / stepSecs
|
|
}
|
|
s.Tags["perSecond"] = "1"
|
|
s.Name = fmt.Sprintf(`perSecond(%s)`, s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
func nonNegativeDelta(curr, prev, max, min float64) (float64, float64) {
|
|
if !math.IsNaN(max) && curr > max {
|
|
return nan, nan
|
|
}
|
|
if !math.IsNaN(min) && curr < min {
|
|
return nan, nan
|
|
}
|
|
if math.IsNaN(curr) || math.IsNaN(prev) {
|
|
return nan, curr
|
|
}
|
|
if curr >= prev {
|
|
return curr - prev, curr
|
|
}
|
|
if !math.IsNaN(max) {
|
|
if math.IsNaN(min) {
|
|
min = float64(0)
|
|
}
|
|
return max + 1 + curr - prev - min, curr
|
|
}
|
|
if !math.IsNaN(min) {
|
|
return curr - min, curr
|
|
}
|
|
return nan, curr
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.threshold
|
|
func transformThreshold(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
value, err := getNumber(args, "value", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
label, err := getOptionalString(args, "label", 1, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = getOptionalString(args, "color", 2, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries := constantLine(ec, fe, value)
|
|
if label == "" {
|
|
return nextSeries, nil
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.Name = label
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sumSeries
|
|
func transformSumSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "sum")
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.substr
|
|
func transformSubstr(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
startf, err := getOptionalNumber(args, "start", 1, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stopf, err := getOptionalNumber(args, "stop", 2, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
path := getPathFromName(s.Name)
|
|
splitName := strings.Split(path, ".")
|
|
start := int(startf)
|
|
stop := int(stopf)
|
|
if start > len(splitName) {
|
|
start = len(splitName)
|
|
} else if start < 0 {
|
|
start = len(splitName) + start
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
}
|
|
if stop == 0 {
|
|
stop = len(splitName)
|
|
} else if stop > len(splitName) {
|
|
stop = len(splitName)
|
|
} else if stop < 0 {
|
|
stop = len(splitName) + stop
|
|
if stop < 0 {
|
|
stop = 0
|
|
}
|
|
}
|
|
if stop < start {
|
|
stop = start
|
|
}
|
|
s.Name = strings.Join(splitName[start:stop], ".")
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.stdev
|
|
func transformStdev(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 2 to 3 args", len(args))
|
|
}
|
|
pointsf, err := getNumber(args, "points", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
points := int(pointsf)
|
|
pointsStr := fmt.Sprintf("%d", points)
|
|
windowTolerance, err := getOptionalNumber(args, "windowTolerance", 2, 0.1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
var sum, sum2 float64
|
|
var n int
|
|
values := s.Values
|
|
dstValues := make([]float64, len(values))
|
|
for i, v := range values {
|
|
if !math.IsNaN(v) {
|
|
n++
|
|
sum += v
|
|
sum2 += v * v
|
|
}
|
|
if i >= points {
|
|
v = values[i-points]
|
|
if !math.IsNaN(v) {
|
|
n--
|
|
sum -= v
|
|
sum2 -= v * v
|
|
}
|
|
}
|
|
stddev := nan
|
|
if n > 0 && float64(n)/pointsf >= windowTolerance {
|
|
stddev = math.Sqrt(float64(n)*sum2-sum*sum) / float64(n)
|
|
}
|
|
dstValues[i] = stddev
|
|
}
|
|
s.Values = dstValues
|
|
s.Tags["stdev"] = pointsStr
|
|
s.Name = fmt.Sprintf("stdev(%s,%s)", s.Name, pointsStr)
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.stddevSeries
|
|
func transformStddevSeries(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
return aggregateSeriesGeneric(ec, fe, "stddev")
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.stacked
|
|
func transformStacked(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 2 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 2 args", len(args))
|
|
}
|
|
stackName, err := getOptionalString(args, "stackName", 1, "__DEFAULT__")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
totalStack := make([]float64, ec.pointsLen(step))
|
|
// Use nextSeriesSerialWrapper instead of nextSeriesConcurrentWrapper for preserving the original order of series.
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
// Consolidation is needed in order to align points in time. Otherwise stacking has little sense.
|
|
s.consolidate(ec, step)
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if !math.IsNaN(v) {
|
|
totalStack[i] += v
|
|
values[i] = totalStack[i]
|
|
}
|
|
}
|
|
if stackName == "__DEFAULT__" {
|
|
s.Tags["stacked"] = stackName
|
|
s.Name = fmt.Sprintf("stacked(%s)", s.Name)
|
|
}
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.squareRoot
|
|
func transformSquareRoot(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1 arg", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = math.Pow(v, 0.5)
|
|
}
|
|
s.Tags["squareRoot"] = "1"
|
|
s.Name = fmt.Sprintf("squareRoot(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sortByTotal
|
|
func transformSortByTotal(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1 arg", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sortByGeneric(fe, nextSeries, "sum", true)
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sortBy
|
|
func transformSortBy(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
funcName, err := getOptionalString(args, "func", 1, "average")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reverse, err := getOptionalBool(args, "reverse", 2, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sortByGeneric(fe, nextSeries, funcName, reverse)
|
|
}
|
|
|
|
func sortByGeneric(fe *graphiteql.FuncExpr, nextSeries nextSeriesFunc, funcName string, reverse bool) (nextSeriesFunc, error) {
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
_, _ = drainAllSeries(nextSeries)
|
|
return nil, err
|
|
}
|
|
var sws []seriesWithWeight
|
|
var ssLock sync.Mutex
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
v := aggrFunc(s.Values)
|
|
if math.IsNaN(v) {
|
|
v = math.Inf(-1)
|
|
}
|
|
s.expr = fe
|
|
ssLock.Lock()
|
|
sws = append(sws, seriesWithWeight{
|
|
s: s,
|
|
v: v,
|
|
})
|
|
ssLock.Unlock()
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Slice(sws, func(i, j int) bool {
|
|
left := sws[i].v
|
|
right := sws[j].v
|
|
if reverse {
|
|
left, right = right, left
|
|
}
|
|
return left < right
|
|
})
|
|
ss := make([]*series, len(sws))
|
|
for i, sw := range sws {
|
|
ss[i] = sw.s
|
|
}
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sortByName
|
|
func transformSortByName(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
natural, err := getOptionalBool(args, "natural", 1, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reverse, err := getOptionalBool(args, "reverse", 2, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
type seriesWithName struct {
|
|
s *series
|
|
name string
|
|
}
|
|
var sns []seriesWithName
|
|
f := nextSeriesSerialWrapper(nextSeries, func(s *series) (*series, error) {
|
|
name := s.Name
|
|
sns = append(sns, seriesWithName{
|
|
s: s,
|
|
name: name,
|
|
})
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Slice(sns, func(i, j int) bool {
|
|
left := sns[i].name
|
|
right := sns[j].name
|
|
if reverse {
|
|
left, right = right, left
|
|
}
|
|
if natural {
|
|
return naturalLess(left, right)
|
|
}
|
|
return left < right
|
|
})
|
|
ss := make([]*series, len(sns))
|
|
for i, sn := range sns {
|
|
ss[i] = sn.s
|
|
}
|
|
return multiSeriesFunc(ss), nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sortByMinima
|
|
func transformSortByMinima(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1 arg", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Filter out series with all the values smaller than 0
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
max := aggrMax(s.Values)
|
|
if math.IsNaN(max) || max <= 0 {
|
|
return nil, nil
|
|
}
|
|
return s, nil
|
|
})
|
|
return sortByGeneric(fe, f, "min", false)
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sortByMaxima
|
|
func transformSortByMaxima(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1 arg", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sortByGeneric(fe, nextSeries, "max", true)
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.smartSummarize
|
|
func transformSmartSummarize(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 2 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want from 2 to 4", len(args))
|
|
}
|
|
intervalString, err := getString(args, "intervalString", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
interval, err := parseInterval(intervalString)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse intervalString: %w", err)
|
|
}
|
|
if interval <= 0 {
|
|
return nil, fmt.Errorf("interval must be positive; got %dms", interval)
|
|
}
|
|
funcName, err := getOptionalString(args, "func", 2, "sum")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aggrFunc, err := getAggrFunc(funcName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alignTo, err := getOptionalString(args, "alignTo", 3, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecCopy := *ec
|
|
if alignTo != "" {
|
|
ecCopy.startTime, err = alignTimeUnit(ecCopy.startTime, alignTo, ec.currentTime.Location())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.summarize(aggrFunc, ecCopy.startTime, ecCopy.endTime, interval, s.xFilesFactor)
|
|
s.Tags["smartSummarize"] = intervalString
|
|
s.Tags["smartSummarizeFunction"] = funcName
|
|
s.Name = fmt.Sprintf("smartSummarize(%s,%s,%s)", s.Name, graphiteql.QuoteString(intervalString), graphiteql.QuoteString(funcName))
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
func alignTimeUnit(startTime int64, s string, tz *time.Location) (int64, error) {
|
|
t := time.Unix(startTime/1e3, (startTime%1000)*1e6).In(tz)
|
|
switch {
|
|
case strings.HasPrefix(s, "ms"):
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), (t.Nanosecond()/1e6)*1e6, tz)
|
|
case strings.HasPrefix(s, "s"):
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, tz)
|
|
case strings.HasPrefix(s, "min"):
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, tz)
|
|
case strings.HasPrefix(s, "h"):
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, tz)
|
|
case strings.HasPrefix(s, "d"):
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, tz)
|
|
case strings.HasPrefix(s, "w"):
|
|
// check for week day to align.
|
|
weekday := s[len(s)-1]
|
|
isoWeekDayAlignTo := 1
|
|
if weekday >= '0' && weekday <= '9' {
|
|
isoWeekDayAlignTo = int(weekday - '0')
|
|
}
|
|
daysToSubtract := int(t.Weekday()) - isoWeekDayAlignTo
|
|
if daysToSubtract < 0 {
|
|
daysToSubtract += 7
|
|
}
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, tz).Add(-time.Hour * 24 * time.Duration(daysToSubtract))
|
|
case strings.HasPrefix(s, "mon"):
|
|
t = time.Date(t.Year(), t.Month(), 0, 0, 0, 0, 0, tz)
|
|
case strings.HasPrefix(s, "y"):
|
|
t = time.Date(t.Year(), 0, 0, 0, 0, 0, 0, tz)
|
|
default:
|
|
return 0, fmt.Errorf("unsupported interval %q", s)
|
|
}
|
|
return t.UnixNano() / 1e6, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sinFunction
|
|
func transformSinFunction(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
name, err := getString(args, "name", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
amplitude, err := getOptionalNumber(args, "amplitude", 1, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := getOptionalNumber(args, "step", 2, 60)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if step <= 0 {
|
|
return nil, fmt.Errorf("step must be positive; got %g", step)
|
|
}
|
|
stepMsecs := int64(step * 1000)
|
|
values := make([]float64, 0, ec.pointsLen(stepMsecs))
|
|
timestamps := make([]int64, 0, ec.pointsLen(stepMsecs))
|
|
ts := ec.startTime
|
|
for ts < ec.endTime {
|
|
v := amplitude * math.Sin(float64(ts)/1000)
|
|
values = append(values, v)
|
|
timestamps = append(timestamps, ts)
|
|
ts += stepMsecs
|
|
}
|
|
s := &series{
|
|
Name: name,
|
|
Tags: unmarshalTags(name),
|
|
Timestamps: timestamps,
|
|
Values: values,
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: stepMsecs,
|
|
}
|
|
return singleSeriesFunc(s), nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sigmoid
|
|
func transformSigmoid(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting 1 arg", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
values[i] = 1 / (1 + math.Exp(-v))
|
|
}
|
|
s.Tags["sigmoid"] = "sigmoid"
|
|
s.Name = fmt.Sprintf("sigmoid(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.scaleToSeconds
|
|
func transformScaleToSeconds(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 2", len(args))
|
|
}
|
|
seconds, err := getNumber(args, "seconds", 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
secondsStr := fmt.Sprintf("%g", seconds)
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
timestamps := s.Timestamps
|
|
values := s.Values
|
|
step := nan
|
|
if len(timestamps) > 1 {
|
|
step = float64(timestamps[1]-timestamps[0]) / 1000
|
|
}
|
|
for i, v := range values {
|
|
if i > 0 {
|
|
step = float64(timestamps[i]-timestamps[i-1]) / 1000
|
|
}
|
|
values[i] = v * (seconds / step)
|
|
}
|
|
s.Tags["scaleToSeconds"] = secondsStr
|
|
s.Name = fmt.Sprintf("scaleToSeconds(%s,%s)", s.Name, secondsStr)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.secondYAxis
|
|
func transformSecondYAxis(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.Tags["secondYAxis"] = "1"
|
|
s.Name = fmt.Sprintf("secondYAxis(%s)", s.Name)
|
|
s.expr = fe
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.isNonNull
|
|
func transformIsNonNull(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of args; got %d; want 1", len(args))
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if math.IsNaN(v) {
|
|
values[i] = 0
|
|
} else {
|
|
values[i] = 1
|
|
}
|
|
}
|
|
s.Tags["isNonNull"] = "1"
|
|
s.Name = fmt.Sprintf("isNonNull(%s)", s.Name)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.sinFunction
|
|
func transformLinearRegression(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ss, _, err := fetchNormalizedSeries(ec, nextSeries, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
startSourceAt := getOptionalArg(args, "startSourceAt", 1)
|
|
endSourceAt := getOptionalArg(args, "endSourceAt", 2)
|
|
|
|
if startSourceAt == nil && endSourceAt == nil {
|
|
// fast path, calculate for series with the same time range.
|
|
return linearRegressionForSeries(ec, fe, ss, ss)
|
|
}
|
|
ecCopy := *ec
|
|
ecCopy.startTime, err = getTimeFromArgExpr(ecCopy.startTime, ecCopy.currentTime, startSourceAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecCopy.endTime, err = getTimeFromArgExpr(ecCopy.endTime, ecCopy.currentTime, endSourceAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextSourceSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceSeries, _, err := fetchNormalizedSeries(&ecCopy, nextSourceSeries, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return linearRegressionForSeries(&ecCopy, fe, ss, sourceSeries)
|
|
}
|
|
|
|
func linearRegressionForSeries(ec *evalConfig, fe *graphiteql.FuncExpr, ss, sourceSeries []*series) (nextSeriesFunc, error) {
|
|
var resp []*series
|
|
for i := 0; i < len(ss); i++ {
|
|
source := sourceSeries[i]
|
|
s := ss[i]
|
|
s.Tags["linearRegressions"] = fmt.Sprintf("%d, %d", ec.startTime/1e3, ec.endTime/1e3)
|
|
s.Tags["name"] = s.Name
|
|
s.Name = fmt.Sprintf("linearRegression(%s, %d, %d)", s.Name, ec.startTime/1e3, ec.endTime/1e3)
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
ok, factor, offset := linearRegressionAnalysis(source, float64(s.step))
|
|
// skip
|
|
if !ok {
|
|
continue
|
|
}
|
|
values := s.Values
|
|
for j := 0; j < len(values); j++ {
|
|
values[j] = offset + (float64(int(s.Timestamps[0])+j*int(s.step)))*factor
|
|
}
|
|
resp = append(resp, s)
|
|
}
|
|
return multiSeriesFunc(resp), nil
|
|
}
|
|
|
|
func getTimeFromArgExpr(originT int64, currentT time.Time, expr *graphiteql.ArgExpr) (int64, error) {
|
|
if expr == nil {
|
|
return originT, nil
|
|
}
|
|
switch data := expr.Expr.(type) {
|
|
case *graphiteql.NoneExpr:
|
|
case *graphiteql.StringExpr:
|
|
t, err := parseTime(currentT, data.S)
|
|
if err != nil {
|
|
return originT, err
|
|
}
|
|
originT = t
|
|
case *graphiteql.NumberExpr:
|
|
originT = int64(data.N * 1e3)
|
|
}
|
|
return originT, nil
|
|
}
|
|
|
|
// Returns is_not_none, factor and offset of linear regression function by least squares method.
|
|
// https://en.wikipedia.org/wiki/Linear_least_squares
|
|
// https://github.com/graphite-project/graphite-web/blob/master/webapp/graphite/render/functions.py#L4158
|
|
func linearRegressionAnalysis(s *series, step float64) (bool, float64, float64) {
|
|
if step == 0 {
|
|
return false, 0, 0
|
|
}
|
|
var sumI, sumII int
|
|
var sumV, sumIV float64
|
|
values := s.Values
|
|
for i, v := range values {
|
|
if math.IsNaN(v) {
|
|
continue
|
|
}
|
|
sumI += i
|
|
sumII += i * i
|
|
sumIV += float64(i) * v
|
|
sumV += v
|
|
}
|
|
denominator := float64(len(values)*sumII - sumI*sumI)
|
|
if denominator == 0 {
|
|
return false, 0.0, 0.0
|
|
}
|
|
factor := (float64(len(values))*sumIV - float64(sumI)*sumV) / denominator / step
|
|
// safe to take index, denominator cannot be non zero in case of empty array.
|
|
offset := (float64(sumII)*sumV-sumIV*float64(sumI))/denominator - factor*float64(s.Timestamps[0])
|
|
return true, factor, offset
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.holtWintersConfidenceBands
|
|
func transformHoltWintersConfidenceBands(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 4 args", len(args))
|
|
}
|
|
resultSeries, err := holtWinterConfidenceBands(ec, fe, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return multiSeriesFunc(resultSeries), nil
|
|
}
|
|
|
|
func holtWinterConfidenceBands(ec *evalConfig, fe *graphiteql.FuncExpr, args []*graphiteql.ArgExpr) ([]*series, error) {
|
|
delta, err := getOptionalNumber(args, "delta", 1, 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bootstrapInterval, err := getOptionalString(args, "bootstrapInterval", 2, "7d")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bootstrapMs, err := parseInterval(bootstrapInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seasonality, err := getOptionalString(args, "seasonality", 3, "1d")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
seasonalityMs, err := parseInterval(seasonality)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecCopy := *ec
|
|
ecCopy.startTime = ecCopy.startTime - bootstrapMs
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
trimWindowPoints := ecCopy.pointsLen(step) - ec.pointsLen(step)
|
|
var resultSeries []*series
|
|
var resultSeriesLock sync.Mutex
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(&ecCopy, step)
|
|
timeStamps := s.Timestamps[trimWindowPoints:]
|
|
analysis := holtWintersAnalysis(s, seasonalityMs)
|
|
forecastValues := analysis.predictions.Values[trimWindowPoints:]
|
|
deviationValues := analysis.deviations.Values[trimWindowPoints:]
|
|
valuesLen := len(forecastValues)
|
|
upperBand := make([]float64, 0, valuesLen)
|
|
lowerBand := make([]float64, 0, valuesLen)
|
|
for i := 0; i < valuesLen; i++ {
|
|
forecastItem := forecastValues[i]
|
|
deviationItem := deviationValues[i]
|
|
if math.IsNaN(forecastItem) || math.IsNaN(deviationItem) {
|
|
upperBand = append(upperBand, nan)
|
|
lowerBand = append(lowerBand, nan)
|
|
} else {
|
|
scaledDeviation := delta * deviationItem
|
|
upperBand = append(upperBand, forecastItem+scaledDeviation)
|
|
lowerBand = append(lowerBand, forecastItem-scaledDeviation)
|
|
}
|
|
}
|
|
name := fmt.Sprintf("holtWintersConfidenceUpper(%s)", s.Name)
|
|
upperSeries := &series{
|
|
Timestamps: timeStamps,
|
|
Values: upperBand,
|
|
Name: name,
|
|
Tags: map[string]string{"holtWintersConfidenceUpper": "1", "name": s.Name},
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: step,
|
|
}
|
|
name = fmt.Sprintf("holtWintersConfidenceLower(%s)", s.Name)
|
|
lowerSeries := &series{
|
|
Timestamps: timeStamps,
|
|
Values: lowerBand,
|
|
Name: name,
|
|
Tags: map[string]string{"holtWintersConfidenceLower": "1", "name": s.Name},
|
|
expr: fe,
|
|
pathExpression: name,
|
|
step: step,
|
|
}
|
|
resultSeriesLock.Lock()
|
|
resultSeries = append(resultSeries, upperSeries, lowerSeries)
|
|
resultSeriesLock.Unlock()
|
|
return s, nil
|
|
})
|
|
if _, err := drainAllSeries(f); err != nil {
|
|
return nil, err
|
|
}
|
|
return resultSeries, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.holtWintersConfidenceArea
|
|
func transformHoltWintersConfidenceArea(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 4 args", len(args))
|
|
}
|
|
bands, err := holtWinterConfidenceBands(ec, fe, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(bands) != 2 {
|
|
return nil, fmt.Errorf("expecting exactly two series; got more series")
|
|
}
|
|
for _, s := range bands {
|
|
s.Name = fmt.Sprintf("areaBetween(%s)", s.Name)
|
|
s.Tags["areaBetween"] = "1"
|
|
}
|
|
return multiSeriesFunc(bands), nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.holtWintersAberration
|
|
func transformHoltWintersAberration(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 4 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 4 args", len(args))
|
|
}
|
|
bands, err := holtWinterConfidenceBands(ec, fe, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
confidenceBands := make(map[string][]float64)
|
|
for _, s := range bands {
|
|
confidenceBands[s.Name] = s.Values
|
|
}
|
|
nextSeries, err := evalSeriesList(ec, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(ec, step)
|
|
values := s.Values
|
|
lowerBand := confidenceBands[fmt.Sprintf("holtWintersConfidenceLower(%s)", s.Name)]
|
|
upperBand := confidenceBands[fmt.Sprintf("holtWintersConfidenceUpper(%s)", s.Name)]
|
|
if len(values) != len(lowerBand) || len(values) != len(upperBand) {
|
|
return nil, fmt.Errorf("bug, len mismatch for series: %d and upperBand values: %d or lowerBand values: %d", len(values), len(upperBand), len(lowerBand))
|
|
}
|
|
aberration := make([]float64, 0, len(values))
|
|
for i := 0; i < len(values); i++ {
|
|
v := values[i]
|
|
upperValue := upperBand[i]
|
|
lowerValue := lowerBand[i]
|
|
if math.IsNaN(v) {
|
|
aberration = append(aberration, 0)
|
|
continue
|
|
}
|
|
if !math.IsNaN(upperValue) && v > upperValue {
|
|
aberration = append(aberration, v-upperValue)
|
|
continue
|
|
}
|
|
if !math.IsNaN(lowerValue) && v < lowerValue {
|
|
aberration = append(aberration, v-lowerValue)
|
|
continue
|
|
}
|
|
aberration = append(aberration, 0)
|
|
}
|
|
s.Tags["holtWintersAberration"] = "1"
|
|
s.Name = fmt.Sprintf("holtWintersAberration(%s)", s.Name)
|
|
s.Values = aberration
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
}
|
|
|
|
// https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.holtWintersForecast
|
|
func transformHoltWintersForecast(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
|
|
args := fe.Args
|
|
if len(args) < 1 || len(args) > 3 {
|
|
return nil, fmt.Errorf("unexpected number of args: %d; expecting from 1 to 3 args", len(args))
|
|
}
|
|
bootstrapInterval, err := getOptionalString(args, "bootstrapInterval", 1, "7d")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bootstrapMs, err := parseInterval(bootstrapInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seasonality, err := getOptionalString(args, "seasonality", 2, "1d")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seasonalityMs, err := parseInterval(seasonality)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ecCopy := *ec
|
|
ecCopy.startTime = ecCopy.startTime - bootstrapMs
|
|
nextSeries, err := evalSeriesList(&ecCopy, args, "seriesList", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := nextSeries.peekStep(ec.storageStep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
trimWindowPoints := ecCopy.pointsLen(step) - ec.pointsLen(step)
|
|
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
|
s.consolidate(&ecCopy, step)
|
|
analysis := holtWintersAnalysis(s, seasonalityMs)
|
|
predictions := analysis.predictions
|
|
|
|
s.Tags["holtWintersForecast"] = "1"
|
|
s.Values = predictions.Values[trimWindowPoints:]
|
|
s.Timestamps = predictions.Timestamps[trimWindowPoints:]
|
|
newName := fmt.Sprintf("holtWintersForecast(%s)", s.Name)
|
|
s.Name = newName
|
|
s.Tags["name"] = newName
|
|
s.expr = fe
|
|
s.pathExpression = s.Name
|
|
|
|
return s, nil
|
|
})
|
|
return f, nil
|
|
|
|
}
|
|
|
|
func holtWintersAnalysis(s *series, seasonality int64) holtWintersAnalysisResult {
|
|
alpha := 0.1
|
|
gamma := alpha
|
|
beta := 0.0035
|
|
|
|
seasonLength := seasonality / s.step
|
|
|
|
var intercept, seasonal, deviation, slope float64
|
|
|
|
intercepts := make([]float64, 0, len(s.Values))
|
|
predictions := make([]float64, 0, len(s.Values))
|
|
slopes := make([]float64, 0, len(s.Values))
|
|
seasonals := make([]float64, 0, len(s.Values))
|
|
deviations := make([]float64, 0, len(s.Values))
|
|
|
|
getlastSeasonal := func(i int64) float64 {
|
|
j := i - seasonLength
|
|
if j >= 0 {
|
|
return seasonals[j]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
getlastDeviation := func(i int64) float64 {
|
|
j := i - seasonLength
|
|
if j >= 0 {
|
|
return deviations[j]
|
|
}
|
|
return 0
|
|
}
|
|
var lastSeasonal, lastSeasonalDev, nextLastSeasonal float64
|
|
nextPred := nan
|
|
|
|
for i, v := range s.Values {
|
|
if math.IsNaN(v) {
|
|
intercepts = append(intercepts, 0)
|
|
slopes = append(slopes, 0)
|
|
seasonals = append(seasonals, 0)
|
|
predictions = append(predictions, nextPred)
|
|
deviations = append(deviations, 0)
|
|
nextPred = nan
|
|
continue
|
|
}
|
|
|
|
var lastIntercept, lastSlope, prediction float64
|
|
if i == 0 {
|
|
lastIntercept = v
|
|
lastSlope = 0
|
|
prediction = v
|
|
} else {
|
|
lastIntercept = intercepts[len(intercepts)-1]
|
|
lastSlope = slopes[len(slopes)-1]
|
|
if math.IsNaN(lastIntercept) {
|
|
lastIntercept = v
|
|
}
|
|
prediction = nextPred
|
|
}
|
|
|
|
lastSeasonal = getlastSeasonal(int64(i))
|
|
nextLastSeasonal = getlastSeasonal(int64(i + 1))
|
|
lastSeasonalDev = getlastDeviation(int64(i))
|
|
|
|
intercept = holtWintersIntercept(alpha, v, lastSeasonal, lastIntercept, lastSlope)
|
|
slope = holtWintersSlope(beta, intercept, lastIntercept, lastSlope)
|
|
seasonal = holtWintersSeasonal(gamma, v, intercept, lastSeasonal)
|
|
|
|
nextPred = intercept + slope + nextLastSeasonal
|
|
deviation = holtWintersDeviation(gamma, v, prediction, lastSeasonalDev)
|
|
|
|
intercepts = append(intercepts, intercept)
|
|
slopes = append(slopes, slope)
|
|
seasonals = append(seasonals, seasonal)
|
|
predictions = append(predictions, prediction)
|
|
deviations = append(deviations, deviation)
|
|
}
|
|
forecastSeries := &series{
|
|
Timestamps: s.Timestamps,
|
|
Values: predictions,
|
|
Name: fmt.Sprintf("holtWintersForecast(%s)", s.Name),
|
|
step: s.step,
|
|
}
|
|
deviationsSS := &series{
|
|
Timestamps: s.Timestamps,
|
|
Values: deviations,
|
|
Name: fmt.Sprintf("holtWintersDeviation(%s)", s.Name),
|
|
step: s.step,
|
|
}
|
|
|
|
return holtWintersAnalysisResult{
|
|
deviations: deviationsSS,
|
|
predictions: forecastSeries,
|
|
}
|
|
}
|
|
|
|
type holtWintersAnalysisResult struct {
|
|
predictions *series
|
|
deviations *series
|
|
}
|
|
|
|
func holtWintersIntercept(alpha, actual, lastReason, lastIntercept, lastSlope float64) float64 {
|
|
return alpha*(actual-lastReason) + (1-alpha)*(lastIntercept+lastSlope)
|
|
}
|
|
|
|
func holtWintersSlope(beta, intercept, lastIntercept, lastSlope float64) float64 {
|
|
return beta*(intercept-lastIntercept) + (1-beta)*lastSlope
|
|
}
|
|
func holtWintersSeasonal(gamma, actual, intercept, lastSeason float64) float64 {
|
|
return gamma*(actual-intercept) + (1-gamma)*lastSeason
|
|
}
|
|
|
|
func holtWintersDeviation(gamma, actual, prediction, lastSeasonalDev float64) float64 {
|
|
if math.IsNaN(prediction) {
|
|
prediction = 0
|
|
}
|
|
return gamma*math.Abs(actual-prediction) + (1-gamma)*lastSeasonalDev
|
|
}
|
|
|
|
func (nsf *nextSeriesFunc) peekStep(step int64) (int64, error) {
|
|
nextSeries := *nsf
|
|
s, err := nextSeries()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if s != nil {
|
|
step = s.step
|
|
}
|
|
var calls atomic.Uint64
|
|
*nsf = func() (*series, error) {
|
|
if calls.Add(1) == 1 {
|
|
return s, nil
|
|
}
|
|
return nextSeries()
|
|
}
|
|
return step, nil
|
|
}
|