// FunctionAnomalyMaker makes anomaly-measurement functions that return simple p-values for deviations from the predicted model. // In order to make this procedure mostly automatic, it performs a join on the original tagsets to match them up with their predictions. func FunctionPeriodicAnomalyMaker(name string, model function.MetricFunction) function.MetricFunction { if model.MinArguments < 2 { panic("FunctionAnomalyMaker requires that the model argument take at least two parameters; series and period.") } return function.MetricFunction{ Name: name, MinArguments: model.MinArguments, MaxArguments: model.MaxArguments, Compute: func(context *function.EvaluationContext, arguments []function.Expression, groups function.Groups) (function.Value, error) { original, err := function.EvaluateToSeriesList(arguments[0], context) if err != nil { return nil, err } predictionValue, err := model.Compute(context, arguments, groups) if err != nil { return nil, err // TODO: add decoration to describe it's coming from the anomaly function } prediction, err := predictionValue.ToSeriesList(context.Timerange) if err != nil { return nil, err } period, err := function.EvaluateToDuration(arguments[1], context) if err != nil { return nil, err } periodSlots := int(period / context.Timerange.Resolution()) // Now we need to match up 'original' and 'prediction' // We'll use a hashmap for now. // TODO: clean this up to hog less memory lookup := map[string][]float64{} for _, series := range original.Series { lookup[series.TagSet.Serialize()] = series.Values } result := make([]api.Timeseries, len(prediction.Series)) for i, series := range prediction.Series { result[i] = series result[i].Values, err = pValueFromNormalDifferenceSlices(lookup[series.TagSet.Serialize()], series.Values, periodSlots) if err != nil { return nil, err } } prediction.Series = result return prediction, nil }, } }
package forecast import ( "math" "github.com/square/metrics/api" "github.com/square/metrics/function" ) var FunctionDrop = function.MetricFunction{ Name: "forecast.drop", MinArguments: 2, MaxArguments: 2, Compute: func(context *function.EvaluationContext, arguments []function.Expression, groups function.Groups) (function.Value, error) { original, err := function.EvaluateToSeriesList(arguments[0], context) if err != nil { return nil, err } dropTime, err := function.EvaluateToDuration(arguments[1], context) if err != nil { return nil, err } lastValue := float64(context.Timerange.Slots()) - dropTime.Seconds()/context.Timerange.Resolution().Seconds() result := make([]api.Timeseries, len(original.Series)) for i, series := range original.Series { values := make([]float64, len(series.Values)) result[i] = series for j := range values { if float64(j) < lastValue { values[j] = series.Values[j]
return nil, err } } if extraTrainingTime < 0 { return nil, fmt.Errorf("Extra training time must be non-negative, but got %s", extraTrainingTime.String()) // TODO: use structured error } samples := int(period / context.Timerange.Resolution()) if samples <= 0 { return nil, fmt.Errorf("forecast.rolling_multiplicative_holt_winters expects the period parameter to mean at least one slot") // TODO: use a structured error } newContext := context.Copy() newContext.Timerange = newContext.Timerange.ExtendBefore(extraTrainingTime) extraSlots := newContext.Timerange.Slots() - context.Timerange.Slots() seriesList, err := function.EvaluateToSeriesList(arguments[0], &newContext) context.CopyNotesFrom(&newContext) newContext.Invalidate() if err != nil { return nil, err } result := api.SeriesList{ Series: make([]api.Timeseries, len(seriesList.Series)), Timerange: context.Timerange, Name: seriesList.Name, Query: fmt.Sprintf("forecast.rolling_multiplicative_holt_winters(%s, %s, %f, %f)", seriesList.Query, period.String(), seasonalLearningRate, trendLearningRate), } if extraTrainingTime > 0 { result.Query = fmt.Sprintf("forecast.rolling_multiplicative_holt_winters(%s, %s, %f, %f, %s)", seriesList.Query, period.String(), seasonalLearningRate, trendLearningRate, extraTrainingTime.String()) }