github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/metrics/rules/active_ruleset.go (about) 1 // Copyright (c) 2020 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 rules 22 23 import ( 24 "bytes" 25 "fmt" 26 "sort" 27 28 "github.com/m3db/m3/src/metrics/aggregation" 29 "github.com/m3db/m3/src/metrics/filters" 30 "github.com/m3db/m3/src/metrics/metadata" 31 "github.com/m3db/m3/src/metrics/metric" 32 metricid "github.com/m3db/m3/src/metrics/metric/id" 33 mpipeline "github.com/m3db/m3/src/metrics/pipeline" 34 "github.com/m3db/m3/src/metrics/pipeline/applied" 35 "github.com/m3db/m3/src/metrics/rules/view" 36 "github.com/m3db/m3/src/query/models" 37 xerrors "github.com/m3db/m3/src/x/errors" 38 ) 39 40 type activeRuleSet struct { 41 version int 42 mappingRules []*mappingRule 43 rollupRules []*rollupRule 44 cutoverTimesAsc []int64 45 tagsFilterOpts filters.TagsFilterOptions 46 newRollupIDFn metricid.NewIDFn 47 isRollupIDFn metricid.MatchIDFn 48 } 49 50 func newActiveRuleSet( 51 version int, 52 mappingRules []*mappingRule, 53 rollupRules []*rollupRule, 54 tagsFilterOpts filters.TagsFilterOptions, 55 newRollupIDFn metricid.NewIDFn, 56 isRollupIDFn metricid.MatchIDFn, 57 ) *activeRuleSet { 58 uniqueCutoverTimes := make(map[int64]struct{}) 59 for _, mappingRule := range mappingRules { 60 for _, snapshot := range mappingRule.snapshots { 61 uniqueCutoverTimes[snapshot.cutoverNanos] = struct{}{} 62 } 63 } 64 for _, rollupRule := range rollupRules { 65 for _, snapshot := range rollupRule.snapshots { 66 uniqueCutoverTimes[snapshot.cutoverNanos] = struct{}{} 67 } 68 } 69 70 cutoverTimesAsc := make([]int64, 0, len(uniqueCutoverTimes)) 71 for t := range uniqueCutoverTimes { 72 cutoverTimesAsc = append(cutoverTimesAsc, t) 73 } 74 sort.Sort(int64Asc(cutoverTimesAsc)) 75 76 return &activeRuleSet{ 77 version: version, 78 mappingRules: mappingRules, 79 rollupRules: rollupRules, 80 cutoverTimesAsc: cutoverTimesAsc, 81 tagsFilterOpts: tagsFilterOpts, 82 newRollupIDFn: newRollupIDFn, 83 isRollupIDFn: isRollupIDFn, 84 } 85 } 86 87 // The forward matching logic goes like this: 88 // 89 // Imagine you have the list of rules in the ruleset lined up vertically. Each rule may have one 90 // or more snapshots, each of which represents a change to that rule (e.g., filter change, policy 91 // change, etc.). These snapshots are naturally non-overlapping in time since only one snapshot 92 // can be active at a given point in time. As a result, if we use the x axis to represent time, 93 // then for each rule, a snapshot is active for some amount of time. IOW, if you pick a time and 94 // draw a vertical line across the set of rules, the snapshots of different ruels that intersect 95 // with the vertical line are the active rule snapshots for the ruleset. 96 // 97 // Now you have a list of times you need to perform rule matching at. Each matching time 98 // corresponds to a cutover time of a rule in the ruleset, because that's when matching the metric 99 // ID against this rule may lead to a different metadata including different storage policies and 100 // new rollup IDs to be generated or existing rollup IDs to stop being generated. The final match 101 // result is a collection of such metadata sorted by time in ascending order. 102 // 103 // NB(xichen): can further consolidate consecutive staged metadata to deduplicate. 104 func (as *activeRuleSet) ForwardMatch( 105 id metricid.ID, 106 fromNanos, toNanos int64, 107 opts MatchOptions, 108 ) (MatchResult, error) { 109 currMatchRes, err := as.forwardMatchAt(id.Bytes(), fromNanos, opts) 110 if err != nil { 111 return MatchResult{}, err 112 } 113 var ( 114 forExistingID = metadata.StagedMetadatas{currMatchRes.forExistingID} 115 forNewRollupIDs = currMatchRes.forNewRollupIDs 116 nextIdx = as.nextCutoverIdx(fromNanos) 117 nextCutoverNanos = as.cutoverNanosAt(nextIdx) 118 keepOriginal = currMatchRes.keepOriginal 119 ) 120 121 for nextIdx < len(as.cutoverTimesAsc) && nextCutoverNanos < toNanos { 122 nextMatchRes, err := as.forwardMatchAt(id.Bytes(), nextCutoverNanos, opts) 123 if err != nil { 124 return MatchResult{}, err 125 } 126 forExistingID = mergeResultsForExistingID(forExistingID, nextMatchRes.forExistingID, nextCutoverNanos) 127 forNewRollupIDs = mergeResultsForNewRollupIDs(forNewRollupIDs, nextMatchRes.forNewRollupIDs, nextCutoverNanos) 128 nextIdx++ 129 nextCutoverNanos = as.cutoverNanosAt(nextIdx) 130 keepOriginal = nextMatchRes.keepOriginal 131 } 132 133 // The result expires when the beginning of the match time range reaches the first cutover time 134 // after `fromNanos`, or the end of the match time range reaches the first cutover time after 135 // `toNanos` among all active rules because the metric may then be matched against a different 136 // set of rules. 137 return NewMatchResult( 138 as.version, 139 nextCutoverNanos, 140 forExistingID, 141 forNewRollupIDs, 142 keepOriginal, 143 ), nil 144 } 145 146 func (as *activeRuleSet) ReverseMatch( 147 id metricid.ID, 148 fromNanos, toNanos int64, 149 mt metric.Type, 150 at aggregation.Type, 151 isMultiAggregationTypesAllowed bool, 152 aggTypesOpts aggregation.TypesOptions, 153 ) (MatchResult, error) { 154 var ( 155 nextIdx = as.nextCutoverIdx(fromNanos) 156 nextCutoverNanos = as.cutoverNanosAt(nextIdx) 157 forExistingID metadata.StagedMetadatas 158 isRollupID bool 159 keepOriginal bool 160 ) 161 162 // Determine whether the ID is a rollup metric ID. 163 name, tags, err := as.tagsFilterOpts.NameAndTagsFn(id.Bytes()) 164 if err == nil { 165 isRollupID = as.isRollupIDFn(name, tags) 166 } 167 168 currResult, found, err := as.reverseMappingsFor( 169 id.Bytes(), 170 name, 171 tags, 172 isRollupID, 173 fromNanos, 174 mt, 175 at, 176 isMultiAggregationTypesAllowed, 177 aggTypesOpts, 178 ) 179 if err != nil { 180 return MatchResult{}, err 181 } 182 if found { 183 forExistingID = mergeResultsForExistingID(forExistingID, currResult.metadata, fromNanos) 184 if currResult.keepOriginal { 185 keepOriginal = true 186 } 187 } 188 189 for nextIdx < len(as.cutoverTimesAsc) && nextCutoverNanos < toNanos { 190 nextResult, found, err := as.reverseMappingsFor( 191 id.Bytes(), 192 name, 193 tags, 194 isRollupID, 195 nextCutoverNanos, 196 mt, 197 at, 198 isMultiAggregationTypesAllowed, 199 aggTypesOpts, 200 ) 201 if err != nil { 202 return MatchResult{}, err 203 } 204 if found { 205 forExistingID = mergeResultsForExistingID( 206 forExistingID, 207 nextResult.metadata, 208 nextCutoverNanos, 209 ) 210 if nextResult.keepOriginal { 211 keepOriginal = true 212 } 213 } 214 215 nextIdx++ 216 nextCutoverNanos = as.cutoverNanosAt(nextIdx) 217 } 218 return NewMatchResult(as.version, nextCutoverNanos, forExistingID, nil, keepOriginal), nil 219 } 220 221 // NB(xichen): can further consolidate pipelines with the same aggregation ID 222 // and same applied pipeline but different storage policies to reduce amount of 223 // data that needed to be stored in memory and sent across the wire. 224 func (as *activeRuleSet) forwardMatchAt( 225 id []byte, 226 timeNanos int64, 227 matchOpts MatchOptions, 228 ) (forwardMatchResult, error) { 229 mappingResults, err := as.mappingsForNonRollupID(id, timeNanos, matchOpts) 230 if err != nil { 231 return forwardMatchResult{}, err 232 } 233 rollupResults, err := as.rollupResultsFor(id, timeNanos, matchOpts) 234 if err != nil { 235 return forwardMatchResult{}, err 236 } 237 forExistingID := mappingResults.forExistingID. 238 merge(rollupResults.forExistingID). 239 unique(). 240 toStagedMetadata() 241 forNewRollupIDs := make([]IDWithMetadatas, 0, len(rollupResults.forNewRollupIDs)) 242 for _, idWithMatchResult := range rollupResults.forNewRollupIDs { 243 stagedMetadata := idWithMatchResult.matchResults.unique().toStagedMetadata() 244 newIDWithMetadatas := IDWithMetadatas{ 245 ID: idWithMatchResult.id, 246 Metadatas: metadata.StagedMetadatas{stagedMetadata}, 247 } 248 forNewRollupIDs = append(forNewRollupIDs, newIDWithMetadatas) 249 } 250 sort.Sort(IDWithMetadatasByIDAsc(forNewRollupIDs)) 251 return forwardMatchResult{ 252 forExistingID: forExistingID, 253 forNewRollupIDs: forNewRollupIDs, 254 keepOriginal: rollupResults.keepOriginal, 255 }, nil 256 } 257 258 func (as *activeRuleSet) mappingsForNonRollupID( 259 id []byte, 260 timeNanos int64, 261 matchOpts MatchOptions, 262 ) (mappingResults, error) { 263 var ( 264 cutoverNanos int64 265 pipelines []metadata.PipelineMetadata 266 ) 267 for _, mappingRule := range as.mappingRules { 268 snapshot := mappingRule.activeSnapshot(timeNanos) 269 if snapshot == nil { 270 continue 271 } 272 matches, err := snapshot.filter.Matches(id, filters.TagMatchOptions{ 273 SortedTagIteratorFn: matchOpts.SortedTagIteratorFn, 274 NameAndTagsFn: matchOpts.NameAndTagsFn, 275 }) 276 if err != nil { 277 return mappingResults{}, err 278 } 279 if !matches { 280 continue 281 } 282 // Make sure the cutover time tracks the latest cutover time among all matching 283 // mapping rules to represent the correct time of rule change. 284 if cutoverNanos < snapshot.cutoverNanos { 285 cutoverNanos = snapshot.cutoverNanos 286 } 287 // If the mapping rule snapshot is a tombstoned snapshot, its cutover time is 288 // recorded to indicate a rule change, but its policies are no longer in effect. 289 if snapshot.tombstoned { 290 continue 291 } 292 pipeline := metadata.PipelineMetadata{ 293 AggregationID: snapshot.aggregationID, 294 StoragePolicies: snapshot.storagePolicies.Clone(), 295 DropPolicy: snapshot.dropPolicy, 296 Tags: snapshot.tags, 297 GraphitePrefix: snapshot.graphitePrefix, 298 } 299 pipelines = append(pipelines, pipeline) 300 } 301 302 // NB: The pipeline list should never be empty as the resulting pipelines are 303 // used to determine how the *existing* ID is aggregated and retained. If there 304 // are no rule match, the default pipeline list is used. 305 if len(pipelines) == 0 { 306 pipelines = metadata.DefaultPipelineMetadatas.Clone() 307 } 308 return mappingResults{ 309 forExistingID: ruleMatchResults{cutoverNanos: cutoverNanos, pipelines: pipelines}, 310 }, nil 311 } 312 313 func (as *activeRuleSet) LatestRollupRules(_ []byte, timeNanos int64) ([]view.RollupRule, error) { 314 out := []view.RollupRule{} 315 // Return the list of cloned rollup rule views that were active (and are still 316 // active) as of timeNanos. 317 for _, rollupRule := range as.rollupRules { 318 rule := rollupRule.activeRule(timeNanos) 319 // Skip missing or empty rules. 320 // tombstoned() returns true if the length of rule.snapshots is zero. 321 if rule == nil || rule.tombstoned() { 322 continue 323 } 324 325 view, err := rule.rollupRuleView(len(rule.snapshots) - 1) 326 if err != nil { 327 return nil, err 328 } 329 out = append(out, view) 330 } 331 return out, nil 332 } 333 334 func (as *activeRuleSet) rollupResultsFor(id []byte, timeNanos int64, matchOpts MatchOptions) (rollupResults, error) { 335 var ( 336 cutoverNanos int64 337 rollupTargets []rollupTarget 338 keepOriginal bool 339 tags [][]models.Tag 340 ) 341 342 for _, rollupRule := range as.rollupRules { 343 snapshot := rollupRule.activeSnapshot(timeNanos) 344 if snapshot == nil { 345 continue 346 } 347 match, err := snapshot.filter.Matches(id, filters.TagMatchOptions{ 348 NameAndTagsFn: matchOpts.NameAndTagsFn, 349 SortedTagIteratorFn: matchOpts.SortedTagIteratorFn, 350 }) 351 if err != nil { 352 return rollupResults{}, err 353 } 354 if !match { 355 continue 356 } 357 358 // Make sure the cutover time tracks the latest cutover time among all matching 359 // rollup rules to represent the correct time of rule change. 360 if cutoverNanos < snapshot.cutoverNanos { 361 cutoverNanos = snapshot.cutoverNanos 362 } 363 364 if snapshot.keepOriginal { 365 keepOriginal = true 366 } 367 368 // If the rollup rule snapshot is a tombstoned snapshot, its cutover time is 369 // recorded to indicate a rule change, but its rollup targets are no longer in effect. 370 if snapshot.tombstoned { 371 continue 372 } 373 374 for _, target := range snapshot.targets { 375 rollupTargets = append(rollupTargets, target.clone()) 376 tags = append(tags, snapshot.tags) 377 } 378 } 379 // NB: could log the matching error here if needed. 380 res, _ := as.toRollupResults(id, cutoverNanos, rollupTargets, keepOriginal, tags, matchOpts) 381 return res, nil 382 } 383 384 // toRollupMatchResult applies the rollup operation in each rollup pipelines contained 385 // in the rollup targets against the matching ID to determine the resulting new rollup 386 // ID. It additionally distinguishes rollup pipelines whose first operation is a rollup 387 // operation from those that aren't since the former pipelines are applied against the 388 // original metric ID and the latter are applied against new rollup IDs due to the 389 // application of the rollup operation. 390 // nolint: unparam 391 func (as *activeRuleSet) toRollupResults( 392 id []byte, 393 cutoverNanos int64, 394 targets []rollupTarget, 395 keepOriginal bool, 396 tags [][]models.Tag, 397 matchOpts MatchOptions, 398 ) (rollupResults, error) { 399 if len(targets) == 0 { 400 return rollupResults{}, nil 401 } 402 403 // If we cannot extract tags from the id, this is likely an invalid 404 // metric and we bail early. 405 _, sortedTagPairBytes, err := matchOpts.NameAndTagsFn(id) 406 if err != nil { 407 return rollupResults{}, err 408 } 409 410 var ( 411 multiErr = xerrors.NewMultiError() 412 pipelines = make([]metadata.PipelineMetadata, 0, len(targets)) 413 newRollupIDResults = make([]idWithMatchResults, 0, len(targets)) 414 tagPairs []metricid.TagPair 415 ) 416 417 for idx, target := range targets { 418 pipeline := target.Pipeline 419 // A rollup target should always have a non-empty pipeline but 420 // just being defensive here. 421 if pipeline.IsEmpty() { 422 err = fmt.Errorf("target %v has empty pipeline", target) 423 multiErr = multiErr.Add(err) 424 continue 425 } 426 var ( 427 aggregationID aggregation.ID 428 rollupID []byte 429 numSteps = pipeline.Len() 430 firstOp = pipeline.At(0) 431 toApply mpipeline.Pipeline 432 ) 433 switch firstOp.Type { 434 case mpipeline.AggregationOpType: 435 aggregationID, err = aggregation.CompressTypes(firstOp.Aggregation.Type) 436 if err != nil { 437 err = fmt.Errorf("target %v operation 0 aggregation type compression error: %v", target, err) 438 multiErr = multiErr.Add(err) 439 continue 440 } 441 toApply = pipeline.SubPipeline(1, numSteps) 442 case mpipeline.TransformationOpType: 443 aggregationID = aggregation.DefaultID 444 toApply = pipeline 445 case mpipeline.RollupOpType: 446 tagPairs = tagPairs[:0] 447 var matched bool 448 rollupID, matched, err = as.matchRollupTarget( 449 sortedTagPairBytes, 450 firstOp.Rollup, 451 tagPairs, 452 tags[idx], 453 matchRollupTargetOptions{generateRollupID: true}, 454 matchOpts) 455 if err != nil { 456 multiErr = multiErr.Add(err) 457 continue 458 } 459 if !matched { 460 // The incoming metric ID did not match the rollup target. 461 continue 462 } 463 aggregationID = firstOp.Rollup.AggregationID 464 toApply = pipeline.SubPipeline(1, numSteps) 465 default: 466 err = fmt.Errorf("target %v operation 0 has unknown type: %v", target, firstOp.Type) 467 multiErr = multiErr.Add(err) 468 continue 469 } 470 tagPairs = tagPairs[:0] 471 applied, err := as.applyIDToPipeline(sortedTagPairBytes, toApply, tagPairs, tags[idx], matchOpts) 472 if err != nil { 473 err = fmt.Errorf("failed to apply id %s to pipeline %v: %v", id, toApply, err) 474 multiErr = multiErr.Add(err) 475 continue 476 } 477 newPipeline := metadata.PipelineMetadata{ 478 AggregationID: aggregationID, 479 StoragePolicies: target.StoragePolicies, 480 Pipeline: applied, 481 ResendEnabled: target.ResendEnabled, 482 } 483 if rollupID == nil { 484 // The applied pipeline applies to the incoming ID. 485 pipelines = append(pipelines, newPipeline) 486 } else { 487 if len(tags[idx]) > 0 { 488 newPipeline.Tags = tags[idx] 489 } 490 // The applied pipeline applies to a new rollup ID. 491 matchResults := ruleMatchResults{ 492 cutoverNanos: cutoverNanos, 493 pipelines: []metadata.PipelineMetadata{newPipeline}, 494 } 495 newRollupIDResult := idWithMatchResults{id: rollupID, matchResults: matchResults} 496 newRollupIDResults = append(newRollupIDResults, newRollupIDResult) 497 } 498 } 499 500 return rollupResults{ 501 forExistingID: ruleMatchResults{cutoverNanos: cutoverNanos, pipelines: pipelines}, 502 forNewRollupIDs: newRollupIDResults, 503 keepOriginal: keepOriginal, 504 }, multiErr.FinalError() 505 } 506 507 // matchRollupTarget matches an incoming metric ID against a rollup target, 508 // returns the new rollup ID if the metric ID contains the full list of rollup 509 // tags, and nil otherwise. 510 func (as *activeRuleSet) matchRollupTarget( 511 sortedTagPairBytes []byte, 512 rollupOp mpipeline.RollupOp, 513 tagPairs []metricid.TagPair, // buffer for reuse to generate rollup ID across calls 514 tags []models.Tag, 515 targetOpts matchRollupTargetOptions, 516 matchOpts MatchOptions, 517 ) ([]byte, bool, error) { 518 if rollupOp.Type == mpipeline.ExcludeByRollupType && !targetOpts.generateRollupID { 519 // Exclude by tag always matches, if not generating rollup ID 520 // then immediately return. 521 return nil, true, nil 522 } 523 524 var ( 525 rollupTags = rollupOp.Tags 526 sortedTagIter = matchOpts.SortedTagIteratorFn(sortedTagPairBytes) 527 matchTagIdx = 0 528 nameTagName = as.tagsFilterOpts.NameTagKey 529 nameTagValue []byte 530 ) 531 532 switch rollupOp.Type { 533 case mpipeline.GroupByRollupType: 534 // Iterate through each tag, looking to match it with corresponding filter tags on the rule 535 // 536 // For include rules, every rule has to have a corresponding match. This means we return 537 // early whenever there's a missing match and increment matchRuleIdx whenever there is a match. 538 for hasMoreTags := sortedTagIter.Next(); hasMoreTags; hasMoreTags = sortedTagIter.Next() { 539 tagName, tagVal := sortedTagIter.Current() 540 // nolint:gosimple 541 isNameTag := bytes.Compare(tagName, nameTagName) == 0 542 if isNameTag { 543 nameTagValue = tagVal 544 } 545 546 // If we've matched all tags, no need to process. 547 // We don't break out of the for loop, because we may still need to find the name tag. 548 if matchTagIdx >= len(rollupTags) { 549 continue 550 } 551 552 res := bytes.Compare(tagName, rollupTags[matchTagIdx]) 553 if res == 0 { 554 // Include grouped by tag. 555 if targetOpts.generateRollupID { 556 tagPairs = append(tagPairs, metricid.TagPair{Name: tagName, Value: tagVal}) 557 } 558 matchTagIdx++ 559 continue 560 } 561 562 // If one of the target tags is not found in the ID, this is considered a non-match so return immediately. 563 if res > 0 { 564 return nil, false, nil 565 } 566 } 567 case mpipeline.ExcludeByRollupType: 568 // Iterate through each tag, looking to match it with corresponding filter tags on the rule. 569 // 570 // For exclude rules, this means merging with the tag rule list and incrementing the 571 // matchTagIdx whenever the current tag rule is lexigraphically greater than the rule tag, 572 // since we need to be careful in the case where there is no matching input tag for some rule. 573 for hasMoreTags := sortedTagIter.Next(); hasMoreTags; { 574 tagName, tagVal := sortedTagIter.Current() 575 // nolint:gosimple 576 isNameTag := bytes.Compare(tagName, nameTagName) == 0 577 if isNameTag { 578 nameTagValue = tagVal 579 580 // Don't copy name tag since we'll add that using the new rollup ID fn. 581 hasMoreTags = sortedTagIter.Next() 582 continue 583 } 584 585 if matchTagIdx >= len(rollupTags) { 586 // Have matched all the tags to exclude, just blindly copy. 587 if targetOpts.generateRollupID { 588 tagPairs = append(tagPairs, metricid.TagPair{Name: tagName, Value: tagVal}) 589 } 590 hasMoreTags = sortedTagIter.Next() 591 continue 592 } 593 594 res := bytes.Compare(tagName, rollupTags[matchTagIdx]) 595 if res > 0 { 596 // Current tag is greater than the current exclude rule, 597 // so we know the current exclude rule has no match and 598 // we should move on to the next one. 599 matchTagIdx++ 600 continue 601 } 602 603 if res != 0 { 604 // Only include tags that don't match the exclude tag 605 if targetOpts.generateRollupID { 606 tagPairs = append(tagPairs, metricid.TagPair{Name: tagName, Value: tagVal}) 607 } 608 } 609 610 hasMoreTags = sortedTagIter.Next() 611 } 612 } 613 614 if sortedTagIter.Err() != nil { 615 return nil, false, sortedTagIter.Err() 616 } 617 618 if !targetOpts.generateRollupID { 619 return nil, true, nil 620 } 621 622 for _, tag := range tags { 623 tagPairs = append(tagPairs, metricid.TagPair{ 624 Name: tag.Name, 625 Value: tag.Value, 626 }) 627 } 628 629 newName := rollupOp.NewName(nameTagValue) 630 return as.newRollupIDFn(newName, tagPairs), true, nil 631 } 632 633 func (as *activeRuleSet) applyIDToPipeline( 634 sortedTagPairBytes []byte, 635 pipeline mpipeline.Pipeline, 636 tagPairs []metricid.TagPair, // buffer for reuse across calls 637 tags []models.Tag, 638 matchOpts MatchOptions, 639 ) (applied.Pipeline, error) { 640 operations := make([]applied.OpUnion, 0, pipeline.Len()) 641 for i := 0; i < pipeline.Len(); i++ { 642 pipelineOp := pipeline.At(i) 643 var opUnion applied.OpUnion 644 switch pipelineOp.Type { 645 case mpipeline.TransformationOpType: 646 opUnion = applied.OpUnion{ 647 Type: mpipeline.TransformationOpType, 648 Transformation: pipelineOp.Transformation, 649 } 650 case mpipeline.RollupOpType: 651 rollupOp := pipelineOp.Rollup 652 var matched bool 653 rollupID, matched, err := as.matchRollupTarget( 654 sortedTagPairBytes, 655 rollupOp, 656 tagPairs, 657 tags, 658 matchRollupTargetOptions{generateRollupID: true}, 659 matchOpts) 660 if err != nil { 661 return applied.Pipeline{}, err 662 } 663 if !matched { 664 err := fmt.Errorf("existing tag pairs %s do not contain all rollup tags %s", sortedTagPairBytes, rollupOp.Tags) 665 return applied.Pipeline{}, err 666 } 667 opUnion = applied.OpUnion{ 668 Type: mpipeline.RollupOpType, 669 Rollup: applied.RollupOp{ID: rollupID, AggregationID: rollupOp.AggregationID}, 670 } 671 default: 672 return applied.Pipeline{}, fmt.Errorf("unexpected pipeline op type: %v", pipelineOp.Type) 673 } 674 operations = append(operations, opUnion) 675 } 676 return applied.NewPipeline(operations), nil 677 } 678 679 func (as *activeRuleSet) reverseMappingsFor( 680 id, name, tags []byte, 681 isRollupID bool, 682 timeNanos int64, 683 mt metric.Type, 684 at aggregation.Type, 685 isMultiAggregationTypesAllowed bool, 686 aggTypesOpts aggregation.TypesOptions, 687 ) (reverseMatchResult, bool, error) { 688 if !isRollupID { 689 return as.reverseMappingsForNonRollupID(id, timeNanos, mt, at, aggTypesOpts) 690 } 691 return as.reverseMappingsForRollupID(name, tags, timeNanos, mt, at, isMultiAggregationTypesAllowed, aggTypesOpts) 692 } 693 694 type reverseMatchResult struct { 695 metadata metadata.StagedMetadata 696 keepOriginal bool 697 } 698 699 // reverseMappingsForNonRollupID returns the staged metadata for the given non-rollup ID at 700 // the given time, and true if a non-empty list of pipelines are found, and false otherwise. 701 func (as *activeRuleSet) reverseMappingsForNonRollupID( 702 id []byte, 703 timeNanos int64, 704 mt metric.Type, 705 at aggregation.Type, 706 aggTypesOpts aggregation.TypesOptions, 707 ) (reverseMatchResult, bool, error) { 708 mapping, err := as.mappingsForNonRollupID(id, timeNanos, MatchOptions{ 709 NameAndTagsFn: as.tagsFilterOpts.NameAndTagsFn, 710 SortedTagIteratorFn: as.tagsFilterOpts.SortedTagIteratorFn, 711 }) 712 if err != nil { 713 return reverseMatchResult{}, false, err 714 } 715 mappingRes := mapping.forExistingID 716 // Always filter pipelines with aggregation types because for non rollup IDs, it is possible 717 // that none of the rules would match based on the aggregation types, in which case we fall 718 // back to the default staged metadata. 719 filteredPipelines := filteredPipelinesWithAggregationType(mappingRes.pipelines, mt, at, aggTypesOpts) 720 if len(filteredPipelines) == 0 { 721 return reverseMatchResult{ 722 metadata: metadata.DefaultStagedMetadata, 723 }, false, nil 724 } 725 726 return reverseMatchResult{ 727 metadata: metadata.StagedMetadata{ 728 CutoverNanos: mappingRes.cutoverNanos, 729 Tombstoned: false, 730 Metadata: metadata.Metadata{Pipelines: filteredPipelines}, 731 }, 732 }, true, nil 733 } 734 735 // NB(xichen): in order to determine the applicable policies for a rollup metric, we need to 736 // match the id against rollup rules to determine which rollup rules are applicable, under the 737 // assumption that no two rollup targets in the same namespace may have the same rollup metric 738 // name and the list of rollup tags. Otherwise, a rollup metric could potentially match more 739 // than one rollup rule with different policies even though only one of the matched rules was 740 // used to produce the given rollup metric id due to its tag filters, thereby causing the wrong 741 // staged policies to be returned. This also implies at any given time, at most one rollup target 742 // may match the given rollup id. 743 // Since we may have rollup pipelines with different aggregation types defined for a roll up rule, 744 // and each aggregation type would generate a new id. So when doing reverse mapping, not only do 745 // we need to match the roll up tags, we also need to check the aggregation type against 746 // each rollup pipeline to see if the aggregation type was actually contained in the pipeline. 747 func (as *activeRuleSet) reverseMappingsForRollupID( 748 name, sortedTagPairBytes []byte, 749 timeNanos int64, 750 mt metric.Type, 751 at aggregation.Type, 752 isMultiAggregationTypesAllowed bool, 753 aggTypesOpts aggregation.TypesOptions, 754 ) (reverseMatchResult, bool, error) { 755 for _, rollupRule := range as.rollupRules { 756 snapshot := rollupRule.activeSnapshot(timeNanos) 757 if snapshot == nil || snapshot.tombstoned { 758 continue 759 } 760 761 for _, target := range snapshot.targets { 762 for i := 0; i < target.Pipeline.Len(); i++ { 763 pipelineOp := target.Pipeline.At(i) 764 if pipelineOp.Type != mpipeline.RollupOpType { 765 continue 766 } 767 rollupOp := pipelineOp.Rollup 768 if !bytes.Equal(rollupOp.NewName(name), name) { 769 continue 770 } 771 _, matched, err := as.matchRollupTarget( 772 sortedTagPairBytes, 773 rollupOp, 774 nil, 775 nil, 776 matchRollupTargetOptions{generateRollupID: false}, 777 MatchOptions{ 778 NameAndTagsFn: as.tagsFilterOpts.NameAndTagsFn, 779 SortedTagIteratorFn: as.tagsFilterOpts.SortedTagIteratorFn, 780 }, 781 ) 782 if err != nil { 783 return reverseMatchResult{}, false, err 784 } 785 if !matched { 786 continue 787 } 788 // NB: the list of pipeline steps is not important and thus not computed and returned. 789 pipeline := metadata.PipelineMetadata{ 790 AggregationID: rollupOp.AggregationID, 791 StoragePolicies: target.StoragePolicies.Clone(), 792 } 793 // Only further filter the pipelines with aggregation types if the given metric type 794 // supports multiple aggregation types. This is because if a metric type only supports 795 // a single aggregation type, this is the only pipline that could possibly produce this 796 // rollup metric and as such is chosen. The aggregation type passed in is not used because 797 // it maybe not be accurate because it may not be possible to infer the actual aggregation 798 // type only from the metric ID. 799 filteredPipelines := []metadata.PipelineMetadata{pipeline} 800 if isMultiAggregationTypesAllowed { 801 filteredPipelines = filteredPipelinesWithAggregationType(filteredPipelines, mt, at, aggTypesOpts) 802 } 803 if len(filteredPipelines) == 0 { 804 return reverseMatchResult{ 805 metadata: metadata.DefaultStagedMetadata, 806 }, false, nil 807 } 808 809 return reverseMatchResult{ 810 metadata: metadata.StagedMetadata{ 811 CutoverNanos: snapshot.cutoverNanos, 812 Tombstoned: false, 813 Metadata: metadata.Metadata{Pipelines: filteredPipelines}, 814 }, 815 keepOriginal: snapshot.keepOriginal, 816 }, true, nil 817 } 818 } 819 } 820 return reverseMatchResult{ 821 metadata: metadata.DefaultStagedMetadata, 822 }, false, nil 823 } 824 825 // nextCutoverIdx returns the next snapshot index whose cutover time is after t. 826 // NB(xichen): not using sort.Search to avoid a lambda capture. 827 func (as *activeRuleSet) nextCutoverIdx(t int64) int { 828 i, j := 0, len(as.cutoverTimesAsc) 829 for i < j { 830 h := i + (j-i)/2 831 if as.cutoverTimesAsc[h] <= t { 832 i = h + 1 833 } else { 834 j = h 835 } 836 } 837 return i 838 } 839 840 // cutoverNanosAt returns the cutover time at given index. 841 func (as *activeRuleSet) cutoverNanosAt(idx int) int64 { 842 if idx < len(as.cutoverTimesAsc) { 843 return as.cutoverTimesAsc[idx] 844 } 845 return timeNanosMax 846 } 847 848 // filterByAggregationType takes a list of pipelines as input and returns those 849 // containing the given aggregation type. 850 func filteredPipelinesWithAggregationType( 851 pipelines []metadata.PipelineMetadata, 852 mt metric.Type, 853 at aggregation.Type, 854 opts aggregation.TypesOptions, 855 ) []metadata.PipelineMetadata { 856 var cur int 857 for i := 0; i < len(pipelines); i++ { 858 var containsAggType bool 859 if aggID := pipelines[i].AggregationID; aggID.IsDefault() { 860 containsAggType = opts.IsContainedInDefaultAggregationTypes(at, mt) 861 } else { 862 containsAggType = aggID.Contains(at) 863 } 864 if !containsAggType { 865 continue 866 } 867 if cur != i { 868 pipelines[cur] = pipelines[i] 869 } 870 cur++ 871 } 872 return pipelines[:cur] 873 } 874 875 // mergeResultsForExistingID merges the next staged metadata into the current list of staged 876 // metadatas while ensuring the cutover times of the staged metadatas are non-decreasing. This 877 // is needed because the cutover times of staged metadata results produced by mapping rule matching 878 // may not always be in ascending order. For example, if at time T0 a metric matches against a 879 // mapping rule, and the filter of such rule changed at T1 such that the metric no longer matches 880 // the rule, this would indicate the staged metadata at T0 would have a cutover time of T0, 881 // whereas the staged metadata at T1 would have a cutover time of 0 (due to no rule match), 882 // in which case we need to set the cutover time of the staged metadata at T1 to T1 to ensure 883 // the mononicity of cutover times. 884 func mergeResultsForExistingID( 885 currMetadatas metadata.StagedMetadatas, 886 nextMetadata metadata.StagedMetadata, 887 nextCutoverNanos int64, 888 ) metadata.StagedMetadatas { 889 if len(currMetadatas) == 0 { 890 return metadata.StagedMetadatas{nextMetadata} 891 } 892 currCutoverNanos := currMetadatas[len(currMetadatas)-1].CutoverNanos 893 if currCutoverNanos > nextMetadata.CutoverNanos { 894 nextMetadata.CutoverNanos = nextCutoverNanos 895 } 896 currMetadatas = append(currMetadatas, nextMetadata) 897 return currMetadatas 898 } 899 900 // mergeResultsForNewRollupIDs merges the current list of staged metadatas for new rollup IDs 901 // with the list of staged metadatas for new rollup IDs at the next rule cutover time, assuming 902 // that both the current metadatas list and the next metadatas list are sorted by rollup IDs 903 // in ascending order. 904 // NB: each item in the `nextResults` array has a single staged metadata in the `metadatas` array 905 // as the staged metadata for the associated rollup ID at the next cutover time. 906 func mergeResultsForNewRollupIDs( 907 currResults []IDWithMetadatas, 908 nextResults []IDWithMetadatas, 909 nextCutoverNanos int64, 910 ) []IDWithMetadatas { 911 var ( 912 currLen, nextLen = len(currResults), len(nextResults) 913 currIdx, nextIdx int 914 ) 915 for currIdx < currLen || nextIdx < nextLen { 916 var compareResult int 917 if currIdx >= currLen { 918 compareResult = 1 919 } else if nextIdx >= nextLen { 920 compareResult = -1 921 } else { 922 compareResult = bytes.Compare(currResults[currIdx].ID, nextResults[nextIdx].ID) 923 } 924 925 // If the current result and the next result have the same ID, we append the next metadata 926 // to the end of the metadata list. 927 if compareResult == 0 { 928 currResults[currIdx].Metadatas = append(currResults[currIdx].Metadatas, nextResults[nextIdx].Metadatas[0]) 929 currIdx++ 930 nextIdx++ 931 continue 932 } 933 934 // If the current ID is smaller, it means the current rollup ID is tombstoned at the next 935 // cutover time. 936 if compareResult < 0 { 937 tombstonedMetadata := metadata.StagedMetadata{CutoverNanos: nextCutoverNanos, Tombstoned: true} 938 currResults[currIdx].Metadatas = append(currResults[currIdx].Metadatas, tombstonedMetadata) 939 currIdx++ 940 continue 941 } 942 943 // Otherwise the current ID is larger, meaning a new ID is added at the next cutover time. 944 currResults = append(currResults, nextResults[nextIdx]) 945 nextIdx++ 946 } 947 sort.Sort(IDWithMetadatasByIDAsc(currResults)) 948 return currResults 949 } 950 951 type int64Asc []int64 952 953 func (a int64Asc) Len() int { return len(a) } 954 func (a int64Asc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 955 func (a int64Asc) Less(i, j int) bool { return a[i] < a[j] } 956 957 type matchRollupTargetOptions struct { 958 generateRollupID bool 959 } 960 961 type ruleMatchResults struct { 962 cutoverNanos int64 963 pipelines []metadata.PipelineMetadata 964 } 965 966 // merge merges in another rule match results in place. 967 func (res *ruleMatchResults) merge(other ruleMatchResults) *ruleMatchResults { 968 if res.cutoverNanos < other.cutoverNanos { 969 res.cutoverNanos = other.cutoverNanos 970 } 971 res.pipelines = append(res.pipelines, other.pipelines...) 972 return res 973 } 974 975 // unique de-duplicates the pipelines. 976 func (res *ruleMatchResults) unique() *ruleMatchResults { 977 if len(res.pipelines) == 0 { 978 return res 979 } 980 981 // Otherwise merge as per usual 982 curr := 0 983 for i := 1; i < len(res.pipelines); i++ { 984 foundDup := false 985 for j := 0; j <= curr; j++ { 986 if res.pipelines[j].Equal(res.pipelines[i]) { 987 foundDup = true 988 break 989 } 990 } 991 if foundDup { 992 continue 993 } 994 curr++ 995 res.pipelines[curr] = res.pipelines[i] 996 } 997 for i := curr + 1; i < len(res.pipelines); i++ { 998 res.pipelines[i] = metadata.PipelineMetadata{} 999 } 1000 res.pipelines = res.pipelines[:curr+1] 1001 return res 1002 } 1003 1004 // toStagedMetadata converts the match results to a staged metadata. 1005 func (res *ruleMatchResults) toStagedMetadata() metadata.StagedMetadata { 1006 return metadata.StagedMetadata{ 1007 CutoverNanos: res.cutoverNanos, 1008 Tombstoned: false, 1009 Metadata: metadata.Metadata{Pipelines: res.resolvedPipelines()}, 1010 } 1011 } 1012 1013 func (res *ruleMatchResults) resolvedPipelines() []metadata.PipelineMetadata { 1014 if len(res.pipelines) > 0 { 1015 return res.pipelines 1016 } 1017 return metadata.DefaultPipelineMetadatas 1018 } 1019 1020 type idWithMatchResults struct { 1021 id []byte 1022 matchResults ruleMatchResults 1023 } 1024 1025 type mappingResults struct { 1026 // This represent the match result that should be applied against the 1027 // incoming metric ID the mapping rules were matched against. 1028 forExistingID ruleMatchResults 1029 } 1030 1031 type rollupResults struct { 1032 // This represent the match result that should be applied against the 1033 // incoming metric ID the rollup rules were matched against. This usually contains 1034 // the match result produced by rollup rules containing rollup pipelines whose first 1035 // pipeline operation is not a rollup operation. 1036 forExistingID ruleMatchResults 1037 1038 // This represents the match result that should be applied against new rollup 1039 // IDs generated during the rule matching process. This usually contains 1040 // the match result produced by rollup rules containing rollup pipelines whose first 1041 // pipeline operation is a rollup operation. 1042 forNewRollupIDs []idWithMatchResults 1043 1044 // This represents whether or not the original (source) metric for the 1045 // matched rollup rule should be kept. If true, both metrics are written; 1046 // if false, only the new generated rollup metric is written. 1047 keepOriginal bool 1048 } 1049 1050 type forwardMatchResult struct { 1051 forExistingID metadata.StagedMetadata 1052 forNewRollupIDs []IDWithMetadatas 1053 keepOriginal bool 1054 }