github.com/m3db/m3@v1.5.0/src/metrics/rules/validator/validator.go (about) 1 // Copyright (c) 2017 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 validator 22 23 import ( 24 "errors" 25 "fmt" 26 27 "github.com/m3db/m3/src/metrics/aggregation" 28 merrors "github.com/m3db/m3/src/metrics/errors" 29 "github.com/m3db/m3/src/metrics/filters" 30 "github.com/m3db/m3/src/metrics/metric" 31 mpipeline "github.com/m3db/m3/src/metrics/pipeline" 32 "github.com/m3db/m3/src/metrics/policy" 33 "github.com/m3db/m3/src/metrics/rules" 34 "github.com/m3db/m3/src/metrics/rules/validator/namespace" 35 "github.com/m3db/m3/src/metrics/rules/view" 36 ) 37 38 var ( 39 errNoStoragePolicies = errors.New("no storage policies") 40 errEmptyRollupMetricName = errors.New("empty rollup metric name") 41 errEmptyPipeline = errors.New("empty pipeline") 42 errMoreThanOneAggregationOpInPipeline = errors.New("more than one aggregation operation in pipeline") 43 errAggregationOpNotFirstInPipeline = errors.New("aggregation operation is not the first operation in pipeline") 44 errNoRollupOpInPipeline = errors.New("no rollup operation in pipeline") 45 ) 46 47 type validator struct { 48 opts Options 49 nsValidator namespace.Validator 50 } 51 52 // NewValidator creates a new validator. 53 func NewValidator(opts Options) rules.Validator { 54 return &validator{ 55 opts: opts, 56 nsValidator: opts.NamespaceValidator(), 57 } 58 } 59 60 func (v *validator) Validate(rs rules.RuleSet) error { 61 // Only the latest (a.k.a. the first) view needs to be validated 62 // because that is the view that may be invalid due to latest update. 63 latest, err := rs.Latest() 64 if err != nil { 65 return v.wrapError(fmt.Errorf("could not get the latest ruleset snapshot: %v", err)) 66 } 67 return v.ValidateSnapshot(latest) 68 } 69 70 func (v *validator) ValidateSnapshot(snapshot view.RuleSet) error { 71 if err := v.validateSnapshot(snapshot); err != nil { 72 return v.wrapError(err) 73 } 74 return nil 75 } 76 77 func (v *validator) Close() { 78 v.nsValidator.Close() 79 } 80 81 func (v *validator) validateSnapshot(snapshot view.RuleSet) error { 82 if err := v.validateNamespace(snapshot.Namespace); err != nil { 83 return err 84 } 85 if err := v.validateMappingRules(snapshot.MappingRules); err != nil { 86 return err 87 } 88 return v.validateRollupRules(snapshot.RollupRules) 89 } 90 91 func (v *validator) validateNamespace(ns string) error { 92 return v.nsValidator.Validate(ns) 93 } 94 95 func (v *validator) validateMappingRules(mrv []view.MappingRule) error { 96 namesSeen := make(map[string]struct{}, len(mrv)) 97 for _, rule := range mrv { 98 if rule.Tombstoned { 99 continue 100 } 101 // Validate that no rules with the same name exist. 102 if _, exists := namesSeen[rule.Name]; exists { 103 return merrors.NewInvalidInputError(fmt.Sprintf("mapping rule '%s' already exists", rule.Name)) 104 } 105 namesSeen[rule.Name] = struct{}{} 106 107 // Validate that the filter is valid. 108 filterValues, err := v.validateFilter(rule.Filter) 109 if err != nil { 110 return fmt.Errorf("mapping rule '%s' has invalid filter %s: %v", rule.Name, rule.Filter, err) 111 } 112 113 // Validate the metric types. 114 types, err := v.opts.MetricTypesFn()(filterValues) 115 if err != nil { 116 return fmt.Errorf("mapping rule '%s' cannot infer metric types from filter %v: %v", rule.Name, rule.Filter, err) 117 } 118 if len(types) == 0 { 119 return fmt.Errorf("mapping rule '%s' does not match any allowed metric types, filter=%s", rule.Name, rule.Filter) 120 } 121 122 // Validate the aggregation ID. 123 if err := v.validateAggregationID(rule.AggregationID, firstLevelAggregationType, types); err != nil { 124 return fmt.Errorf("mapping rule '%s' has invalid aggregation ID %v: %v", rule.Name, rule.AggregationID, err) 125 } 126 127 // Validate the drop policy is valid. 128 if !rule.DropPolicy.IsValid() { 129 return fmt.Errorf("mapping rule '%s' has an invalid drop policy: value=%d, string=%s, valid_values=%v", 130 rule.Name, int(rule.DropPolicy), rule.DropPolicy.String(), policy.ValidDropPolicies()) 131 } 132 133 // Validate the storage policies if drop policy not active, otherwise ensure none. 134 if rule.DropPolicy.IsDefault() { 135 // Drop policy not set, validate that the storage policies are valid. 136 if err := v.validateStoragePolicies(rule.StoragePolicies, types); err != nil { 137 return fmt.Errorf("mapping rule '%s' has invalid storage policies in %v: %v", rule.Name, rule.StoragePolicies, err) 138 } 139 } else { 140 // Drop policy is set, ensure default aggregation ID and no storage policies set. 141 if !rule.AggregationID.IsDefault() { 142 return fmt.Errorf("mapping rule '%s' has a drop policy error: must use default aggregation ID", rule.Name) 143 } 144 if len(rule.StoragePolicies) != 0 { 145 return fmt.Errorf("mapping rule '%s' has a drop policy error: cannot specify storage policies", rule.Name) 146 } 147 } 148 } 149 return nil 150 } 151 152 func (v *validator) validateRollupRules(rrv []view.RollupRule) error { 153 var ( 154 namesSeen = make(map[string]struct{}, len(rrv)) 155 pipelines = make([]mpipeline.Pipeline, 0, len(rrv)) 156 ) 157 for _, rule := range rrv { 158 if rule.Tombstoned { 159 continue 160 } 161 // Validate that no rules with the same name exist. 162 if _, exists := namesSeen[rule.Name]; exists { 163 return merrors.NewInvalidInputError(fmt.Sprintf("rollup rule '%s' already exists", rule.Name)) 164 } 165 namesSeen[rule.Name] = struct{}{} 166 167 // Validate that the filter is valid. 168 filterValues, err := v.validateFilter(rule.Filter) 169 if err != nil { 170 return fmt.Errorf("rollup rule '%s' has invalid filter %s: %v", rule.Name, rule.Filter, err) 171 } 172 173 // Validate the metric types. 174 types, err := v.opts.MetricTypesFn()(filterValues) 175 if err != nil { 176 return fmt.Errorf("rollup rule '%s' cannot infer metric types from filter %v: %v", rule.Name, rule.Filter, err) 177 } 178 if len(types) == 0 { 179 return fmt.Errorf("rollup rule '%s' does not match any allowed metric types, filter=%s", rule.Name, rule.Filter) 180 } 181 182 for _, target := range rule.Targets { 183 // Validate the pipeline is valid. 184 if err := v.validatePipeline(target.Pipeline, types); err != nil { 185 return fmt.Errorf("rollup rule '%s' has invalid pipeline '%v': %v", rule.Name, target.Pipeline, err) 186 } 187 188 // Validate that the storage policies are valid. 189 if err := v.validateStoragePolicies(target.StoragePolicies, types); err != nil { 190 return fmt.Errorf("rollup rule '%s' has invalid storage policies in %v: %v", rule.Name, target.StoragePolicies, err) 191 } 192 pipelines = append(pipelines, target.Pipeline) 193 } 194 } 195 196 return validateNoDuplicateRollupIDIn(pipelines) 197 } 198 199 func (v *validator) validateFilter(f string) (filters.TagFilterValueMap, error) { 200 filterValues, err := filters.ValidateTagsFilter(f) 201 if err != nil { 202 return nil, err 203 } 204 for tag := range filterValues { 205 // Validating the filter tag name does not contain invalid chars. 206 if err := v.opts.CheckInvalidCharactersForTagName(tag); err != nil { 207 return nil, fmt.Errorf("tag name '%s' contains invalid character, err: %v", tag, err) 208 } 209 if err := v.opts.CheckFilterTagNameValid(tag); err != nil { 210 return nil, err 211 } 212 } 213 return filterValues, nil 214 } 215 216 func (v *validator) validateAggregationID( 217 aggregationID aggregation.ID, 218 aggregationType aggregationType, 219 types []metric.Type, 220 ) error { 221 // Default aggregation types are always allowed. 222 if aggregationID.IsDefault() { 223 return nil 224 } 225 aggTypes, err := aggregationID.Types() 226 if err != nil { 227 return err 228 } 229 if len(aggTypes) > 1 { 230 for _, t := range types { 231 if !v.opts.IsMultiAggregationTypesEnabledFor(t) { 232 return fmt.Errorf("metric type %v does not support multiple aggregation types %v", t, aggTypes) 233 } 234 } 235 } 236 isAllowedAggregationTypeForFn := v.opts.IsAllowedFirstLevelAggregationTypeFor 237 if aggregationType == nonFirstLevelAggregationType { 238 isAllowedAggregationTypeForFn = v.opts.IsAllowedNonFirstLevelAggregationTypeFor 239 } 240 for _, t := range types { 241 for _, aggType := range aggTypes { 242 if !isAllowedAggregationTypeForFn(t, aggType) { 243 return fmt.Errorf("aggregation type %v is not allowed for metric type %v", aggType, t) 244 } 245 } 246 } 247 return nil 248 } 249 250 func (v *validator) validateStoragePolicies( 251 storagePolicies policy.StoragePolicies, 252 types []metric.Type, 253 ) error { 254 // Validating that at least one storage policy is provided. 255 if len(storagePolicies) == 0 { 256 return errNoStoragePolicies 257 } 258 259 // Validating that no duplicate storage policies exist. 260 seen := make(map[policy.StoragePolicy]struct{}, len(storagePolicies)) 261 for _, sp := range storagePolicies { 262 if _, exists := seen[sp]; exists { 263 return fmt.Errorf("duplicate storage policy '%s'", sp.String()) 264 } 265 seen[sp] = struct{}{} 266 } 267 268 // Validating that provided storage policies are allowed for the specified metric type. 269 for _, t := range types { 270 for _, sp := range storagePolicies { 271 if !v.opts.IsAllowedStoragePolicyFor(t, sp) { 272 return fmt.Errorf("storage policy '%s' is not allowed for metric type %v", sp.String(), t) 273 } 274 } 275 } 276 return nil 277 } 278 279 // validatePipeline validates the rollup pipeline as follows: 280 // * The pipeline must contain at least one operation. 281 // * The pipeline can contain at most one aggregation operation, and if there is one, 282 // it must be the first operation. 283 // * The pipeline can contain arbitrary number of transformation operations. However, 284 // the transformation derivative order computed from the list of transformations must 285 // be no more than the maximum transformation derivative order that is supported. 286 // * The pipeline must contain at least one rollup operation and at most `n` rollup operations, 287 // where `n` is the maximum supported number of rollup levels. 288 func (v *validator) validatePipeline(pipeline mpipeline.Pipeline, types []metric.Type) error { 289 if pipeline.IsEmpty() { 290 return errEmptyPipeline 291 } 292 var ( 293 numAggregationOps int 294 transformationDerivativeOrder int 295 numRollupOps int 296 previousRollupTags map[string]struct{} 297 numPipelineOps = pipeline.Len() 298 ) 299 for i := 0; i < numPipelineOps; i++ { 300 pipelineOp := pipeline.At(i) 301 switch pipelineOp.Type { 302 case mpipeline.AggregationOpType: 303 numAggregationOps++ 304 if numAggregationOps > 1 { 305 return errMoreThanOneAggregationOpInPipeline 306 } 307 if i != 0 { 308 return errAggregationOpNotFirstInPipeline 309 } 310 if err := v.validateAggregationOp(pipelineOp.Aggregation, types); err != nil { 311 return fmt.Errorf("invalid aggregation operation at index %d: %v", i, err) 312 } 313 case mpipeline.TransformationOpType: 314 transformOp := pipelineOp.Transformation 315 if transformOp.Type.IsBinaryTransform() { 316 transformationDerivativeOrder++ 317 if transformationDerivativeOrder > v.opts.MaxTransformationDerivativeOrder() { 318 return fmt.Errorf("transformation derivative order is %d higher than supported %d", transformationDerivativeOrder, v.opts.MaxTransformationDerivativeOrder()) 319 } 320 } 321 if err := validateTransformationOp(transformOp); err != nil { 322 return fmt.Errorf("invalid transformation operation at index %d: %v", i, err) 323 } 324 case mpipeline.RollupOpType: 325 // We only care about the derivative order of transformation operations in between 326 // two consecutive rollup operations and as such we reset the derivative order when 327 // encountering a rollup operation. 328 transformationDerivativeOrder = 0 329 numRollupOps++ 330 if numRollupOps > v.opts.MaxRollupLevels() { 331 return fmt.Errorf("number of rollup levels is %d higher than supported %d", numRollupOps, v.opts.MaxRollupLevels()) 332 } 333 if err := v.validateRollupOp(pipelineOp.Rollup, i, types, previousRollupTags); err != nil { 334 return fmt.Errorf("invalid rollup operation at index %d: %v", i, err) 335 } 336 previousRollupTags = make(map[string]struct{}, len(pipelineOp.Rollup.Tags)) 337 for _, tag := range pipelineOp.Rollup.Tags { 338 previousRollupTags[string(tag)] = struct{}{} 339 } 340 default: 341 return fmt.Errorf("operation at index %d has invalid type: %v", i, pipelineOp.Type) 342 } 343 } 344 if numRollupOps == 0 { 345 return errNoRollupOpInPipeline 346 } 347 return nil 348 } 349 350 func (v *validator) validateAggregationOp( 351 aggregationOp mpipeline.AggregationOp, 352 types []metric.Type, 353 ) error { 354 aggregationID, err := aggregation.CompressTypes(aggregationOp.Type) 355 if err != nil { 356 return err 357 } 358 return v.validateAggregationID(aggregationID, firstLevelAggregationType, types) 359 } 360 361 func validateTransformationOp(transformationOp mpipeline.TransformationOp) error { 362 if !transformationOp.Type.IsValid() { 363 return fmt.Errorf("invalid transformation type: %v", transformationOp.Type) 364 } 365 return nil 366 } 367 368 func (v *validator) validateRollupOp( 369 rollupOp mpipeline.RollupOp, 370 opIdxInPipeline int, 371 types []metric.Type, 372 previousRollupTags map[string]struct{}, 373 ) error { 374 newName := rollupOp.NewName([]byte("")) 375 // Validate that the rollup metric name is valid. 376 if err := v.validateRollupMetricName(newName); err != nil { 377 return fmt.Errorf("invalid rollup metric name '%s': %w", newName, err) 378 } 379 380 // Validate that the rollup tags are valid. 381 if err := v.validateRollupTags(rollupOp.Tags, previousRollupTags); err != nil { 382 return fmt.Errorf("invalid rollup tags %v: %w", rollupOp.Tags, err) 383 } 384 385 // Validate that the aggregation ID is valid. 386 aggType := firstLevelAggregationType 387 if opIdxInPipeline > 0 { 388 aggType = nonFirstLevelAggregationType 389 } 390 if err := v.validateAggregationID(rollupOp.AggregationID, aggType, types); err != nil { 391 return fmt.Errorf("invalid aggregation ID %v: %w", rollupOp.AggregationID, err) 392 } 393 394 return nil 395 } 396 397 func (v *validator) validateRollupMetricName(metricName []byte) error { 398 // Validate that rollup metric name is not empty. 399 if len(metricName) == 0 { 400 return errEmptyRollupMetricName 401 } 402 403 // Validate that rollup metric name has valid characters. 404 return v.opts.CheckInvalidCharactersForMetricName(string(metricName)) 405 } 406 407 func (v *validator) validateRollupTags( 408 tags [][]byte, 409 previousRollupTags map[string]struct{}, 410 ) error { 411 // Validating that all tag names have valid characters. 412 for _, tag := range tags { 413 if err := v.opts.CheckInvalidCharactersForTagName(string(tag)); err != nil { 414 return fmt.Errorf("invalid rollup tag '%s': %v", tag, err) 415 } 416 } 417 418 // Validating that there are no duplicate rollup tags. 419 rollupTags := make(map[string]struct{}, len(tags)) 420 for _, tag := range tags { 421 tagStr := string(tag) 422 if _, exists := rollupTags[tagStr]; exists { 423 return fmt.Errorf("duplicate rollup tag: '%s'", tagStr) 424 } 425 rollupTags[tagStr] = struct{}{} 426 } 427 428 // Validate that the set of rollup tags are a strict subset of those in 429 // previous rollup operations. 430 // NB: `previousRollupTags` is nil for the first rollup operation. 431 if previousRollupTags != nil { 432 var numSeenTags int 433 for _, tag := range tags { 434 if _, exists := previousRollupTags[string(tag)]; !exists { 435 return fmt.Errorf("tag %s not found in previous rollup operations", tag) 436 } 437 numSeenTags++ 438 } 439 if numSeenTags == len(previousRollupTags) { 440 return fmt.Errorf("same set of %d rollup tags in consecutive rollup operations", numSeenTags) 441 } 442 } 443 444 // Validating the list of rollup tags in the rule contain all required tags. 445 requiredTags := v.opts.RequiredRollupTags() 446 if len(requiredTags) == 0 { 447 return nil 448 } 449 for _, requiredTag := range requiredTags { 450 if _, exists := rollupTags[requiredTag]; !exists { 451 return fmt.Errorf("missing required rollup tag: '%s'", requiredTag) 452 } 453 } 454 455 return nil 456 } 457 458 func validateNoDuplicateRollupIDIn(pipelines []mpipeline.Pipeline) error { 459 rollupOps := make([]mpipeline.RollupOp, 0, len(pipelines)) 460 for _, pipeline := range pipelines { 461 numOps := pipeline.Len() 462 for i := 0; i < numOps; i++ { 463 pipelineOp := pipeline.At(i) 464 if pipelineOp.Type != mpipeline.RollupOpType { 465 continue 466 } 467 rollupOp := pipelineOp.Rollup 468 for _, existing := range rollupOps { 469 if rollupOp.SameTransform(existing) { 470 return merrors.NewInvalidInputError(fmt.Sprintf( 471 "more than one rollup operations with name '%s' and tags '%s' exist", 472 rollupOp.NewName([]byte("")), 473 rollupOp.Tags, 474 )) 475 } 476 } 477 rollupOps = append(rollupOps, rollupOp) 478 } 479 } 480 return nil 481 } 482 483 func (v *validator) wrapError(err error) error { 484 if err == nil { 485 return nil 486 } 487 switch err.(type) { 488 // Do not wrap error for these error types so caller can take actions 489 // based on the correct error type. 490 case merrors.InvalidInputError, merrors.ValidationError: 491 return err 492 default: 493 return merrors.NewValidationError(err.Error()) 494 } 495 } 496 497 type aggregationType int 498 499 const ( 500 // First-level aggregation refers to the aggregation operation performed as the first 501 // step of metrics processing, such as the aggregations specified by a mapping rule, 502 // or those specified by the first operation in a rollup pipeline. 503 firstLevelAggregationType aggregationType = iota 504 505 // Non-first-level aggregation refers to the aggregation operation performed as the 506 // second step or later step of a rollup pipeline. 507 nonFirstLevelAggregationType 508 )