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

     1  package movingMedian
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"strconv"
     7  
     8  	"github.com/JaderDias/movingmedian"
     9  	"github.com/lomik/zapwriter"
    10  	"github.com/spf13/viper"
    11  	"go.uber.org/zap"
    12  
    13  	"github.com/go-graphite/carbonapi/expr/helper"
    14  	"github.com/go-graphite/carbonapi/expr/interfaces"
    15  	"github.com/go-graphite/carbonapi/expr/types"
    16  	"github.com/go-graphite/carbonapi/pkg/parser"
    17  )
    18  
    19  type movingMedian struct {
    20  	config movingMedianConfig
    21  }
    22  
    23  func GetOrder() interfaces.Order {
    24  	return interfaces.Any
    25  }
    26  
    27  type movingMedianConfig struct {
    28  	ReturnNaNsIfStepMismatch *bool
    29  }
    30  
    31  func New(configFile string) []interfaces.FunctionMetadata {
    32  	logger := zapwriter.Logger("functionInit").With(zap.String("function", "movingMedian"))
    33  	res := make([]interfaces.FunctionMetadata, 0)
    34  	f := &movingMedian{}
    35  	functions := []string{"movingMedian"}
    36  	for _, n := range functions {
    37  		res = append(res, interfaces.FunctionMetadata{Name: n, F: f})
    38  	}
    39  
    40  	cfg := movingMedianConfig{}
    41  	v := viper.New()
    42  	v.SetConfigFile(configFile)
    43  	err := v.ReadInConfig()
    44  	if err != nil {
    45  		logger.Info("failed to read config file, using default",
    46  			zap.Error(err),
    47  		)
    48  	} else {
    49  		err = v.Unmarshal(&cfg)
    50  		if err != nil {
    51  			logger.Fatal("failed to parse config",
    52  				zap.Error(err),
    53  			)
    54  			return nil
    55  		}
    56  		f.config = cfg
    57  	}
    58  
    59  	if cfg.ReturnNaNsIfStepMismatch == nil {
    60  		v := true
    61  		f.config.ReturnNaNsIfStepMismatch = &v
    62  	}
    63  	return res
    64  }
    65  
    66  // movingMedian(seriesList, windowSize)
    67  func (f *movingMedian) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
    68  	if e.ArgsLen() < 2 {
    69  		return nil, parser.ErrMissingArgument
    70  	}
    71  
    72  	var n int
    73  	var err error
    74  
    75  	var scaleByStep bool
    76  
    77  	var argstr string
    78  
    79  	switch e.Arg(1).Type() {
    80  	case parser.EtConst:
    81  		n, err = e.GetIntArg(1)
    82  		argstr = strconv.Itoa(n)
    83  	case parser.EtString:
    84  		var n32 int32
    85  		n32, err = e.GetIntervalArg(1, 1)
    86  		n = int(n32)
    87  		argstr = "'" + e.Arg(1).StringValue() + "'"
    88  		scaleByStep = true
    89  	default:
    90  		err = parser.ErrBadType
    91  	}
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	windowSize := n
    97  
    98  	start := from
    99  	if scaleByStep {
   100  		start -= int64(n)
   101  	}
   102  
   103  	arg, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), start, until, values)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	if len(arg) == 0 {
   109  		return nil, nil
   110  	}
   111  
   112  	var offset int
   113  
   114  	if scaleByStep {
   115  		windowSize /= int(arg[0].StepTime)
   116  		offset = windowSize
   117  	}
   118  
   119  	result := make([]*types.MetricData, len(arg))
   120  	for n, a := range arg {
   121  		r := a.CopyLink()
   122  		r.Name = "movingMedian(" + a.Name + "," + argstr + ")"
   123  
   124  		if windowSize == 0 {
   125  			if *f.config.ReturnNaNsIfStepMismatch {
   126  				r.Values = make([]float64, len(a.Values))
   127  				for i := range a.Values {
   128  					r.Values[i] = math.NaN()
   129  				}
   130  			}
   131  		} else {
   132  			r.Values = make([]float64, len(a.Values)-offset)
   133  			r.StartTime = (from + r.StepTime - 1) / r.StepTime * r.StepTime // align StartTime to closest >= StepTime
   134  			r.StopTime = r.StartTime + int64(len(r.Values))*r.StepTime
   135  
   136  			data := movingmedian.NewMovingMedian(windowSize)
   137  
   138  			for i, v := range a.Values {
   139  				data.Push(v)
   140  
   141  				if ridx := i - offset; ridx >= 0 {
   142  					r.Values[ridx] = math.NaN()
   143  					if i >= (windowSize - 1) {
   144  						r.Values[ridx] = data.Median()
   145  					}
   146  				}
   147  			}
   148  		}
   149  		r.Tags["movingMedian"] = argstr
   150  		result[n] = r
   151  	}
   152  	return result, nil
   153  }
   154  
   155  // Description is auto-generated description, based on output of https://github.com/graphite-project/graphite-web
   156  func (f *movingMedian) Description() map[string]types.FunctionDescription {
   157  	return map[string]types.FunctionDescription{
   158  		"movingMedian": {
   159  			Description: "Graphs the moving median of a metric (or metrics) over a fixed number of\npast points, or a time interval.\n\nTakes one metric or a wildcard seriesList followed by a number N of datapoints\nor a quoted string with a length of time like '1hour' or '5min' (See ``from /\nuntil`` in the render\\_api_ for examples of time formats), and an xFilesFactor value to specify\nhow many points in the window must be non-null for the output to be considered valid. Graphs the\nmedian of the preceeding datapoints for each point on the graph.\n\nExample:\n\n.. code-block:: none\n\n  &target=movingMedian(Server.instance01.threads.busy,10)\n  &target=movingMedian(Server.instance*.threads.idle,'5min')",
   160  			Function:    "movingMedian(seriesList, windowSize, xFilesFactor=None)",
   161  			Group:       "Calculate",
   162  			Module:      "graphite.render.functions",
   163  			Name:        "movingMedian",
   164  			Params: []types.FunctionParam{
   165  				{
   166  					Name:     "seriesList",
   167  					Required: true,
   168  					Type:     types.SeriesList,
   169  				},
   170  				{
   171  					Name:     "windowSize",
   172  					Required: true,
   173  					Suggestions: types.NewSuggestions(
   174  						5,
   175  						7,
   176  						10,
   177  						"1min",
   178  						"5min",
   179  						"10min",
   180  						"30min",
   181  						"1hour",
   182  					),
   183  					Type: types.IntOrInterval,
   184  				},
   185  				{
   186  					Name: "xFilesFactor",
   187  					Type: types.Float,
   188  				},
   189  			},
   190  			NameChange:   true, // name changed
   191  			ValuesChange: true, // values changed
   192  		},
   193  	}
   194  }