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  )