github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/util/validation/validate.go (about)

     1  package validation
     2  
     3  import (
     4  	"net/http"
     5  	"strings"
     6  	"time"
     7  	"unicode/utf8"
     8  
     9  	"github.com/go-kit/log"
    10  	"github.com/go-kit/log/level"
    11  	"github.com/prometheus/client_golang/prometheus"
    12  	"github.com/prometheus/common/model"
    13  	"github.com/weaveworks/common/httpgrpc"
    14  
    15  	"github.com/cortexproject/cortex/pkg/cortexpb"
    16  	"github.com/cortexproject/cortex/pkg/util"
    17  	"github.com/cortexproject/cortex/pkg/util/extract"
    18  )
    19  
    20  const (
    21  	discardReasonLabel = "reason"
    22  
    23  	errMetadataMissingMetricName = "metadata missing metric name"
    24  	errMetadataTooLong           = "metadata '%s' value too long: %.200q metric %.200q"
    25  
    26  	typeMetricName = "METRIC_NAME"
    27  	typeHelp       = "HELP"
    28  	typeUnit       = "UNIT"
    29  
    30  	metricNameTooLong = "metric_name_too_long"
    31  	helpTooLong       = "help_too_long"
    32  	unitTooLong       = "unit_too_long"
    33  
    34  	// ErrQueryTooLong is used in chunk store, querier and query frontend.
    35  	ErrQueryTooLong = "the query time range exceeds the limit (query length: %s, limit: %s)"
    36  
    37  	missingMetricName       = "missing_metric_name"
    38  	invalidMetricName       = "metric_name_invalid"
    39  	greaterThanMaxSampleAge = "greater_than_max_sample_age"
    40  	maxLabelNamesPerSeries  = "max_label_names_per_series"
    41  	tooFarInFuture          = "too_far_in_future"
    42  	invalidLabel            = "label_invalid"
    43  	labelNameTooLong        = "label_name_too_long"
    44  	duplicateLabelNames     = "duplicate_label_names"
    45  	labelsNotSorted         = "labels_not_sorted"
    46  	labelValueTooLong       = "label_value_too_long"
    47  
    48  	// Exemplar-specific validation reasons
    49  	exemplarLabelsMissing    = "exemplar_labels_missing"
    50  	exemplarLabelsTooLong    = "exemplar_labels_too_long"
    51  	exemplarTimestampInvalid = "exemplar_timestamp_invalid"
    52  
    53  	// RateLimited is one of the values for the reason to discard samples.
    54  	// Declared here to avoid duplication in ingester and distributor.
    55  	RateLimited = "rate_limited"
    56  
    57  	// Too many HA clusters is one of the reasons for discarding samples.
    58  	TooManyHAClusters = "too_many_ha_clusters"
    59  
    60  	// The combined length of the label names and values of an Exemplar's LabelSet MUST NOT exceed 128 UTF-8 characters
    61  	// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#exemplars
    62  	ExemplarMaxLabelSetLength = 128
    63  )
    64  
    65  // DiscardedSamples is a metric of the number of discarded samples, by reason.
    66  var DiscardedSamples = prometheus.NewCounterVec(
    67  	prometheus.CounterOpts{
    68  		Name: "cortex_discarded_samples_total",
    69  		Help: "The total number of samples that were discarded.",
    70  	},
    71  	[]string{discardReasonLabel, "user"},
    72  )
    73  
    74  // DiscardedExemplars is a metric of the number of discarded exemplars, by reason.
    75  var DiscardedExemplars = prometheus.NewCounterVec(
    76  	prometheus.CounterOpts{
    77  		Name: "cortex_discarded_exemplars_total",
    78  		Help: "The total number of exemplars that were discarded.",
    79  	},
    80  	[]string{discardReasonLabel, "user"},
    81  )
    82  
    83  // DiscardedMetadata is a metric of the number of discarded metadata, by reason.
    84  var DiscardedMetadata = prometheus.NewCounterVec(
    85  	prometheus.CounterOpts{
    86  		Name: "cortex_discarded_metadata_total",
    87  		Help: "The total number of metadata that were discarded.",
    88  	},
    89  	[]string{discardReasonLabel, "user"},
    90  )
    91  
    92  func init() {
    93  	prometheus.MustRegister(DiscardedSamples)
    94  	prometheus.MustRegister(DiscardedExemplars)
    95  	prometheus.MustRegister(DiscardedMetadata)
    96  }
    97  
    98  // SampleValidationConfig helps with getting required config to validate sample.
    99  type SampleValidationConfig interface {
   100  	RejectOldSamples(userID string) bool
   101  	RejectOldSamplesMaxAge(userID string) time.Duration
   102  	CreationGracePeriod(userID string) time.Duration
   103  }
   104  
   105  // ValidateSample returns an err if the sample is invalid.
   106  // The returned error may retain the provided series labels.
   107  func ValidateSample(cfg SampleValidationConfig, userID string, ls []cortexpb.LabelAdapter, s cortexpb.Sample) ValidationError {
   108  	unsafeMetricName, _ := extract.UnsafeMetricNameFromLabelAdapters(ls)
   109  
   110  	if cfg.RejectOldSamples(userID) && model.Time(s.TimestampMs) < model.Now().Add(-cfg.RejectOldSamplesMaxAge(userID)) {
   111  		DiscardedSamples.WithLabelValues(greaterThanMaxSampleAge, userID).Inc()
   112  		return newSampleTimestampTooOldError(unsafeMetricName, s.TimestampMs)
   113  	}
   114  
   115  	if model.Time(s.TimestampMs) > model.Now().Add(cfg.CreationGracePeriod(userID)) {
   116  		DiscardedSamples.WithLabelValues(tooFarInFuture, userID).Inc()
   117  		return newSampleTimestampTooNewError(unsafeMetricName, s.TimestampMs)
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  // ValidateExemplar returns an error if the exemplar is invalid.
   124  // The returned error may retain the provided series labels.
   125  func ValidateExemplar(userID string, ls []cortexpb.LabelAdapter, e cortexpb.Exemplar) ValidationError {
   126  	if len(e.Labels) <= 0 {
   127  		DiscardedExemplars.WithLabelValues(exemplarLabelsMissing, userID).Inc()
   128  		return newExemplarEmtpyLabelsError(ls, []cortexpb.LabelAdapter{}, e.TimestampMs)
   129  	}
   130  
   131  	if e.TimestampMs == 0 {
   132  		DiscardedExemplars.WithLabelValues(exemplarTimestampInvalid, userID).Inc()
   133  		return newExemplarMissingTimestampError(
   134  			ls,
   135  			e.Labels,
   136  			e.TimestampMs,
   137  		)
   138  	}
   139  
   140  	// Exemplar label length does not include chars involved in text
   141  	// rendering such as quotes, commas, etc.  See spec and const definition.
   142  	labelSetLen := 0
   143  	for _, l := range e.Labels {
   144  		labelSetLen += utf8.RuneCountInString(l.Name)
   145  		labelSetLen += utf8.RuneCountInString(l.Value)
   146  	}
   147  
   148  	if labelSetLen > ExemplarMaxLabelSetLength {
   149  		DiscardedExemplars.WithLabelValues(exemplarLabelsTooLong, userID).Inc()
   150  		return newExemplarLabelLengthError(
   151  			ls,
   152  			e.Labels,
   153  			e.TimestampMs,
   154  		)
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  // LabelValidationConfig helps with getting required config to validate labels.
   161  type LabelValidationConfig interface {
   162  	EnforceMetricName(userID string) bool
   163  	MaxLabelNamesPerSeries(userID string) int
   164  	MaxLabelNameLength(userID string) int
   165  	MaxLabelValueLength(userID string) int
   166  }
   167  
   168  // ValidateLabels returns an err if the labels are invalid.
   169  // The returned error may retain the provided series labels.
   170  func ValidateLabels(cfg LabelValidationConfig, userID string, ls []cortexpb.LabelAdapter, skipLabelNameValidation bool) ValidationError {
   171  	if cfg.EnforceMetricName(userID) {
   172  		unsafeMetricName, err := extract.UnsafeMetricNameFromLabelAdapters(ls)
   173  		if err != nil {
   174  			DiscardedSamples.WithLabelValues(missingMetricName, userID).Inc()
   175  			return newNoMetricNameError()
   176  		}
   177  
   178  		if !model.IsValidMetricName(model.LabelValue(unsafeMetricName)) {
   179  			DiscardedSamples.WithLabelValues(invalidMetricName, userID).Inc()
   180  			return newInvalidMetricNameError(unsafeMetricName)
   181  		}
   182  	}
   183  
   184  	numLabelNames := len(ls)
   185  	if numLabelNames > cfg.MaxLabelNamesPerSeries(userID) {
   186  		DiscardedSamples.WithLabelValues(maxLabelNamesPerSeries, userID).Inc()
   187  		return newTooManyLabelsError(ls, cfg.MaxLabelNamesPerSeries(userID))
   188  	}
   189  
   190  	maxLabelNameLength := cfg.MaxLabelNameLength(userID)
   191  	maxLabelValueLength := cfg.MaxLabelValueLength(userID)
   192  	lastLabelName := ""
   193  	for _, l := range ls {
   194  		if !skipLabelNameValidation && !model.LabelName(l.Name).IsValid() {
   195  			DiscardedSamples.WithLabelValues(invalidLabel, userID).Inc()
   196  			return newInvalidLabelError(ls, l.Name)
   197  		} else if len(l.Name) > maxLabelNameLength {
   198  			DiscardedSamples.WithLabelValues(labelNameTooLong, userID).Inc()
   199  			return newLabelNameTooLongError(ls, l.Name)
   200  		} else if len(l.Value) > maxLabelValueLength {
   201  			DiscardedSamples.WithLabelValues(labelValueTooLong, userID).Inc()
   202  			return newLabelValueTooLongError(ls, l.Value)
   203  		} else if cmp := strings.Compare(lastLabelName, l.Name); cmp >= 0 {
   204  			if cmp == 0 {
   205  				DiscardedSamples.WithLabelValues(duplicateLabelNames, userID).Inc()
   206  				return newDuplicatedLabelError(ls, l.Name)
   207  			}
   208  
   209  			DiscardedSamples.WithLabelValues(labelsNotSorted, userID).Inc()
   210  			return newLabelsNotSortedError(ls, l.Name)
   211  		}
   212  
   213  		lastLabelName = l.Name
   214  	}
   215  	return nil
   216  }
   217  
   218  // MetadataValidationConfig helps with getting required config to validate metadata.
   219  type MetadataValidationConfig interface {
   220  	EnforceMetadataMetricName(userID string) bool
   221  	MaxMetadataLength(userID string) int
   222  }
   223  
   224  // ValidateMetadata returns an err if a metric metadata is invalid.
   225  func ValidateMetadata(cfg MetadataValidationConfig, userID string, metadata *cortexpb.MetricMetadata) error {
   226  	if cfg.EnforceMetadataMetricName(userID) && metadata.GetMetricFamilyName() == "" {
   227  		DiscardedMetadata.WithLabelValues(missingMetricName, userID).Inc()
   228  		return httpgrpc.Errorf(http.StatusBadRequest, errMetadataMissingMetricName)
   229  	}
   230  
   231  	maxMetadataValueLength := cfg.MaxMetadataLength(userID)
   232  	var reason string
   233  	var cause string
   234  	var metadataType string
   235  	if len(metadata.GetMetricFamilyName()) > maxMetadataValueLength {
   236  		metadataType = typeMetricName
   237  		reason = metricNameTooLong
   238  		cause = metadata.GetMetricFamilyName()
   239  	} else if len(metadata.Help) > maxMetadataValueLength {
   240  		metadataType = typeHelp
   241  		reason = helpTooLong
   242  		cause = metadata.Help
   243  	} else if len(metadata.Unit) > maxMetadataValueLength {
   244  		metadataType = typeUnit
   245  		reason = unitTooLong
   246  		cause = metadata.Unit
   247  	}
   248  
   249  	if reason != "" {
   250  		DiscardedMetadata.WithLabelValues(reason, userID).Inc()
   251  		return httpgrpc.Errorf(http.StatusBadRequest, errMetadataTooLong, metadataType, cause, metadata.GetMetricFamilyName())
   252  	}
   253  
   254  	return nil
   255  }
   256  
   257  func DeletePerUserValidationMetrics(userID string, log log.Logger) {
   258  	filter := map[string]string{"user": userID}
   259  
   260  	if err := util.DeleteMatchingLabels(DiscardedSamples, filter); err != nil {
   261  		level.Warn(log).Log("msg", "failed to remove cortex_discarded_samples_total metric for user", "user", userID, "err", err)
   262  	}
   263  	if err := util.DeleteMatchingLabels(DiscardedExemplars, filter); err != nil {
   264  		level.Warn(log).Log("msg", "failed to remove cortex_discarded_exemplars_total metric for user", "user", userID, "err", err)
   265  	}
   266  	if err := util.DeleteMatchingLabels(DiscardedMetadata, filter); err != nil {
   267  		level.Warn(log).Log("msg", "failed to remove cortex_discarded_metadata_total metric for user", "user", userID, "err", err)
   268  	}
   269  }