// Copyright 2019 Yunion // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package conditions import ( "math" "sort" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/utils" "yunion.io/x/onecloud/pkg/apis/monitor" "yunion.io/x/onecloud/pkg/monitor/validators" ) type Reducer interface { Reduce(series *monitor.TimeSeries) (*float64, []string) GetType() monitor.ReducerType GetParams() []float64 } // queryReducer reduces an timeseries to a float type queryReducer struct { // Type is how the timeseries should be reduced. // Ex: avg, sum, max, min, count Type monitor.ReducerType Params []float64 } func (s *queryReducer) GetParams() []float64 { return s.Params } func (s *queryReducer) GetType() monitor.ReducerType { return s.Type } func getRoundFloat(point monitor.TimePoint) float64 { pv := point.Value() if pv < 1 { return pv } return math.Trunc(pv) } func (s *queryReducer) Reduce(series *monitor.TimeSeries) (*float64, []string) { if len(series.Points) == 0 { return nil, nil } value := float64(0) allNull := true valArr := make([]string, 0) switch s.Type { case monitor.REDUCER_AVG: validPointsCount := 0 for _, point := range series.Points { if point.IsValid() { value += point.Value() validPointsCount++ allNull = false } } if validPointsCount > 0 { value = value / float64(validPointsCount) } case monitor.REDUCER_SUM: for _, point := range series.Points { if point.IsValid() { value += point.Value() allNull = false } } case monitor.REDUCER_MIN: value = math.MaxFloat64 for _, point := range series.Points { if point.IsValid() { allNull = false pv := getRoundFloat(point) if value > pv { value = pv } } } case monitor.REDUCER_MAX: value = -math.MaxFloat64 for _, point := range series.Points { if point.IsValid() { allNull = false pv := getRoundFloat(point) if value < pv { value = pv } } } case monitor.REDUCER_COUNT: value = float64(len(series.Points)) allNull = false case monitor.REDUCER_LAST: points := series.Points for i := len(points) - 1; i >= 0; i-- { if points[i].IsValid() { value = points[i].Value() valArr = points[i].PointValueStr() allNull = false break } } case monitor.REDUCER_MEDIAN: var values []float64 for _, v := range series.Points { if v.IsValid() { allNull = false values = append(values, v.Value()) } } if len(values) >= 1 { sort.Float64s(values) length := len(values) if length%2 == 1 { value = values[(length-1)/2] } else { value = (values[(length/2)-1] + values[length/2]) / 2 } } case monitor.REDUCER_DIFF: allNull, value = calculateDiff(series, allNull, value, diff) case monitor.REDUCER_DELTA: allNull, value = calculateDelta(series, allNull, value) case monitor.REDUCER_PERCENT_DIFF: allNull, value = calculateDiff(series, allNull, value, percentDiff) case monitor.REDUCER_COUNT_NON_NULL: for _, v := range series.Points { if v.IsValid() { value++ } } if value > 0 { allNull = false } case monitor.REDUCER_PERCENTILE: var values []float64 for _, v := range series.Points { if v.IsValid() { allNull = false values = append(values, v.Value()) } } pNum := float64(95) if len(s.Params) != 0 { pNum = s.Params[0] } if len(values) >= 1 { sort.Float64s(values) length := len(values) index := math.Floor(float64(length) * pNum / float64(100)) value = values[int64(index)] } } if allNull { return nil, nil } return &value, valArr } func newSimpleReducer(cond *monitor.Condition) *queryReducer { return &queryReducer{ Type: monitor.ReducerType(cond.Type), Params: cond.Params, } } func newSimpleReducerByType(typ string) *queryReducer { return &queryReducer{ Type: monitor.ReducerType(typ), Params: []float64{}, } } func calculateDiff(series *monitor.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) { var ( points = series.Points first float64 i int ) // get the newest point for i = len(points) - 1; i >= 0; i-- { if points[i].IsValid() { allNull = false first = points[i].Value() break } } if i >= 1 { // get the oldest point for i := 0; i < len(points); i++ { if points[i].IsValid() { allNull = false val := fn(first, points[i].Value()) value = math.Abs(val) break } } } return allNull, value } var diff = func(newest, oldest float64) float64 { return newest - oldest } var percentDiff = func(newest, oldest float64) float64 { return (newest - oldest) / oldest * 100 } func calculateDelta(series *monitor.TimeSeries, allNull bool, value float64) (bool, float64) { var ( points = series.Points first float64 i int ) // get the newest point for i = len(points) - 1; i >= 0; i-- { if points[i].IsValid() { allNull = false first = points[i].Value() break } } if i >= 1 { // get the oldest point for i := 0; i < len(points); i++ { if points[i].IsValid() { allNull = false value = first - points[i].Value() break } } } return allNull, value } func NewAlertReducer(cond *monitor.Condition) (Reducer, error) { if len(cond.Operators) == 0 { return newSimpleReducer(cond), nil } if utils.IsInStringArray(cond.Operators[0], validators.CommonAlertReducerFieldOpts) { return newMathReducer(cond) } return nil, errors.Wrapf(errors.Error("reducer operator is illegal"), "operator: %s", cond.Operators[0]) }