github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/segment/results/mresults/metricresults.go (about) 1 /* 2 Copyright 2023. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package mresults 18 19 import ( 20 "errors" 21 "fmt" 22 "strings" 23 "sync" 24 "time" 25 26 pql "github.com/influxdata/promql/v2" 27 "github.com/influxdata/promql/v2/pkg/labels" 28 tsidtracker "github.com/siglens/siglens/pkg/segment/results/mresults/tsid" 29 "github.com/siglens/siglens/pkg/segment/structs" 30 segutils "github.com/siglens/siglens/pkg/segment/utils" 31 "github.com/siglens/siglens/pkg/utils" 32 "github.com/valyala/bytebufferpool" 33 ) 34 35 type bucketState uint8 36 37 const ( 38 SERIES_READING bucketState = iota 39 DOWNSAMPLING 40 AGGREGATED 41 ) 42 43 /* 44 Represents the results for a running query 45 46 Depending on the State the stored information is different: 47 - SERIES_READING: maps a tsid to the all raw read dp & downsampled times 48 - DOWNSAMPLING: maps a group to all downsampled series results. This downsampled series may have repeated timestamps from different tsids 49 - AGGREGATING: maps a groupid to the resulting aggregated values 50 */ 51 type MetricsResult struct { 52 // maps tsid to the raw read series (with downsampled timestamp) 53 AllSeries map[uint64]*Series 54 55 // maps groupid to all raw downsampled series. This downsampled series may have repeated timestamps from different tsids 56 DsResults map[string]*DownsampleSeries 57 // maps groupid to a map of ts to value. This aggregates DsResults based on the aggregation function 58 Results map[string]map[uint32]float64 59 60 State bucketState 61 62 rwLock *sync.RWMutex 63 ErrList []error 64 } 65 66 /* 67 Inits a metricsresults holder 68 69 TODO: depending on metrics query, have different cases on how to resolve dps 70 */ 71 func InitMetricResults(mQuery *structs.MetricsQuery, qid uint64) *MetricsResult { 72 return &MetricsResult{ 73 AllSeries: make(map[uint64]*Series), 74 rwLock: &sync.RWMutex{}, 75 ErrList: make([]error, 0), 76 } 77 } 78 79 /* 80 Add a given series for the tsid and group information 81 82 This does not protect againt concurrency. The caller is responsible for coordination 83 */ 84 func (r *MetricsResult) AddSeries(series *Series, tsid uint64, tsGroupId *bytebufferpool.ByteBuffer) { 85 currSeries, ok := r.AllSeries[tsid] 86 if !ok { 87 currSeries = series 88 r.AllSeries[tsid] = series 89 return 90 } 91 currSeries.Merge(series) 92 } 93 94 /* 95 Return the number of final series in the metrics result 96 */ 97 func (r *MetricsResult) GetNumSeries() uint64 { 98 return uint64(len(r.Results)) 99 } 100 101 /* 102 Downsample all series 103 104 Insert into r.DsResults, mapping a groupid to all RunningDownsample Series entries 105 This means that a single tsid will have unique timetamps, but those timestamps can exist for another tsid 106 */ 107 func (r *MetricsResult) DownsampleResults(ds structs.Downsampler, parallelism int) []error { 108 109 // maps a group id to the running downsampled series 110 allDSSeries := make(map[string]*DownsampleSeries, len(r.AllSeries)) 111 112 var idx int 113 wg := &sync.WaitGroup{} 114 dataLock := &sync.Mutex{} 115 116 errorLock := &sync.Mutex{} 117 errors := make([]error, 0) 118 119 for _, series := range r.AllSeries { 120 wg.Add(1) 121 122 go func(s *Series) { 123 defer wg.Done() 124 125 dsSeries, err := s.Downsample(ds) 126 if err != nil { 127 errorLock.Lock() 128 errors = append(errors, err) 129 errorLock.Unlock() 130 return 131 } 132 133 grp := s.grpID.String() 134 135 dataLock.Lock() 136 allDS, ok := allDSSeries[grp] 137 if !ok { 138 allDSSeries[grp] = dsSeries 139 } else { 140 allDS.Merge(dsSeries) 141 } 142 dataLock.Unlock() 143 }(series) 144 idx++ 145 if idx%parallelism == 0 { 146 wg.Wait() 147 } 148 } 149 wg.Wait() 150 r.DsResults = allDSSeries 151 r.State = DOWNSAMPLING 152 r.AllSeries = nil 153 154 if len(errors) > 0 { 155 return errors 156 } 157 158 return nil 159 } 160 161 /* 162 Aggregate results for series sharing a groupid 163 164 Internally, this will store the final aggregated results 165 e.g. will store avg instead of running sum&count 166 */ 167 func (r *MetricsResult) AggregateResults(parallelism int) []error { 168 if r.State != DOWNSAMPLING { 169 return []error{errors.New("results is not in downsampling state")} 170 } 171 172 r.Results = make(map[string]map[uint32]float64) 173 lock := &sync.Mutex{} 174 wg := &sync.WaitGroup{} 175 176 errorLock := &sync.Mutex{} 177 errors := make([]error, 0) 178 179 var idx int 180 for grpID, runningDS := range r.DsResults { 181 wg.Add(1) 182 go func(grp string, ds *DownsampleSeries) { 183 defer wg.Done() 184 185 grpVal, err := ds.Aggregate() 186 if err != nil { 187 errorLock.Lock() 188 errors = append(errors, err) 189 errorLock.Unlock() 190 return 191 } 192 193 lock.Lock() 194 r.Results[grp] = grpVal 195 lock.Unlock() 196 }(grpID, runningDS) 197 idx++ 198 if idx%parallelism == 0 { 199 wg.Wait() 200 } 201 } 202 203 wg.Wait() 204 r.DsResults = nil 205 r.State = AGGREGATED 206 207 if len(errors) > 0 { 208 return errors 209 } 210 211 return nil 212 } 213 214 /* 215 Apply range function to results for series sharing a groupid. 216 */ 217 func (r *MetricsResult) ApplyRangeFunctionsToResults(parallelism int, function segutils.RangeFunctions) error { 218 219 lock := &sync.Mutex{} 220 wg := &sync.WaitGroup{} 221 errList := []error{} // Thread-safe list of errors 222 223 var idx int 224 for grpID, timeSeries := range r.Results { 225 wg.Add(1) 226 go func(grp string, ts map[uint32]float64, function segutils.RangeFunctions) { 227 defer wg.Done() 228 grpVal, err := ApplyRangeFunction(ts, function) 229 if err != nil { 230 lock.Lock() 231 errList = append(errList, err) 232 lock.Unlock() 233 return 234 } 235 lock.Lock() 236 r.Results[grp] = grpVal 237 lock.Unlock() 238 }(grpID, timeSeries, function) 239 idx++ 240 if idx%parallelism == 0 { 241 wg.Wait() 242 } 243 } 244 245 wg.Wait() 246 r.DsResults = nil 247 248 return nil 249 } 250 251 func (r *MetricsResult) AddError(err error) { 252 r.rwLock.Lock() 253 r.ErrList = append(r.ErrList, err) 254 r.rwLock.Unlock() 255 } 256 257 /* 258 Merge series with global results 259 260 This can only merge results if both structs are in SERIES_READING state 261 */ 262 func (r *MetricsResult) Merge(localRes *MetricsResult) error { 263 if r.State != SERIES_READING || localRes.State != SERIES_READING { 264 return errors.New("merged results are not in serires reading state") 265 } 266 r.rwLock.Lock() 267 defer r.rwLock.Unlock() 268 r.ErrList = append(r.ErrList, localRes.ErrList...) 269 for tsid, series := range localRes.AllSeries { 270 currSeries, ok := r.AllSeries[tsid] 271 if !ok { 272 currSeries = series 273 r.AllSeries[tsid] = series 274 continue 275 } 276 currSeries.Merge(series) 277 } 278 return nil 279 } 280 281 func (r *MetricsResult) GetOTSDBResults(mQuery *structs.MetricsQuery) ([]*structs.MetricsQueryResponse, error) { 282 if r.State != AGGREGATED { 283 return nil, errors.New("results is not in aggregated state") 284 } 285 retVal := make([]*structs.MetricsQueryResponse, len(r.Results)) 286 287 idx := 0 288 uniqueTagKeys := make(map[string]bool) 289 tagKeys := make([]string, 0) 290 for _, tag := range mQuery.TagsFilters { 291 if _, ok := uniqueTagKeys[tag.TagKey]; !ok { 292 uniqueTagKeys[tag.TagKey] = true 293 tagKeys = append(tagKeys, tag.TagKey) 294 } 295 } 296 297 for grpId, results := range r.Results { 298 tags := make(map[string]string) 299 tagValues := strings.Split(grpId, tsidtracker.TAG_VALUE_DELIMITER_STR) 300 if len(tagKeys) != len(tagValues)-1 { 301 err := errors.New("GetResults: the length of tag key and tag value pair must match") 302 return nil, err 303 } 304 305 for index, val := range tagValues[:len(tagValues)-1] { 306 tags[tagKeys[index]] = val 307 } 308 retVal[idx] = &structs.MetricsQueryResponse{ 309 MetricName: mQuery.MetricName, 310 Tags: tags, 311 Dps: results, 312 } 313 idx++ 314 } 315 return retVal, nil 316 } 317 func (r *MetricsResult) GetResultsPromQl(mQuery *structs.MetricsQuery, pqlQuerytype pql.ValueType) ([]*structs.MetricsQueryResponsePromQl, error) { 318 if r.State != AGGREGATED { 319 return nil, errors.New("results is not in aggregated state") 320 } 321 var pqldata structs.Data 322 var series pql.Series 323 var label structs.Label 324 325 retVal := make([]*structs.MetricsQueryResponsePromQl, len(r.Results)) 326 idx := 0 327 uniqueTagKeys := make(map[string]bool) 328 tagKeys := make([]string, 0) 329 for _, tag := range mQuery.TagsFilters { 330 if _, ok := uniqueTagKeys[tag.TagKey]; !ok { 331 uniqueTagKeys[tag.TagKey] = true 332 tagKeys = append(tagKeys, tag.TagKey) 333 } 334 } 335 switch pqlQuerytype { 336 case pql.ValueTypeVector: 337 pqldata.ResultType = pql.ValueType("vector") 338 for grpId, results := range r.Results { 339 tags := make(map[string]string) 340 tagValues := strings.Split(grpId, tsidtracker.TAG_VALUE_DELIMITER_STR) 341 342 if len(tagKeys) != len(tagValues)-1 { 343 err := errors.New("GetResultsPromQl: the length of tag key and tag value pair must match") 344 return nil, err 345 } 346 for index, val := range tagValues[:len(tagValues)-1] { 347 tags[tagKeys[index]] = val 348 label.Name = tagKeys[index] 349 label.Value = val 350 series.Metric = append(series.Metric, labels.Label(label)) 351 } 352 label.Name = "__name__" 353 label.Value = mQuery.MetricName 354 series.Metric = append(series.Metric, labels.Label(label)) 355 for k, v := range results { 356 var point pql.Point 357 point.T = int64(k) 358 point.V = v 359 series.Points = append(series.Points, point) 360 } 361 pqldata.Result = append(pqldata.Result, series) 362 363 retVal[idx] = &structs.MetricsQueryResponsePromQl{ 364 Status: "success", 365 Data: pqldata, 366 } 367 pqldata.Result = nil 368 series = pql.Series{} 369 idx++ 370 } 371 default: 372 return retVal, fmt.Errorf("GetResultsPromQl: Unsupported PromQL query result type") 373 } 374 return retVal, nil 375 } 376 377 func (r *MetricsResult) GetResultsPromQlForUi(mQuery *structs.MetricsQuery, pqlQuerytype pql.ValueType, startTime, endTime, interval uint32) (utils.MetricsStatsResponseInfo, error) { 378 var httpResp utils.MetricsStatsResponseInfo 379 httpResp.AggStats = make(map[string]map[string]interface{}) 380 if r.State != AGGREGATED { 381 return utils.MetricsStatsResponseInfo{}, errors.New("results is not in aggregated state") 382 } 383 uniqueTagKeys := make(map[string]bool) 384 tagKeys := make([]string, 0) 385 for _, tag := range mQuery.TagsFilters { 386 if _, ok := uniqueTagKeys[tag.TagKey]; !ok { 387 uniqueTagKeys[tag.TagKey] = true 388 tagKeys = append(tagKeys, tag.TagKey) 389 } 390 } 391 for grpId, results := range r.Results { 392 tagValues := strings.Split(grpId, tsidtracker.TAG_VALUE_DELIMITER_STR) 393 if len(tagKeys) != len(tagValues)-1 { // Subtract 1 because grpId has a delimiter after the last value 394 err := errors.New("GetResults: the length of tag key and tag value pair must match") 395 return httpResp, err 396 } 397 groupId := mQuery.MetricName + "{" 398 for index, val := range tagValues[:len(tagValues)-1] { 399 groupId += fmt.Sprintf("%v=\"%v\",", tagKeys[index], val) 400 } 401 if last := len(groupId) - 1; last >= 0 && groupId[last] == ',' { 402 groupId = groupId[:last] 403 } 404 groupId += "}" 405 httpResp.AggStats[groupId] = make(map[string]interface{}, 1) 406 for ts, v := range results { 407 temp := time.Unix(int64(ts), 0) 408 409 bucketInterval := temp.Format("2006-01-02T15:04") 410 411 httpResp.AggStats[groupId][bucketInterval] = v 412 } 413 } 414 endEpoch := time.Unix(int64(endTime), 0) 415 startEpoch := time.Unix(int64(startTime), 0) 416 runningTs := startEpoch 417 var startDuration, endDuration int64 418 419 switch pqlQuerytype { 420 case pql.ValueTypeVector: 421 for groupId := range httpResp.AggStats { 422 startDuration = (startEpoch.UnixMilli() / segutils.MS_IN_MIN) * segutils.MS_IN_MIN 423 endDuration = (endEpoch.UnixMilli() / segutils.MS_IN_MIN) * segutils.MS_IN_MIN 424 runningTs = startEpoch 425 426 for endDuration > startDuration+segutils.MS_IN_MIN { 427 runningTs = runningTs.Add(1 * time.Minute) 428 startDuration = startDuration + segutils.MS_IN_MIN 429 bucketInterval := runningTs.Format("2006-01-02T15:04") 430 if _, ok := httpResp.AggStats[groupId][bucketInterval]; !ok { 431 httpResp.AggStats[groupId][bucketInterval] = 0 432 } 433 } 434 435 } 436 default: 437 return httpResp, fmt.Errorf("GetResultsPromQl: Unsupported PromQL query result type") 438 } 439 440 return httpResp, nil 441 }