github.com/go-graphite/carbonapi@v0.17.0/expr/functions/hitcount/function.go (about) 1 package hitcount 2 3 import ( 4 "context" 5 "math" 6 "strconv" 7 "strings" 8 9 "github.com/go-graphite/carbonapi/expr/helper" 10 "github.com/go-graphite/carbonapi/expr/interfaces" 11 "github.com/go-graphite/carbonapi/expr/types" 12 "github.com/go-graphite/carbonapi/pkg/parser" 13 pb "github.com/go-graphite/protocol/carbonapi_v3_pb" 14 ) 15 16 type hitcount struct{} 17 18 func GetOrder() interfaces.Order { 19 return interfaces.Any 20 } 21 22 func New(configFile string) []interfaces.FunctionMetadata { 23 res := make([]interfaces.FunctionMetadata, 0) 24 f := &hitcount{} 25 functions := []string{"hitcount"} 26 for _, n := range functions { 27 res = append(res, interfaces.FunctionMetadata{Name: n, F: f}) 28 } 29 return res 30 } 31 32 // hitcount(seriesList, intervalString, alignToInterval=False) 33 func (f *hitcount) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) { 34 // TODO(dgryski): make sure the arrays are all the same 'size' 35 if e.ArgsLen() < 2 { 36 return nil, parser.ErrMissingArgument 37 } 38 39 bucketSizeInt32, err := e.GetIntervalArg(1, 1) 40 if err != nil { 41 return nil, err 42 } 43 interval := int64(bucketSizeInt32) 44 45 alignToInterval, err := e.GetBoolNamedOrPosArgDefault("alignToInterval", 2, false) 46 if err != nil { 47 return nil, err 48 } 49 50 if alignToInterval { 51 // from needs to be adjusted before grabbing the series arg as it has been adjusted in the metric request 52 from = helper.AlignStartToInterval(from, until, interval) 53 } 54 55 args, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values) 56 if err != nil { 57 return nil, err 58 } 59 60 if len(args) == 0 { 61 return []*types.MetricData{}, nil 62 } 63 64 start := args[0].StartTime 65 stop := args[0].StopTime 66 67 // Note: the start time for the fetch request is adjusted in expr.Metrics() so that the fetched 68 // data is already aligned by interval if this parameter is set to true 69 if alignToInterval { 70 intervalCount := (stop - start) / interval 71 stop = start + (intervalCount * interval) + interval 72 } 73 74 results := make([]*types.MetricData, 0, len(args)) 75 for _, arg := range args { 76 var nameBuf strings.Builder 77 bucketSizeStr := e.Arg(1).StringValue() 78 nameBuf.Grow(len(arg.Name) + 13 + len(bucketSizeStr)) 79 nameBuf.WriteString("hitcount(") 80 nameBuf.WriteString(arg.Name) 81 nameBuf.WriteString(",'") 82 nameBuf.WriteString(bucketSizeStr) 83 nameBuf.WriteString("'") 84 if alignToInterval { 85 nameBuf.WriteString(",") 86 nameBuf.WriteString(strconv.FormatBool(alignToInterval)) 87 } 88 nameBuf.WriteString(")") 89 90 bucketCount := helper.GetBuckets(start, stop, interval) 91 92 r := &types.MetricData{ 93 FetchResponse: pb.FetchResponse{ 94 Name: nameBuf.String(), 95 StepTime: interval, 96 StartTime: start, 97 StopTime: stop, 98 }, 99 Tags: helper.CopyTags(arg), 100 } 101 r.Tags["hitcount"] = strconv.FormatInt(int64(bucketSizeInt32), 10) 102 103 step := arg.StepTime 104 buckets := make([][]float64, bucketCount) 105 newStart := stop - bucketCount*interval 106 r.StartTime = newStart 107 108 for i, v := range arg.Values { 109 if math.IsNaN(v) { 110 continue 111 } 112 113 start_time := arg.StartTime + int64(i)*step 114 startBucket, startMod := helper.Divmod(start_time-newStart, interval) 115 end_time := start_time + step 116 endBucket, endMod := helper.Divmod(end_time-newStart, interval) 117 118 if endBucket >= bucketCount { 119 endBucket = bucketCount - 1 120 endMod = interval 121 } 122 123 if startBucket == endBucket { 124 // All hits go into a single bucket 125 if startBucket >= 0 { 126 buckets[startBucket] = append(buckets[startBucket], v*float64(endMod-startMod)) 127 } 128 } else { 129 // Spread the hits amongst 2 or more buckets 130 if startBucket >= 0 { 131 buckets[startBucket] = append(buckets[startBucket], v*float64(interval-startMod)) 132 } 133 hitsPerBucket := v * float64(interval) 134 for j := startBucket + 1; j < endBucket; j++ { 135 buckets[j] = append(buckets[j], hitsPerBucket) 136 } 137 if endMod > 0 { 138 buckets[endBucket] = append(buckets[endBucket], v*float64(endMod)) 139 } 140 } 141 } 142 r.Values = make([]float64, len(buckets)) 143 for i, bucket := range buckets { 144 if len(bucket) != 0 { 145 var sum float64 146 for _, v := range bucket { 147 sum += v 148 } 149 r.Values[i] = sum 150 } else { 151 r.Values[i] = math.NaN() 152 } 153 } 154 155 results = append(results, r) 156 } 157 return results, nil 158 } 159 160 // Description is auto-generated description, based on output of https://github.com/graphite-project/graphite-web 161 func (f *hitcount) Description() map[string]types.FunctionDescription { 162 return map[string]types.FunctionDescription{ 163 "hitcount": { 164 Description: "Estimate hit counts from a list of time series.\n\nThis function assumes the values in each time series represent\nhits per second. It calculates hits per some larger interval\nsuch as per day or per hour. This function is like summarize(),\nexcept that it compensates automatically for different time scales\n(so that a similar graph results from using either fine-grained\nor coarse-grained records) and handles rarely-occurring events\ngracefully.", 165 Function: "hitcount(seriesList, intervalString, alignToInterval=False)", 166 Group: "Transform", 167 Module: "graphite.render.functions", 168 Name: "hitcount", 169 Params: []types.FunctionParam{ 170 { 171 Name: "seriesList", 172 Required: true, 173 Type: types.SeriesList, 174 }, 175 { 176 Name: "intervalString", 177 Required: true, 178 Suggestions: types.NewSuggestions( 179 "10min", 180 "1h", 181 "1d", 182 ), 183 Type: types.Interval, 184 }, 185 { 186 Default: types.NewSuggestion(false), 187 Name: "alignToInterval", 188 Type: types.Boolean, 189 }, 190 }, 191 NameChange: true, // name changed 192 ValuesChange: true, // values changed 193 }, 194 } 195 }