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  }