github.com/m3db/m3@v1.5.0/src/query/graphite/native/summarize.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 native 22 23 import ( 24 "fmt" 25 "math" 26 "strings" 27 "time" 28 29 "github.com/m3db/m3/src/query/graphite/common" 30 "github.com/m3db/m3/src/query/graphite/ts" 31 "github.com/m3db/m3/src/x/errors" 32 ) 33 34 // summarize summarizes each series into interval buckets of a certain size. 35 func summarize( 36 ctx *common.Context, 37 series singlePathSpec, 38 intervalS, fname string, 39 alignToFrom bool, 40 ) (ts.SeriesList, error) { 41 if fname == "" { 42 fname = "sum" 43 } 44 45 safeAggFn, ok := common.SafeAggregationFns[fname] 46 if !ok { 47 return ts.NewSeriesList(), errors.NewInvalidParamsError(fmt.Errorf( 48 "aggregate function not supported: %s", fname)) 49 } 50 51 interval, err := common.ParseInterval(intervalS) 52 if err != nil || interval <= 0 { 53 err := errors.NewInvalidParamsError(fmt.Errorf( 54 "invalid interval %s: %v", interval, err)) 55 return ts.NewSeriesList(), err 56 } 57 58 alignString := "" 59 if alignToFrom { 60 alignString = ", true" 61 } 62 63 results := make([]*ts.Series, len(series.Values)) 64 for i, series := range series.Values { 65 name := fmt.Sprintf("summarize(%s, %q, %q%s)", series.Name(), intervalS, fname, alignString) 66 results[i] = summarizeTimeSeries(ctx, name, series, interval, safeAggFn, alignToFrom) 67 } 68 69 r := ts.SeriesList(series) 70 r.Values = results 71 return r, nil 72 } 73 74 type summarizeBucket struct { 75 vals []float64 76 } 77 78 func summarizeTimeSeries( 79 ctx *common.Context, 80 newName string, 81 series *ts.Series, 82 interval time.Duration, 83 safeAggFn common.SafeAggregationFn, 84 alignToFrom bool, 85 ) *ts.Series { 86 var ( 87 startTimeInSecs = int(series.StartTime().Unix()) 88 intervalInSecs = int(interval / time.Second) 89 intervalInMsecs = intervalInSecs * 1000 90 buckets = make(map[int]*summarizeBucket) 91 ) 92 93 for i := 0; i < series.Len(); i++ { 94 timestamp, n := int(series.StartTimeForStep(i).Unix()), series.ValueAt(i) 95 if math.IsNaN(n) { 96 continue 97 } 98 99 bucketInterval := timestamp - (timestamp % intervalInSecs) 100 if alignToFrom { 101 bucketInterval = (timestamp - startTimeInSecs) / intervalInSecs 102 } 103 104 if bucket, exists := buckets[bucketInterval]; exists { 105 bucket.vals = append(bucket.vals, n) 106 } else { 107 buckets[bucketInterval] = &summarizeBucket{[]float64{n}} 108 } 109 } 110 111 var ( 112 newStart = series.StartTime() 113 newEnd = series.EndTime() 114 ) 115 116 if !alignToFrom { 117 newStartInSecs, newEndInSecs := newStart.Unix(), newEnd.Unix() 118 newStart = time.Unix(newStartInSecs-newStartInSecs%int64(intervalInSecs), 0) 119 newEnd = time.Unix(newEndInSecs-newEndInSecs%int64(intervalInSecs)+int64(intervalInSecs), 0) 120 } 121 122 var ( 123 numSteps = ts.NumSteps(newStart, newEnd, intervalInMsecs) 124 newValues = ts.NewValues(ctx, intervalInMsecs, numSteps) 125 ) 126 127 for timestamp, i := newStart, 0; i < newValues.Len(); timestamp, i = timestamp.Add(interval), i+1 { 128 timestampInSecs := int(timestamp.Unix()) 129 var bucketInterval int 130 if alignToFrom { 131 bucketInterval = (timestampInSecs - startTimeInSecs) / intervalInSecs 132 } else { 133 bucketInterval = timestampInSecs - (timestampInSecs % intervalInSecs) 134 } 135 136 bucket, bucketExists := buckets[bucketInterval] 137 if bucketExists { 138 safeValue, _, safe := safeAggFn(bucket.vals) 139 if safe { 140 newValues.SetValueAt(i, safeValue) 141 } 142 } 143 } 144 return ts.NewSeries(ctx, newName, newStart, newValues) 145 } 146 147 // smartSummarize is an alias of summarize with alignToFrom set to true 148 func smartSummarize( 149 ctx *common.Context, 150 series singlePathSpec, 151 interval, fname string, 152 ) (ts.SeriesList, error) { 153 alignToFrom := true 154 155 seriesList, err := summarize(ctx, series, interval, fname, alignToFrom) 156 if err != nil { 157 return ts.NewSeriesList(), err 158 } 159 160 results := seriesList.Values 161 for i, series := range seriesList.Values { 162 oldName := series.Name() 163 newName := strings.Replace(oldName, "summarize", "smartSummarize", 1) 164 newName = strings.Replace(newName, ", true", "", 1) 165 results[i] = series.RenamedTo(newName) 166 } 167 168 // Retain whether sort was applied or not and metadata. 169 r := ts.SeriesList(series) 170 r.Values = results 171 return r, nil 172 } 173 174 // specificationFunc determines the output series specification given a series list. 175 type specificationFunc func(ts.SeriesList) string 176 177 func sumSpecificationFunc(series ts.SeriesList) string { 178 return wrapPathExpr("sumSeries", series) 179 } 180 181 func averageSpecificationFunc(series ts.SeriesList) string { 182 return wrapPathExpr("averageSeries", series) 183 } 184 185 func multiplyWithWildcardsSpecificationFunc(series ts.SeriesList) string { 186 return wrapPathExpr("multiplySeriesWithWildcards", series) 187 }