github.com/go-graphite/carbonapi@v0.17.0/expr/functions/timeShiftByMetric/function.go (about) 1 package timeShiftByMetric 2 3 import ( 4 "context" 5 "math" 6 "regexp" 7 "strings" 8 9 "github.com/ansel1/merry" 10 11 "github.com/go-graphite/carbonapi/expr/helper" 12 "github.com/go-graphite/carbonapi/expr/interfaces" 13 "github.com/go-graphite/carbonapi/expr/types" 14 "github.com/go-graphite/carbonapi/pkg/parser" 15 ) 16 17 type offsetByVersion map[string]int64 18 19 type timeShiftByMetric struct{} 20 21 func GetOrder() interfaces.Order { 22 return interfaces.Any 23 } 24 25 func New(configFile string) []interfaces.FunctionMetadata { 26 return []interfaces.FunctionMetadata{{ 27 F: &timeShiftByMetric{}, 28 Name: "timeShiftByMetric", 29 }} 30 } 31 32 // timeShiftByMetric(seriesList, markSource, versionRankIndex) 33 func (f *timeShiftByMetric) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) (resultData []*types.MetricData, resultError error) { 34 params, err := f.extractCallParams(ctx, eval, e, from, until, values) 35 if err != nil { 36 return nil, err 37 } 38 39 latestMarks, err := f.locateLatestMarks(params) 40 if err != nil { 41 return nil, err 42 } 43 44 offsets := f.calculateOffsets(params, latestMarks) 45 46 result := f.applyShift(params, offsets) 47 48 return result, nil 49 } 50 51 // applyShift shifts timeline of those metrics which major version is less than top major version 52 func (f *timeShiftByMetric) applyShift(params *callParams, offsets offsetByVersion) []*types.MetricData { 53 result := make([]*types.MetricData, 0, len(params.metrics)) 54 for _, metric := range params.metrics { 55 offsetIsSet := false 56 offset := int64(0) 57 var possibleVersion string 58 59 name := metric.Tags["name"] 60 nameSplit := strings.Split(name, ".") 61 62 // make sure that there is desired rank at all 63 if params.versionRank >= len(nameSplit) { 64 continue 65 } 66 possibleVersion = nameSplit[params.versionRank] 67 68 if possibleOffset, ok := offsets[possibleVersion]; !ok { 69 for key, value := range offsets { 70 if strings.HasPrefix(key, possibleVersion) { 71 offset = value 72 offsetIsSet = true 73 offsets[possibleVersion] = value 74 } 75 } 76 } else { 77 offset = possibleOffset 78 offsetIsSet = true 79 } 80 81 // checking if it is some version after all, otherwise this series will be omitted 82 if offsetIsSet { 83 r := metric.CopyLinkTags() 84 r.Name = "timeShiftByMetric(" + r.Name + ")" 85 r.StopTime += offset 86 r.StartTime += offset 87 88 result = append(result, r) 89 } 90 } 91 92 return result 93 } 94 95 func (f *timeShiftByMetric) calculateOffsets(params *callParams, versions versionInfos) offsetByVersion { 96 result := make(offsetByVersion) 97 topPosition := versions[0].position 98 99 for _, version := range versions { 100 result[version.mark] = int64(topPosition-version.position) * params.stepTime 101 } 102 103 return result 104 } 105 106 // extractCallParams (preliminarily) validates and extracts parameters of timeShiftByMetric's call as structure 107 func (f *timeShiftByMetric) extractCallParams(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) (*callParams, error) { 108 metrics, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values) 109 if err != nil { 110 return nil, err 111 } 112 113 marks, err := helper.GetSeriesArg(ctx, eval, e.Arg(1), from, until, values) 114 if err != nil { 115 return nil, err 116 } 117 118 versionRank, err := e.GetIntArg(2) 119 if err != nil { 120 return nil, err 121 } 122 123 // validating data sets: both metrics and marks must have at least 2 series each 124 // also, all IsAbsent and Values lengths must be equal to each other 125 pointsQty := -1 126 stepTime := int64(-1) 127 var dataSets map[string][]*types.MetricData = map[string][]*types.MetricData{ 128 "marks": marks, 129 "metrics": metrics, 130 } 131 for name, dataSet := range dataSets { 132 if len(dataSet) < 2 { 133 return nil, merry.WithMessagef(errTooFewDatasets, "bad data: need at least 2 %s data sets to process, got %d", name, len(dataSet)) 134 } 135 136 for _, series := range dataSet { 137 if pointsQty == -1 { 138 pointsQty = len(series.Values) 139 if pointsQty == 0 { 140 return nil, merry.WithMessagef(errEmptySeries, "bad data: empty series %s", series.Name) 141 } 142 } else if pointsQty != len(series.Values) { 143 return nil, merry.WithMessagef(errSeriesLengthMismatch, "bad data: length of Values for series %s differs from others", series.Name) 144 } 145 146 if stepTime == -1 { 147 stepTime = series.StepTime 148 } 149 } 150 } 151 152 result := &callParams{ 153 metrics: metrics, 154 marks: marks, 155 versionRank: versionRank, 156 pointsQty: pointsQty, 157 stepTime: stepTime, 158 } 159 return result, nil 160 } 161 162 var reLocateMark *regexp.Regexp = regexp.MustCompile(`(\d+)_(\d+)`) 163 164 // locateLatestMarks gets the series with marks those look like "65_4" 165 // and looks for the latest ones by _major_ versions 166 // e.g. among set [63_0, 64_0, 64_1, 64_2, 65_0, 65_1] it locates 63_0, 64_4 and 65_1 167 // returns located elements 168 func (f *timeShiftByMetric) locateLatestMarks(params *callParams) (versionInfos, error) { 169 170 versions := make(versionInfos, 0, len(params.marks)) 171 172 // noinspection SpellCheckingInspection 173 for _, mark := range params.marks { 174 markSplit := strings.Split(mark.Tags["name"], ".") 175 markVersion := markSplit[len(markSplit)-1] 176 177 // for mark that matches pattern (\d+)_(\d+), this should return slice of 3 strings exactly 178 submatch := reLocateMark.FindStringSubmatch(markVersion) 179 if len(submatch) != 3 { 180 continue 181 } 182 183 position := -1 184 for i := params.pointsQty - 1; i >= 0; i-- { 185 if !math.IsNaN(mark.Values[i]) { 186 position = i 187 break 188 } 189 } 190 191 if position == -1 { 192 // weird, but mark series has no data in it - skipping 193 continue 194 } 195 // collecting all marks found 196 versions = append(versions, versionInfo{ 197 mark: markVersion, 198 position: position, 199 versionMajor: mustAtoi(submatch[1]), 200 versionMinor: mustAtoi(submatch[2]), 201 }) 202 } 203 204 // obtain top versions for each major version 205 result := versions.HighestVersions() 206 if len(result) < 2 { 207 return nil, merry.WithMessagef(errLessThan2Marks, "bad data: could not find 2 marks, only %d found", len(result)) 208 } else { 209 return result, nil 210 } 211 } 212 213 func (f *timeShiftByMetric) Description() map[string]types.FunctionDescription { 214 return map[string]types.FunctionDescription{ 215 "timeShiftByMetric": types.FunctionDescription{ 216 Description: "Takes a seriesList with wildcard in versionRankIndex rank and applies shift to the closest version from markSource\n\n.. code-block:: none\n\n &target=timeShiftByMetric(carbon.agents.graphite.creates)", 217 Function: "timeShiftByMetric(seriesList, markSource, versionRankIndex)", 218 Group: "Transform", 219 Module: "graphite.render.functions", 220 Name: "timeShiftByMetric", 221 Params: []types.FunctionParam{ 222 types.FunctionParam{ 223 Name: "seriesList", 224 Required: true, 225 Type: types.SeriesList, 226 }, 227 types.FunctionParam{ 228 Name: "markSource", 229 Required: true, 230 Type: types.SeriesList, 231 }, 232 types.FunctionParam{ 233 Name: "versionRankIndex", 234 Required: true, 235 Type: types.Integer, 236 }, 237 }, 238 NameChange: true, // name changed 239 ValuesChange: true, // values changed 240 }, 241 } 242 }