github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/graphite/common/transform.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package common 22 23 import ( 24 "fmt" 25 "math" 26 27 "github.com/m3db/m3/src/query/graphite/ts" 28 "github.com/m3db/m3/src/x/errors" 29 ) 30 31 // TransformFunc is used by Transform to apply a function 32 // to all values in a series. 33 type TransformFunc func(float64) float64 34 35 // TransformFuncFactory creates transformation functions 36 type TransformFuncFactory func() TransformFunc 37 38 // Transformer transforms a value 39 type Transformer interface { 40 // Apply applies the transformation 41 Apply(value float64) float64 42 43 // Reset resets the state 44 Reset() 45 } 46 47 type statelessTransformer struct { 48 fn TransformFunc 49 } 50 51 // NewStatelessTransformer creates a new stateless transformer 52 func NewStatelessTransformer(fn TransformFunc) Transformer { 53 return statelessTransformer{fn: fn} 54 } 55 56 func (t statelessTransformer) Apply(value float64) float64 { 57 return t.fn(value) 58 } 59 60 func (t statelessTransformer) Reset() {} 61 62 // MaintainNaNTransformer only applies a given ValueTransformer to 63 // non-NaN values. 64 func MaintainNaNTransformer(f TransformFunc) TransformFunc { 65 return func(v float64) float64 { 66 if math.IsNaN(v) { 67 return v 68 } 69 return f(v) 70 } 71 } 72 73 // Scale multiplies each element of a series list by a given value. 74 func Scale(scale float64) TransformFunc { 75 return MaintainNaNTransformer(func(v float64) float64 { 76 return v * scale 77 }) 78 } 79 80 // Offset adds a value to each element of a series list. 81 func Offset(factor float64) TransformFunc { 82 return MaintainNaNTransformer(func(v float64) float64 { 83 return v + factor 84 }) 85 } 86 87 // TransformNull transforms all nulls in a series to a value. 88 func TransformNull(value float64) TransformFunc { 89 return func(v float64) float64 { 90 if math.IsNaN(v) { 91 return value 92 } 93 94 return v 95 } 96 } 97 98 // IsNonNull replaces datapoints that are non-null with 1, and null values with 0. 99 // This is useful for understanding which series have data at a given point in time (i.e. to count 100 // which servers are alive). 101 func IsNonNull() TransformFunc { 102 return func(v float64) float64 { 103 if math.IsNaN(v) { 104 return 0 105 } 106 107 return 1 108 } 109 } 110 111 // PredicateFn is a predicate function. 112 type PredicateFn func(v float64) bool 113 114 // Filter removes data that does not satisfy a given predicate. 115 func Filter(fn PredicateFn) TransformFunc { 116 return MaintainNaNTransformer(func(v float64) float64 { 117 if !fn(v) { 118 return math.NaN() 119 } 120 121 return v 122 }) 123 } 124 125 // Logarithm takes one series or a series list, and draws the y-axis in logarithmic format. Only support 126 // base 10 logarithms. 127 func Logarithm() TransformFunc { 128 return func(v float64) float64 { 129 if !math.IsNaN(v) && v > 0 { 130 return math.Log10(v) 131 } 132 133 return math.NaN() 134 } 135 } 136 137 // Integral returns a function that accumulates values it has seen 138 func Integral() TransformFunc { 139 currentSum := 0.0 140 141 return func(v float64) float64 { 142 if !math.IsNaN(v) { 143 currentSum += v 144 } else { 145 return v 146 } 147 return currentSum 148 } 149 } 150 151 // Derivative returns a function that computes the derivative among the values 152 // it has seen 153 func Derivative() TransformFunc { 154 previousValue := math.NaN() 155 156 return func(v float64) float64 { 157 var r float64 158 if math.IsNaN(v) || math.IsNaN(previousValue) { 159 previousValue, r = v, math.NaN() 160 } else { 161 previousValue, r = v, v-previousValue 162 } 163 return r 164 } 165 } 166 167 // NonNegativeDerivative returns a function that computes the derivative among the 168 // values it has seen but ignores datapoints that trend down 169 func NonNegativeDerivative(maxValue float64) TransformFunc { 170 previousValue := math.NaN() 171 172 return func(v float64) float64 { 173 var r float64 174 175 if math.IsNaN(v) || math.IsNaN(previousValue) { 176 previousValue, r = v, math.NaN() 177 } else if difference := v - previousValue; difference >= 0 { 178 previousValue, r = v, difference 179 } else if !math.IsNaN(maxValue) && maxValue >= v { 180 previousValue, r = v, (maxValue-previousValue)+v+1.0 181 } else { 182 previousValue, r = v, math.NaN() 183 } 184 return r 185 } 186 } 187 188 // Transform applies a specified ValueTransform to all values in each series, renaming 189 // each series with the given SeriesRenamer. 190 func Transform(ctx *Context, in ts.SeriesList, t Transformer, renamer SeriesRenamer) (ts.SeriesList, error) { 191 results := make([]*ts.Series, in.Len()) 192 193 for i, series := range in.Values { 194 t.Reset() 195 values := ts.NewValues(ctx, series.MillisPerStep(), series.Len()) 196 for step := 0; step < series.Len(); step++ { 197 value := series.ValueAt(step) 198 values.SetValueAt(step, t.Apply(value)) 199 } 200 201 results[i] = ts.NewSeries(ctx, renamer(series), series.StartTime(), values) 202 } 203 204 in.Values = results 205 return in, nil 206 } 207 208 // Stdev takes one metric or a wildcard seriesList followed by an integer N. Draw the standard deviation 209 // of all metrics passed for the past N datapoints. If the ratio of null points in the window is greater than 210 // windowTolerance, skip the calculation. 211 func Stdev(ctx *Context, in ts.SeriesList, points int, windowTolerance float64, renamer RenamerWithNumPoints) (ts.SeriesList, error) { 212 if points <= 0 { 213 return ts.NewSeriesList(), errors.NewInvalidParamsError(fmt.Errorf("invalid window size, points=%d", points)) 214 } 215 results := make([]*ts.Series, 0, in.Len()) 216 for _, series := range in.Values { 217 stdevName := renamer(series, points) 218 stdevVals := ts.NewValues(ctx, series.MillisPerStep(), series.Len()) 219 validPoints := 0 220 currentSum := 0.0 221 currentSumOfSquares := 0.0 222 for index := 0; index < series.Len(); index++ { 223 newValue := series.ValueAt(index) 224 var bootstrapping bool 225 var droppedValue float64 226 227 // Mark whether we've reached our window size, don't drop points out otherwise 228 if index < points { 229 bootstrapping = true 230 droppedValue = math.NaN() 231 } else { 232 bootstrapping = false 233 droppedValue = series.ValueAt(index - points) 234 } 235 236 // Remove the value that just dropped out of the window 237 if !bootstrapping && !math.IsNaN(droppedValue) { 238 validPoints-- 239 currentSum -= droppedValue 240 currentSumOfSquares -= droppedValue * droppedValue 241 } 242 243 // Add in the value that just popped in the window 244 if !math.IsNaN(newValue) { 245 validPoints++ 246 currentSum += newValue 247 currentSumOfSquares += newValue * newValue 248 } 249 250 if validPoints > 0 && float64(validPoints)/float64(points) >= windowTolerance { 251 deviation := math.Sqrt(float64(validPoints)*currentSumOfSquares-currentSum*currentSum) / float64(validPoints) 252 stdevVals.SetValueAt(index, deviation) 253 } 254 } 255 stdevSeries := ts.NewSeries(ctx, stdevName, series.StartTime(), stdevVals) 256 results = append(results, stdevSeries) 257 } 258 in.Values = results 259 return in, nil 260 } 261 262 // RenamerWithNumPoints is a signature for renaming a single series that is passed to Stdev 263 type RenamerWithNumPoints func(series *ts.Series, points int) string 264 265 // PerSecond computes the derivative between consecutive values in the a time series, taking into 266 // account the time interval between the values. It skips missing values, and calculates the 267 // derivative between consecutive non-missing values. 268 func PerSecond(ctx *Context, in ts.SeriesList, renamer SeriesRenamer) (ts.SeriesList, error) { 269 results := make([]*ts.Series, 0, in.Len()) 270 271 for _, series := range in.Values { 272 var ( 273 vals = ts.NewValues(ctx, series.MillisPerStep(), series.Len()) 274 prev = math.NaN() 275 secsPerStep = float64(series.MillisPerStep()) / 1000 276 secsSinceLastVal = secsPerStep 277 ) 278 279 for step := 0; step < series.Len(); step++ { 280 cur := series.ValueAt(step) 281 282 if math.IsNaN(prev) { 283 vals.SetValueAt(step, math.NaN()) 284 prev = cur 285 continue 286 } 287 288 if math.IsNaN(cur) { 289 vals.SetValueAt(step, math.NaN()) 290 secsSinceLastVal += secsPerStep 291 continue 292 } 293 294 diff := cur - prev 295 296 if diff >= 0 { 297 vals.SetValueAt(step, diff/secsSinceLastVal) 298 } else { 299 vals.SetValueAt(step, math.NaN()) 300 } 301 302 prev = cur 303 secsSinceLastVal = secsPerStep 304 } 305 306 s := ts.NewSeries(ctx, renamer(series), series.StartTime(), vals) 307 results = append(results, s) 308 } 309 310 in.Values = results 311 return in, nil 312 }