github.com/go-graphite/carbonapi@v0.17.0/expr/functions/slo/function.go (about) 1 package slo 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 8 pbv3 "github.com/go-graphite/protocol/carbonapi_v3_pb" 9 10 "github.com/go-graphite/carbonapi/expr/helper" 11 "github.com/go-graphite/carbonapi/expr/interfaces" 12 "github.com/go-graphite/carbonapi/expr/types" 13 "github.com/go-graphite/carbonapi/pkg/parser" 14 ) 15 16 type slo struct{} 17 18 func GetOrder() interfaces.Order { 19 return interfaces.Any 20 } 21 22 func New(string) []interfaces.FunctionMetadata { 23 return []interfaces.FunctionMetadata{ 24 {F: &slo{}, Name: "slo"}, 25 {F: &slo{}, Name: "sloErrorBudget"}, 26 } 27 } 28 29 func (f *slo) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) { 30 var ( 31 argsExtended, argsWindowed []*types.MetricData 32 bucketSize32 int32 33 windowSize int64 34 delta int64 35 err error 36 ) 37 38 // requested data points' window 39 argsWindowed, err = helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values) 40 if len(argsWindowed) == 0 || err != nil { 41 return nil, err 42 } 43 44 bucketSize32, err = e.GetIntervalArg(1, 1) 45 if err != nil { 46 return nil, err 47 } 48 bucketSize := int64(bucketSize32) 49 intervalStringValue := e.Arg(1).StringValue() 50 51 // there is an opportunity that requested data points' window is smaller than slo interval 52 // e.g.: requesting slo(some.data.series, '30days', above, 0) with window of 6 hours 53 // this means that we're gonna need 2 sets of data points: 54 // - the first one with range [from, until] - for 6 hours 55 // - the second one with range [from - delta, until] - for 30 days 56 // the result's time range will be 6 hours anyway 57 windowSize = until - from 58 if bucketSize > windowSize && !(from == 0 && until == 1) { 59 delta = bucketSize - windowSize 60 argsExtended, err = helper.GetSeriesArg(ctx, eval, e.Arg(0), from-delta, until, values) 61 62 if err != nil { 63 return nil, err 64 } 65 66 if len(argsExtended) != len(argsWindowed) { 67 return nil, fmt.Errorf( 68 "MetricData quantity differs: there is %d for [%d, %d] and %d for [%d, %d]", 69 len(argsExtended), from-delta, until, 70 len(argsWindowed), from, until, 71 ) 72 } 73 } else { 74 argsExtended = argsWindowed 75 } 76 77 value, err := e.GetFloatArg(3) 78 if err != nil { 79 return nil, err 80 } 81 valueStr := e.Arg(3).StringValue() 82 83 methodFoo, methodName, err := f.buildMethod(e, 2, value) 84 if err != nil { 85 return nil, err 86 } 87 88 var ( 89 isErrorBudget bool 90 objective float64 91 objectiveStr string 92 ) 93 94 isErrorBudget = e.Target() == "sloErrorBudget" 95 if isErrorBudget { 96 objective, err = e.GetFloatArg(4) 97 if err != nil { 98 return nil, err 99 } 100 objectiveStr = e.Arg(4).StringValue() 101 } 102 103 results := make([]*types.MetricData, 0, len(argsWindowed)) 104 105 for i, argWnd := range argsWindowed { 106 var ( 107 argExt *types.MetricData 108 resultName string 109 ) 110 111 if isErrorBudget { 112 resultName = "sloErrorBudget(" + argWnd.Name + ", " + intervalStringValue + ", " + methodName + ", " + valueStr + ", " + objectiveStr + ")" 113 } else { 114 resultName = "slo(" + argWnd.Name + ", " + intervalStringValue + ", " + methodName + ", " + valueStr + ")" 115 } 116 117 // buckets qty is calculated based on requested window 118 bucketsQty := helper.GetBuckets(argWnd.StartTime, argWnd.StopTime, bucketSize) 119 120 // result for the given series (argWnd) 121 r := &types.MetricData{ 122 FetchResponse: pbv3.FetchResponse{ 123 Name: resultName, 124 Values: make([]float64, 0, bucketsQty+1), 125 StepTime: bucketSize, 126 StartTime: argWnd.StartTime, 127 StopTime: argWnd.StartTime + (bucketsQty)*bucketSize, 128 }, 129 Tags: argWnd.Tags, 130 } 131 // it's ok to place new element to result and modify it later since it's the pointer 132 results = append(results, r) 133 134 // if the granularity of series exceeds bucket size then 135 // there are not enough data to do the math 136 if argWnd.StepTime > bucketSize { 137 for i := int64(0); i < bucketsQty; i++ { 138 r.Values = append(r.Values, math.NaN()) 139 } 140 continue 141 } 142 143 // extended data points set will be used for calculating matched items 144 argExt = argsExtended[i] 145 146 // calculate SLO using moving window 147 qtyMatched := 0 // bucket matched items quantity 148 qtyNotNull := 0 // bucket not-null items quantity 149 qtyTotal := 0 150 151 timeCurrent := argExt.StartTime 152 timeStop := argExt.StopTime 153 timeBucketStarts := timeCurrent 154 timeBucketEnds := timeCurrent + bucketSize 155 156 // process full buckets 157 for i, argValue := range argExt.Values { 158 qtyTotal++ 159 160 if !math.IsNaN(argExt.Values[i]) { 161 qtyNotNull++ 162 if methodFoo(argValue) { 163 qtyMatched++ 164 } 165 } 166 167 timeCurrent += argExt.StepTime 168 if timeCurrent > timeStop { 169 break 170 } 171 172 if timeCurrent >= timeBucketEnds { // the bucket ends 173 newIsAbsent, newValue := f.buildDataPoint(qtyMatched, qtyNotNull) 174 if isErrorBudget && !newIsAbsent { 175 newValue = (newValue - objective) * float64(bucketSize) 176 } 177 178 r.Values = append(r.Values, newValue) 179 180 // init the next bucket 181 qtyMatched = 0 182 qtyNotNull = 0 183 qtyTotal = 0 184 timeBucketStarts = timeCurrent 185 timeBucketEnds += bucketSize 186 } 187 } 188 189 // partial bucket might remain 190 if qtyTotal > 0 { 191 newIsAbsent, newValue := f.buildDataPoint(qtyMatched, qtyNotNull) 192 if isErrorBudget && !newIsAbsent { 193 newValue = (newValue - objective) * float64(timeCurrent-timeBucketStarts) 194 } 195 196 r.Values = append(r.Values, newValue) 197 } 198 } 199 200 return results, nil 201 } 202 203 func (f *slo) Description() map[string]types.FunctionDescription { 204 return map[string]types.FunctionDescription{ 205 "slo": { 206 Description: "Returns ratio of points which are in `interval` range and are above/below (`method`) than `value`.\n\nExample:\n\n.. code-block:: none\n\n &target=slo(some.data.series, \"1hour\", \"above\", 117)", 207 Function: "slo(seriesList, interval, method, value)", 208 Group: "Transform", 209 Module: "graphite.render.functions", 210 Name: "slo", 211 Params: []types.FunctionParam{ 212 { 213 Name: "seriesList", 214 Required: true, 215 Type: types.SeriesList, 216 }, 217 { 218 Name: "interval", 219 Required: true, 220 Suggestions: types.NewSuggestions( 221 "10min", 222 "1h", 223 "1d", 224 ), 225 Type: types.Interval, 226 }, 227 { 228 Default: types.NewSuggestion("above"), 229 Name: "method", 230 Options: types.StringsToSuggestionList([]string{ 231 "above", 232 "aboveOrEqual", 233 "below", 234 "belowOrEqual", 235 }), 236 Required: true, 237 Type: types.String, 238 }, 239 { 240 Default: types.NewSuggestion(0.0), 241 Name: "value", 242 Required: true, 243 Type: types.Float, 244 }, 245 }, 246 SeriesChange: true, // function aggregate metrics or change series items count 247 NameChange: true, // name changed 248 TagsChange: true, // name tag changed 249 ValuesChange: true, // values changed 250 }, 251 "sloErrorBudget": { 252 Description: "Returns rest failure/error budget for this time interval\n\nExample:\n\n.. code-block:: none\n\n &target=sloErrorBudget(some.data.series, \"1hour\", \"above\", 117, 9999e-4)", 253 Group: "Transform", 254 Function: "sloErrorBudget(seriesList, interval, method, value, objective)", 255 Module: "graphite.render.functions", 256 Name: "sloErrorBudget", 257 Params: []types.FunctionParam{ 258 { 259 Name: "seriesList", 260 Required: true, 261 Type: types.SeriesList, 262 }, 263 { 264 Name: "interval", 265 Required: true, 266 Suggestions: types.NewSuggestions( 267 "10min", 268 "1h", 269 "1d", 270 ), 271 Type: types.Interval, 272 }, 273 { 274 Default: types.NewSuggestion("above"), 275 Name: "method", 276 Options: types.StringsToSuggestionList([]string{ 277 "above", 278 "aboveOrEqual", 279 "below", 280 "belowOrEqual", 281 }), 282 Required: true, 283 Type: types.String, 284 }, 285 { 286 Default: types.NewSuggestion(0.0), 287 Name: "value", 288 Required: true, 289 Type: types.Float, 290 }, 291 { 292 Default: types.NewSuggestion(9999e-4), 293 Name: "objective", 294 Required: true, 295 Type: types.Float, 296 }, 297 }, 298 SeriesChange: true, // function aggregate metrics or change series items count 299 NameChange: true, // name changed 300 TagsChange: true, // name tag changed 301 ValuesChange: true, // values changed 302 }, 303 } 304 }