github.com/m3db/m3@v1.5.0/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 }