github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/graphite/common/basic_functions.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  	"errors"
    25  	"fmt"
    26  	"math"
    27  	"regexp"
    28  	"strconv"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/query/graphite/ts"
    33  	xerrors "github.com/m3db/m3/src/x/errors"
    34  )
    35  
    36  var (
    37  	// ErrNegativeCount occurs when the request count is < 0.
    38  	ErrNegativeCount = xerrors.NewInvalidParamsError(errors.New("n must be positive"))
    39  	// ErrEmptySeriesList occurs when a function requires a series as input
    40  	ErrEmptySeriesList = xerrors.NewInvalidParamsError(errors.New("empty series list"))
    41  	// ErrInvalidIntervalFormat occurs when invalid interval string encountered
    42  	ErrInvalidIntervalFormat = xerrors.NewInvalidParamsError(errors.New("invalid format"))
    43  
    44  	reInterval *regexp.Regexp
    45  
    46  	intervals = map[string]time.Duration{
    47  		"s":       time.Second,
    48  		"sec":     time.Second,
    49  		"seconds": time.Second,
    50  		"m":       time.Minute,
    51  		"min":     time.Minute,
    52  		"mins":    time.Minute,
    53  		"minute":  time.Minute,
    54  		"minutes": time.Minute,
    55  		"h":       time.Hour,
    56  		"hr":      time.Hour,
    57  		"hour":    time.Hour,
    58  		"hours":   time.Hour,
    59  		"d":       time.Hour * 24,
    60  		"day":     time.Hour * 24,
    61  		"days":    time.Hour * 24,
    62  		"w":       time.Hour * 24 * 7,
    63  		"week":    time.Hour * 24 * 7,
    64  		"weeks":   time.Hour * 24 * 7,
    65  		"mon":     time.Hour * 24 * 30,
    66  		"month":   time.Hour * 24 * 30,
    67  		"months":  time.Hour * 24 * 30,
    68  		"y":       time.Hour * 24 * 365,
    69  		"year":    time.Hour * 24 * 365,
    70  		"years":   time.Hour * 24 * 365,
    71  	}
    72  )
    73  
    74  const (
    75  	// MillisPerSecond is for millis per second
    76  	MillisPerSecond = 1000
    77  	// SecondsPerMinute is for seconds per minute
    78  	SecondsPerMinute = 60
    79  	// MillisPerMinute is for milliseconds per minute
    80  	MillisPerMinute = MillisPerSecond * SecondsPerMinute
    81  )
    82  
    83  // SeriesListRenamer is a signature for renaming multiple series
    84  // into a single name
    85  type SeriesListRenamer func(series ts.SeriesList) string
    86  
    87  // SeriesRenamer is a signature for renaming a single series
    88  type SeriesRenamer func(series *ts.Series) string
    89  
    90  // Head returns the first n elements of a series list or the entire list
    91  func Head(series ts.SeriesList, n int) (ts.SeriesList, error) {
    92  	if n < 0 {
    93  		return ts.NewSeriesList(), ErrNegativeCount
    94  	}
    95  	r := series.Values[:int(math.Min(float64(n), float64(series.Len())))]
    96  	series.Values = r
    97  	return series, nil
    98  }
    99  
   100  // Identity returns datapoints where the value equals the timestamp of the datapoint.
   101  func Identity(ctx *Context, name string) (ts.SeriesList, error) {
   102  	millisPerStep := int(MillisPerMinute)
   103  	numSteps := int(ctx.EndTime.Sub(ctx.StartTime) / time.Minute)
   104  	vals := ts.NewValues(ctx, millisPerStep, numSteps)
   105  	curTimeInSeconds := ctx.StartTime.Unix()
   106  	for i := 0; i < vals.Len(); i++ {
   107  		vals.SetValueAt(i, float64(curTimeInSeconds))
   108  		curTimeInSeconds += SecondsPerMinute
   109  	}
   110  	newSeries := ts.NewSeries(ctx, name, ctx.StartTime, vals)
   111  	newSeries.Specification = fmt.Sprintf("identity(%q)", name)
   112  	return ts.NewSeriesListWithSeries(newSeries), nil
   113  }
   114  
   115  // Normalize normalizes all input series to the same start time, step size, and end time.
   116  func Normalize(ctx *Context, input ts.SeriesList) (ts.SeriesList, time.Time, time.Time, int, error) {
   117  	numSeries := input.Len()
   118  	if numSeries == 0 {
   119  		return ts.NewSeriesList(), ctx.StartTime, ctx.EndTime, -1, xerrors.NewInvalidParamsError(ErrEmptySeriesList)
   120  	}
   121  	if numSeries == 1 {
   122  		return input, input.Values[0].StartTime(), input.Values[0].EndTime(), input.Values[0].MillisPerStep(), nil
   123  	}
   124  
   125  	lcmMillisPerStep := input.Values[0].MillisPerStep()
   126  	minBegin, maxEnd := input.Values[0].StartTime(), input.Values[0].EndTime()
   127  
   128  	for _, in := range input.Values[1:] {
   129  		lcmMillisPerStep = int(ts.Lcm(int64(lcmMillisPerStep), int64(in.MillisPerStep())))
   130  
   131  		if minBegin.After(in.StartTime()) {
   132  			minBegin = in.StartTime()
   133  		}
   134  
   135  		if maxEnd.Before(in.EndTime()) {
   136  			maxEnd = in.EndTime()
   137  		}
   138  	}
   139  
   140  	// Fix the right interval border to be divisible by interval step.
   141  	maxEnd = maxEnd.Add(-time.Duration(
   142  		int64(maxEnd.Sub(minBegin)/time.Millisecond)%int64(lcmMillisPerStep)) * time.Millisecond)
   143  
   144  	numSteps := ts.NumSteps(minBegin, maxEnd, lcmMillisPerStep)
   145  
   146  	results := make([]*ts.Series, input.Len())
   147  
   148  	for i, in := range input.Values {
   149  		if in.StartTime() == minBegin && in.MillisPerStep() == lcmMillisPerStep && in.Len() == numSteps {
   150  			results[i] = in
   151  			continue
   152  		}
   153  
   154  		c := ts.NewConsolidation(ctx, minBegin, maxEnd, lcmMillisPerStep, ts.Avg)
   155  		c.AddSeries(in, ts.Avg)
   156  		results[i] = c.BuildSeries(in.Name(), ts.Finalize)
   157  	}
   158  
   159  	input.Values = results
   160  	return input, minBegin, maxEnd, lcmMillisPerStep, nil
   161  }
   162  
   163  // Count draws a horizontal line representing the number of nodes found in the seriesList.
   164  func Count(ctx *Context, seriesList ts.SeriesList, renamer SeriesListRenamer) (ts.SeriesList, error) {
   165  	if seriesList.Len() == 0 {
   166  		numSteps := ctx.EndTime.Sub(ctx.StartTime).Minutes()
   167  		vals := ts.NewZeroValues(ctx, MillisPerMinute, int(numSteps))
   168  		return ts.NewSeriesListWithSeries(
   169  			ts.NewSeries(ctx, renamer(seriesList), ctx.StartTime, vals),
   170  		), nil
   171  	}
   172  
   173  	normalized, start, end, millisPerStep, err := Normalize(ctx, seriesList)
   174  	if err != nil {
   175  		return ts.NewSeriesList(), err
   176  	}
   177  	numSteps := int(end.Sub(start) / (time.Duration(millisPerStep) * time.Millisecond))
   178  	vals := ts.NewConstantValues(ctx, float64(normalized.Len()), numSteps, millisPerStep)
   179  	return ts.SeriesList{
   180  		Values:   []*ts.Series{ts.NewSeries(ctx, renamer(normalized), start, vals)},
   181  		Metadata: seriesList.Metadata,
   182  	}, nil
   183  }
   184  
   185  // ParseInterval parses an interval string and returns the corresponding duration.
   186  func ParseInterval(fullInterval string) (time.Duration, error) {
   187  	allIntervals := reInterval.FindAllString(fullInterval, -1)
   188  	output := time.Duration(0)
   189  	if allIntervals == nil {
   190  		return 0, xerrors.NewInvalidParamsError(
   191  			fmt.Errorf("unrecognized interval string: %s", fullInterval))
   192  	}
   193  
   194  	for _, interval := range allIntervals {
   195  		if m := reInterval.FindStringSubmatch(strings.TrimSpace(interval)); len(m) != 0 {
   196  			amount, err := strconv.ParseInt(m[1], 10, 32)
   197  			if err != nil {
   198  				return 0, xerrors.NewInvalidParamsError(err)
   199  			}
   200  
   201  			interval := intervals[strings.ToLower(m[2])]
   202  			output += (interval * time.Duration(amount))
   203  		}
   204  	}
   205  
   206  	return output, nil
   207  }
   208  
   209  // ConstantLine draws a horizontal line at a specified value
   210  func ConstantLine(ctx *Context, value float64) (*ts.Series, error) {
   211  	millisPerStep := int(ctx.EndTime.Sub(ctx.StartTime) / (2 * time.Millisecond))
   212  	if millisPerStep <= 0 {
   213  		err := fmt.Errorf("invalid boundary params: startTime=%v, endTime=%v", ctx.StartTime, ctx.EndTime)
   214  		return nil, err
   215  	}
   216  	name := fmt.Sprintf(FloatingPointFormat, value)
   217  	newSeries := ts.NewSeries(ctx, name, ctx.StartTime, ts.NewConstantValues(ctx, value, 3, millisPerStep))
   218  	return newSeries, nil
   219  }
   220  
   221  // ConstantSeries returns a new constant series with a granularity
   222  // of one data point per second
   223  func ConstantSeries(ctx *Context, value float64) (*ts.Series, error) {
   224  	// NB(jeromefroe): We use a granularity of one second to ensure that when multiple series
   225  	// are normalized the constant series will always have the smallest granularity and will
   226  	// not cause another series to be normalized to a greater granularity.
   227  	numSteps := int(ctx.EndTime.Sub(ctx.StartTime) / time.Second)
   228  	if numSteps <= 0 {
   229  		err := fmt.Errorf("invalid boundary params: startTime=%v, endTime=%v", ctx.StartTime, ctx.EndTime)
   230  		return nil, err
   231  	}
   232  	name := fmt.Sprintf(FloatingPointFormat, value)
   233  	newSeries := ts.NewSeries(ctx, name, ctx.StartTime, ts.NewConstantValues(ctx, value, numSteps, MillisPerSecond))
   234  	return newSeries, nil
   235  }
   236  
   237  // RemoveEmpty removes all series that have NaN data
   238  func RemoveEmpty(ctx *Context, input ts.SeriesList, xFilesFactor float64) (ts.SeriesList, error) {
   239  	output := make([]*ts.Series, 0, input.Len())
   240  	for _, series := range input.Values {
   241  		if series.AllNaN() {
   242  			continue
   243  		}
   244  		nonNulls := 0
   245  		for i := 0; i < series.Len(); i++ {
   246  			v := series.ValueAt(i)
   247  			if !math.IsNaN(v) {
   248  				nonNulls++
   249  			}
   250  		}
   251  
   252  		if float64(nonNulls)/float64(series.Len()) >= xFilesFactor {
   253  			output = append(output, series)
   254  		}
   255  	}
   256  	input.Values = output
   257  	return input, nil
   258  }
   259  
   260  // Changed will output a 1 if the value changed or 0 if not
   261  func Changed(ctx *Context, seriesList ts.SeriesList, renamer SeriesRenamer) (ts.SeriesList, error) {
   262  	results := make([]*ts.Series, 0, seriesList.Len())
   263  	nan := math.NaN()
   264  	for _, series := range seriesList.Values {
   265  		previous := nan
   266  		numSteps := series.Len()
   267  		vals := ts.NewValues(ctx, series.MillisPerStep(), numSteps)
   268  		for i := 0; i < numSteps; i++ {
   269  			v := series.ValueAt(i)
   270  			if math.IsNaN(previous) {
   271  				previous = v
   272  				vals.SetValueAt(i, 0)
   273  			} else if !math.IsNaN(v) && previous != v {
   274  				previous = v
   275  				vals.SetValueAt(i, 1)
   276  			} else {
   277  				vals.SetValueAt(i, 0)
   278  			}
   279  		}
   280  		newSeries := ts.NewSeries(ctx, renamer(series), series.StartTime(), vals)
   281  		results = append(results, newSeries)
   282  	}
   283  	seriesList.Values = results
   284  	return seriesList, nil
   285  }
   286  
   287  func init() {
   288  	intervalNames := make([]string, 0, len(intervals))
   289  
   290  	for name := range intervals {
   291  		intervalNames = append(intervalNames, name)
   292  	}
   293  
   294  	reInterval = regexp.MustCompile("(?i)([+-]?[0-9]+)(s|min|h|d|w|mon|y)([A-Z]*)")
   295  }