Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
loki/pkg/util/validation/validate.go

231 lines
8.6 KiB

package validation
import (
"net/http"
"strings"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/weaveworks/common/httpgrpc"
"github.com/grafana/loki/pkg/logproto"
"github.com/grafana/loki/pkg/util"
"github.com/grafana/loki/pkg/util/extract"
)
const (
discardReasonLabel = "reason"
errMetadataMissingMetricName = "metadata missing metric name"
errMetadataTooLong = "metadata '%s' value too long: %.200q metric %.200q"
typeMetricName = "METRIC_NAME"
typeHelp = "HELP"
typeUnit = "UNIT"
metricNameTooLong = "metric_name_too_long"
helpTooLong = "help_too_long"
unitTooLong = "unit_too_long"
// ErrQueryTooLong is used in chunk store, querier and query frontend.
ErrQueryTooLong = "the query time range exceeds the limit (query length: %s, limit: %s)"
missingMetricName = "missing_metric_name"
invalidMetricName = "metric_name_invalid"
greaterThanMaxSampleAge = "greater_than_max_sample_age"
maxLabelNamesPerSeries = "max_label_names_per_series"
tooFarInFuture = "too_far_in_future"
invalidLabel = "label_invalid"
labelNameTooLong = "label_name_too_long"
duplicateLabelNames = "duplicate_label_names"
labelsNotSorted = "labels_not_sorted"
labelValueTooLong = "label_value_too_long"
// RateLimited is one of the values for the reason to discard samples.
// Declared here to avoid duplication in ingester and distributor.
RateLimited = "rate_limited"
// Too many HA clusters is one of the reasons for discarding samples.
TooManyHAClusters = "too_many_ha_clusters"
// DroppedByRelabelConfiguration Samples can also be discarded because of relabeling configuration
DroppedByRelabelConfiguration = "relabel_configuration"
// DroppedByUserConfigurationOverride Samples discarded due to user configuration removing label __name__
DroppedByUserConfigurationOverride = "user_label_removal_configuration"
// The combined length of the label names and values of an Exemplar's LabelSet MUST NOT exceed 128 UTF-8 characters
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#exemplars
ExemplarMaxLabelSetLength = 128
)
// DiscardedSamples is a metric of the number of discarded samples, by reason.
var DiscardedSamples = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cortex_discarded_samples_total",
Help: "The total number of samples that were discarded.",
},
[]string{discardReasonLabel, "user"},
)
// DiscardedExemplars is a metric of the number of discarded exemplars, by reason.
var DiscardedExemplars = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cortex_discarded_exemplars_total",
Help: "The total number of exemplars that were discarded.",
},
[]string{discardReasonLabel, "user"},
)
// DiscardedMetadata is a metric of the number of discarded metadata, by reason.
var DiscardedMetadata = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cortex_discarded_metadata_total",
Help: "The total number of metadata that were discarded.",
},
[]string{discardReasonLabel, "user"},
)
func init() {
prometheus.MustRegister(DiscardedSamples)
prometheus.MustRegister(DiscardedExemplars)
prometheus.MustRegister(DiscardedMetadata)
}
// SampleValidationConfig helps with getting required config to validate sample.
type SampleValidationConfig interface {
RejectOldSamples(userID string) bool
RejectOldSamplesMaxAge(userID string) time.Duration
CreationGracePeriod(userID string) time.Duration
}
// ValidateSample returns an err if the sample is invalid.
// The returned error may retain the provided series labels.
func ValidateSample(cfg SampleValidationConfig, userID string, ls []logproto.LabelAdapter, s logproto.Sample) ValidationError {
unsafeMetricName, _ := extract.UnsafeMetricNameFromLabelAdapters(ls)
if cfg.RejectOldSamples(userID) && model.Time(s.Timestamp) < model.Now().Add(-cfg.RejectOldSamplesMaxAge(userID)) {
DiscardedSamples.WithLabelValues(greaterThanMaxSampleAge, userID).Inc()
return newSampleTimestampTooOldError(unsafeMetricName, s.Timestamp)
}
if model.Time(s.Timestamp) > model.Now().Add(cfg.CreationGracePeriod(userID)) {
DiscardedSamples.WithLabelValues(tooFarInFuture, userID).Inc()
return newSampleTimestampTooNewError(unsafeMetricName, s.Timestamp)
}
return nil
}
// LabelValidationConfig helps with getting required config to validate labels.
type LabelValidationConfig interface {
EnforceMetricName(userID string) bool
MaxLabelNamesPerSeries(userID string) int
MaxLabelNameLength(userID string) int
MaxLabelValueLength(userID string) int
}
// ValidateLabels returns an err if the labels are invalid.
// The returned error may retain the provided series labels.
func ValidateLabels(cfg LabelValidationConfig, userID string, ls []logproto.LabelAdapter, skipLabelNameValidation bool) ValidationError {
if cfg.EnforceMetricName(userID) {
unsafeMetricName, err := extract.UnsafeMetricNameFromLabelAdapters(ls)
if err != nil {
DiscardedSamples.WithLabelValues(missingMetricName, userID).Inc()
return newNoMetricNameError()
}
if !model.IsValidMetricName(model.LabelValue(unsafeMetricName)) {
DiscardedSamples.WithLabelValues(invalidMetricName, userID).Inc()
return newInvalidMetricNameError(unsafeMetricName)
}
}
numLabelNames := len(ls)
if numLabelNames > cfg.MaxLabelNamesPerSeries(userID) {
DiscardedSamples.WithLabelValues(maxLabelNamesPerSeries, userID).Inc()
return newTooManyLabelsError(ls, cfg.MaxLabelNamesPerSeries(userID))
}
maxLabelNameLength := cfg.MaxLabelNameLength(userID)
maxLabelValueLength := cfg.MaxLabelValueLength(userID)
lastLabelName := ""
for _, l := range ls {
if !skipLabelNameValidation && !model.LabelName(l.Name).IsValid() {
DiscardedSamples.WithLabelValues(invalidLabel, userID).Inc()
return newInvalidLabelError(ls, l.Name)
} else if len(l.Name) > maxLabelNameLength {
DiscardedSamples.WithLabelValues(labelNameTooLong, userID).Inc()
return newLabelNameTooLongError(ls, l.Name)
} else if len(l.Value) > maxLabelValueLength {
DiscardedSamples.WithLabelValues(labelValueTooLong, userID).Inc()
return newLabelValueTooLongError(ls, l.Value)
} else if cmp := strings.Compare(lastLabelName, l.Name); cmp >= 0 {
if cmp == 0 {
DiscardedSamples.WithLabelValues(duplicateLabelNames, userID).Inc()
return newDuplicatedLabelError(ls, l.Name)
}
DiscardedSamples.WithLabelValues(labelsNotSorted, userID).Inc()
return newLabelsNotSortedError(ls, l.Name)
}
lastLabelName = l.Name
}
return nil
}
// MetadataValidationConfig helps with getting required config to validate metadata.
type MetadataValidationConfig interface {
EnforceMetadataMetricName(userID string) bool
MaxMetadataLength(userID string) int
}
// ValidateMetadata returns an err if a metric metadata is invalid.
func ValidateMetadata(cfg MetadataValidationConfig, userID string, metadata *logproto.MetricMetadata) error {
if cfg.EnforceMetadataMetricName(userID) && metadata.GetMetricFamilyName() == "" {
DiscardedMetadata.WithLabelValues(missingMetricName, userID).Inc()
return httpgrpc.Errorf(http.StatusBadRequest, errMetadataMissingMetricName)
}
maxMetadataValueLength := cfg.MaxMetadataLength(userID)
var reason string
var cause string
var metadataType string
if len(metadata.GetMetricFamilyName()) > maxMetadataValueLength {
metadataType = typeMetricName
reason = metricNameTooLong
cause = metadata.GetMetricFamilyName()
} else if len(metadata.Help) > maxMetadataValueLength {
metadataType = typeHelp
reason = helpTooLong
cause = metadata.Help
} else if len(metadata.Unit) > maxMetadataValueLength {
metadataType = typeUnit
reason = unitTooLong
cause = metadata.Unit
}
if reason != "" {
DiscardedMetadata.WithLabelValues(reason, userID).Inc()
return httpgrpc.Errorf(http.StatusBadRequest, errMetadataTooLong, metadataType, cause, metadata.GetMetricFamilyName())
}
return nil
}
func DeletePerUserValidationMetrics(userID string, log log.Logger) {
filter := map[string]string{"user": userID}
if err := util.DeleteMatchingLabels(DiscardedSamples, filter); err != nil {
level.Warn(log).Log("msg", "failed to remove cortex_discarded_samples_total metric for user", "user", userID, "err", err)
}
if err := util.DeleteMatchingLabels(DiscardedExemplars, filter); err != nil {
level.Warn(log).Log("msg", "failed to remove cortex_discarded_exemplars_total metric for user", "user", userID, "err", err)
}
if err := util.DeleteMatchingLabels(DiscardedMetadata, filter); err != nil {
level.Warn(log).Log("msg", "failed to remove cortex_discarded_metadata_total metric for user", "user", userID, "err", err)
}
}