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 }