github.com/grafana/pyroscope@v1.18.0/pkg/validation/validate.go (about)

     1  package validation
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"slices"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  	"unicode/utf8"
    11  
    12  	"github.com/grafana/pyroscope/pkg/pprof"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/go-kit/log/level"
    16  	"github.com/pkg/errors"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"github.com/prometheus/client_golang/prometheus/promauto"
    19  	"github.com/prometheus/common/model"
    20  
    21  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    22  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    23  	"github.com/grafana/pyroscope/pkg/util"
    24  	"github.com/grafana/pyroscope/pkg/util/validation"
    25  )
    26  
    27  type Reason string
    28  
    29  const (
    30  	ReasonLabel string = "reason"
    31  	Unknown     Reason = "unknown"
    32  	// InvalidLabels is a reason for discarding profiles which have labels that are invalid.
    33  	InvalidLabels Reason = "invalid_labels"
    34  	// MissingLabels is a reason for discarding profiles which have no labels.
    35  	MissingLabels Reason = "missing_labels"
    36  	// RateLimited is one of the values for the reason to discard samples.
    37  	RateLimited Reason = "rate_limited"
    38  
    39  	// NotInIngestionWindow is a reason for discarding profiles when Pyroscope doesn't accept profiles
    40  	// that are outside of the ingestion window.
    41  	NotInIngestionWindow Reason = "not_in_ingestion_window"
    42  
    43  	// MaxLabelNamesPerSeries is a reason for discarding a request which has too many label names
    44  	MaxLabelNamesPerSeries Reason = "max_label_names_per_series"
    45  	// LabelNameTooLong is a reason for discarding a request which has a label name too long
    46  	LabelNameTooLong Reason = "label_name_too_long"
    47  	// LabelValueTooLong is a reason for discarding a request which has a label value too long
    48  	LabelValueTooLong Reason = "label_value_too_long"
    49  	// DuplicateLabelNames is a reason for discarding a request which has duplicate label names
    50  	DuplicateLabelNames Reason = "duplicate_label_names"
    51  	// SeriesLimit is a reason for discarding lines when we can't create a new stream
    52  	// because the limit of active streams has been reached.
    53  	SeriesLimit           Reason = "series_limit"
    54  	QueryLimit            Reason = "query_limit"
    55  	SamplesLimit          Reason = "samples_limit"
    56  	ProfileSizeLimit      Reason = "profile_size_limit"
    57  	SampleLabelsLimit     Reason = "sample_labels_limit"
    58  	MalformedProfile      Reason = "malformed_profile"
    59  	FlameGraphLimit       Reason = "flamegraph_limit"
    60  	QueryMissingTimeRange Reason = "missing_time_range"
    61  	QueryInvalidTimeRange Reason = "invalid_time_range"
    62  
    63  	IngestLimitReached     Reason = "ingest_limit_reached"
    64  	SkippedBySamplingRules Reason = "dropped_by_sampling_rules"
    65  
    66  	BodySizeLimit Reason = "body_size_limit_exceeded"
    67  
    68  	// Those profiles were dropped because of relabeling rules
    69  	DroppedByRelabelRules Reason = "dropped_by_relabel_rules"
    70  
    71  	SeriesLimitErrorMsg                          = "Maximum active series limit exceeded (%d/%d), reduce the number of active streams (reduce labels or reduce label values), or contact your administrator to see if the limit can be increased"
    72  	MissingLabelsErrorMsg                        = "error at least one label pair is required per profile"
    73  	InvalidLabelsErrorMsg                        = "invalid labels '%s' with error: %s"
    74  	MaxLabelNamesPerSeriesErrorMsg               = "profile series '%s' has %d label names; limit %d"
    75  	LabelNameTooLongErrorMsg                     = "profile with labels '%s' has label name too long: '%s'"
    76  	LabelValueTooLongErrorMsg                    = "profile with labels '%s' has label value too long: '%s'"
    77  	DuplicateLabelNamesErrorMsg                  = "profile with labels '%s' has duplicate label name: '%s'"
    78  	DuplicateLabelNamesAfterSanitizationErrorMsg = "profile with labels '%s' has duplicate label name '%s' after label name sanitization from '%s'"
    79  	QueryTooLongErrorMsg                         = "the query time range exceeds the limit (max_query_length, actual: %s, limit: %s)"
    80  	ProfileTooBigErrorMsg                        = "the profile with labels '%s' exceeds the size limit (max_profile_size_byte, actual: %d, limit: %d)"
    81  	ProfileTooManySamplesErrorMsg                = "the profile with labels '%s' exceeds the samples count limit (max_profile_stacktrace_samples, actual: %d, limit: %d)"
    82  	ProfileTooManySampleLabelsErrorMsg           = "the profile with labels '%s' exceeds the sample labels limit (max_profile_stacktrace_sample_labels, actual: %d, limit: %d)"
    83  	NotInIngestionWindowErrorMsg                 = "profile with labels '%s' is outside of ingestion window (profile timestamp: %s, %s)"
    84  	MaxFlameGraphNodesErrorMsg                   = "max flamegraph nodes limit %d is greater than allowed %d"
    85  	MaxFlameGraphNodesUnlimitedErrorMsg          = "max flamegraph nodes limit must be set (max allowed %d)"
    86  	QueryMissingTimeRangeErrorMsg                = "missing time range in the query"
    87  	QueryStartAfterEndErrorMsg                   = "query start time is after end time"
    88  )
    89  
    90  var (
    91  	// DiscardedBytes is a metric of the total discarded bytes, by reason.
    92  	DiscardedBytes = promauto.NewCounterVec(
    93  		prometheus.CounterOpts{
    94  			Namespace: "pyroscope",
    95  			Name:      "discarded_bytes_total",
    96  			Help:      "The total number of bytes that were discarded.",
    97  		},
    98  		[]string{ReasonLabel, "tenant"},
    99  	)
   100  
   101  	// DiscardedProfiles is a metric of the number of discarded profiles, by reason.
   102  	DiscardedProfiles = promauto.NewCounterVec(
   103  		prometheus.CounterOpts{
   104  			Namespace: "pyroscope",
   105  			Name:      "discarded_samples_total",
   106  			Help:      "The total number of samples that were discarded.",
   107  		},
   108  		[]string{ReasonLabel, "tenant"},
   109  	)
   110  
   111  	// sanitizedLabelNames is a metric of the number of label names that were sanitized.
   112  	sanitizedLabelNames = promauto.NewCounterVec(
   113  		prometheus.CounterOpts{
   114  			Namespace: "pyroscope",
   115  			Name:      "sanitized_label_names_total",
   116  			Help:      "The total number of label names that were sanitized (e.g., dots replaced with underscores).",
   117  		},
   118  		[]string{"tenant"},
   119  	)
   120  )
   121  
   122  type LabelValidationLimits interface {
   123  	MaxLabelNameLength(tenantID string) int
   124  	MaxLabelValueLength(tenantID string) int
   125  	MaxLabelNamesPerSeries(tenantID string) int
   126  }
   127  
   128  // ValidateLabels validates the labels of a profile.
   129  // Returns the potentially modified labels slice (e.g., after sanitization) and any validation error.
   130  func ValidateLabels(limits LabelValidationLimits, tenantID string, ls []*typesv1.LabelPair, logger log.Logger) ([]*typesv1.LabelPair, error) {
   131  	if len(ls) == 0 {
   132  		return nil, NewErrorf(MissingLabels, MissingLabelsErrorMsg)
   133  	}
   134  	sort.Sort(phlaremodel.Labels(ls))
   135  	numLabelNames := len(ls)
   136  	maxLabels := limits.MaxLabelNamesPerSeries(tenantID)
   137  	if numLabelNames > maxLabels {
   138  		return nil, NewErrorf(MaxLabelNamesPerSeries, MaxLabelNamesPerSeriesErrorMsg, phlaremodel.LabelPairsString(ls), numLabelNames, maxLabels)
   139  	}
   140  	metricNameValue := phlaremodel.Labels(ls).Get(model.MetricNameLabel)
   141  	if !model.UTF8Validation.IsValidMetricName(metricNameValue) {
   142  		return nil, NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid metric name")
   143  	}
   144  	serviceNameValue := phlaremodel.Labels(ls).Get(phlaremodel.LabelNameServiceName)
   145  	if !isValidServiceName(serviceNameValue) {
   146  		return nil, NewErrorf(MissingLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "service name is not provided")
   147  	}
   148  
   149  	var (
   150  		lastLabelName = ""
   151  		idx           = 0
   152  	)
   153  	for idx < len(ls) {
   154  		l := ls[idx]
   155  		if len(l.Name) > limits.MaxLabelNameLength(tenantID) {
   156  			return nil, NewErrorf(LabelNameTooLong, LabelNameTooLongErrorMsg, phlaremodel.LabelPairsString(ls), l.Name)
   157  		}
   158  		if len(l.Value) > limits.MaxLabelValueLength(tenantID) {
   159  			return nil, NewErrorf(LabelValueTooLong, LabelValueTooLongErrorMsg, phlaremodel.LabelPairsString(ls), l.Value)
   160  		}
   161  		if origName, newName, ok := SanitizeLegacyLabelName(l.Name); ok && origName != newName {
   162  			var err error
   163  			// todo update canary exporter to check for utf8 labels, at least service.name once the write path supports utf8
   164  			ls, idx, err = handleSanitizedLabel(ls, idx, origName, newName)
   165  			if err != nil {
   166  				return nil, err
   167  			}
   168  			level.Debug(logger).Log(
   169  				"msg", "label name sanitized",
   170  				"origName", origName,
   171  				"serviceName", serviceNameValue)
   172  
   173  			sanitizedLabelNames.WithLabelValues(tenantID).Inc()
   174  			lastLabelName = ""
   175  			if idx > 0 && idx <= len(ls) {
   176  				lastLabelName = ls[idx-1].Name
   177  			}
   178  			continue
   179  		} else if !ok {
   180  			return nil, NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid label name '"+origName+"'")
   181  		}
   182  		if !model.LabelValue(l.Value).IsValid() {
   183  			return nil, NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid label value '"+l.Value+"'")
   184  		}
   185  		if cmp := strings.Compare(lastLabelName, l.Name); cmp == 0 {
   186  			return nil, NewErrorf(DuplicateLabelNames, DuplicateLabelNamesErrorMsg, phlaremodel.LabelPairsString(ls), l.Name)
   187  		}
   188  		lastLabelName = l.Name
   189  		idx += 1
   190  	}
   191  
   192  	return ls, nil
   193  }
   194  
   195  // handleSanitizedLabel handles the case where a label name is sanitized. It ensures that the label name is unique and fails if the value is distinct.
   196  func handleSanitizedLabel(ls []*typesv1.LabelPair, origIdx int, origName, newName string) ([]*typesv1.LabelPair, int, error) {
   197  	newLabel := &typesv1.LabelPair{Name: newName, Value: ls[origIdx].Value}
   198  
   199  	// Create new slice without the original element
   200  	newSlice := make([]*typesv1.LabelPair, 0, len(ls))
   201  	newSlice = append(newSlice, ls[:origIdx]...)
   202  	newSlice = append(newSlice, ls[origIdx+1:]...)
   203  
   204  	insertIdx, found := slices.BinarySearchFunc(newSlice, newLabel,
   205  		func(a, b *typesv1.LabelPair) int {
   206  			return strings.Compare(a.Name, b.Name)
   207  		})
   208  
   209  	if found {
   210  		if newSlice[insertIdx].Value == newLabel.Value {
   211  			// Same name and value we are done and can just return newSlice
   212  			return newSlice, origIdx, nil
   213  		} else {
   214  			// Same name, different value - error
   215  			return nil, 0, NewErrorf(DuplicateLabelNames,
   216  				DuplicateLabelNamesAfterSanitizationErrorMsg,
   217  				phlaremodel.LabelPairsString(ls), newName, origName)
   218  		}
   219  	}
   220  
   221  	// Insert the new label at correct position
   222  	newSlice = slices.Insert(newSlice, insertIdx, newLabel)
   223  
   224  	finalIdx := insertIdx
   225  	if insertIdx >= origIdx {
   226  		finalIdx = origIdx
   227  	}
   228  
   229  	copy(ls, newSlice)
   230  	return ls[:len(newSlice)], finalIdx, nil
   231  }
   232  
   233  // SanitizeLegacyLabelName reports whether the label name is a valid legacy label name,
   234  // and returns the sanitized value. Legacy label names are non utf-8 and contain characters
   235  // [a-zA-Z0-9_.].
   236  //
   237  // The only sanitization the function makes is replacing dots with underscores.
   238  func SanitizeLegacyLabelName(ln string) (old, sanitized string, ok bool) {
   239  	if len(ln) == 0 {
   240  		return ln, ln, false
   241  	}
   242  	hasDots := false
   243  	for i, b := range ln {
   244  		if (b < 'a' || b > 'z') && (b < 'A' || b > 'Z') && b != '_' && (b < '0' || b > '9' || i == 0) {
   245  			if b == '.' {
   246  				hasDots = true
   247  			} else {
   248  				return ln, ln, false
   249  			}
   250  		}
   251  	}
   252  	if !hasDots {
   253  		return ln, ln, true
   254  	}
   255  	r := []rune(ln)
   256  	for i, b := range r {
   257  		if b == '.' {
   258  			r[i] = '_'
   259  		}
   260  	}
   261  	return ln, string(r), true
   262  }
   263  
   264  type ProfileValidationLimits interface {
   265  	MaxProfileSizeBytes(tenantID string) int
   266  	MaxProfileStacktraceSamples(tenantID string) int
   267  	MaxProfileStacktraceSampleLabels(tenantID string) int
   268  	MaxProfileStacktraceDepth(tenantID string) int
   269  	MaxProfileSymbolValueLength(tenantID string) int
   270  	RejectNewerThan(tenantID string) time.Duration
   271  	RejectOlderThan(tenantID string) time.Duration
   272  }
   273  
   274  type ingestionWindow struct {
   275  	from, to model.Time
   276  }
   277  
   278  func newIngestionWindow(limits ProfileValidationLimits, tenantID string, now model.Time) *ingestionWindow {
   279  	var iw ingestionWindow
   280  	if d := limits.RejectNewerThan(tenantID); d != 0 {
   281  		iw.to = now.Add(d)
   282  	}
   283  	if d := limits.RejectOlderThan(tenantID); d != 0 {
   284  		iw.from = now.Add(-d)
   285  	}
   286  	return &iw
   287  }
   288  
   289  func (iw *ingestionWindow) errorDetail() string {
   290  	if iw.to == 0 {
   291  		return fmt.Sprintf("the ingestion window starts at %s", util.FormatTimeMillis(int64(iw.from)))
   292  	}
   293  	if iw.from == 0 {
   294  		return fmt.Sprintf("the ingestion window ends at %s", util.FormatTimeMillis(int64(iw.to)))
   295  	}
   296  	return fmt.Sprintf("the ingestion window starts at %s and ends at %s", util.FormatTimeMillis(int64(iw.from)), util.FormatTimeMillis(int64(iw.to)))
   297  
   298  }
   299  
   300  func (iw *ingestionWindow) valid(t model.Time, ls phlaremodel.Labels) error {
   301  	if (iw.from == 0 || t.After(iw.from)) && (iw.to == 0 || t.Before(iw.to)) {
   302  		return nil
   303  	}
   304  
   305  	return NewErrorf(NotInIngestionWindow, NotInIngestionWindowErrorMsg, phlaremodel.LabelPairsString(ls), util.FormatTimeMillis(int64(t)), iw.errorDetail())
   306  }
   307  
   308  type ValidatedProfile struct {
   309  	*pprof.Profile
   310  }
   311  
   312  func ValidateProfile(limits ProfileValidationLimits, tenantID string, prof *pprof.Profile, uncompressedSize int, ls phlaremodel.Labels, now model.Time) (ValidatedProfile, error) {
   313  	if prof == nil || prof.Profile == nil {
   314  		return ValidatedProfile{}, NewErrorf(MalformedProfile, "nil profile")
   315  	}
   316  	if len(prof.SampleType) == 0 {
   317  		return ValidatedProfile{}, NewErrorf(MalformedProfile, "empty profile")
   318  	}
   319  
   320  	if prof.TimeNanos > 0 {
   321  		// check profile timestamp within ingestion window
   322  		if err := newIngestionWindow(limits, tenantID, now).valid(model.TimeFromUnixNano(prof.TimeNanos), ls); err != nil {
   323  			return ValidatedProfile{}, err
   324  		}
   325  	} else {
   326  		prof.TimeNanos = now.UnixNano()
   327  	}
   328  
   329  	if limit := limits.MaxProfileSizeBytes(tenantID); limit != 0 && uncompressedSize > limit {
   330  		return ValidatedProfile{}, NewErrorf(ProfileSizeLimit, ProfileTooBigErrorMsg, phlaremodel.LabelPairsString(ls), uncompressedSize, limit)
   331  	}
   332  	if limit, size := limits.MaxProfileStacktraceSamples(tenantID), len(prof.Sample); limit != 0 && size > limit {
   333  		return ValidatedProfile{}, NewErrorf(SamplesLimit, ProfileTooManySamplesErrorMsg, phlaremodel.LabelPairsString(ls), size, limit)
   334  	}
   335  	var (
   336  		depthLimit        = limits.MaxProfileStacktraceDepth(tenantID)
   337  		labelsLimit       = limits.MaxProfileStacktraceSampleLabels(tenantID)
   338  		symbolLengthLimit = limits.MaxProfileSymbolValueLength(tenantID)
   339  	)
   340  	for _, s := range prof.Sample {
   341  		if depthLimit != 0 && len(s.LocationId) > depthLimit {
   342  			// Truncate the deepest frames: s.LocationId[0] is the leaf.
   343  			s.LocationId = s.LocationId[len(s.LocationId)-depthLimit:]
   344  		}
   345  		if labelsLimit != 0 && len(s.Label) > labelsLimit {
   346  			return ValidatedProfile{}, NewErrorf(SampleLabelsLimit, ProfileTooManySampleLabelsErrorMsg, phlaremodel.LabelPairsString(ls), len(s.Label), labelsLimit)
   347  		}
   348  	}
   349  	if symbolLengthLimit > 0 {
   350  		for i := range prof.StringTable {
   351  			if len(prof.StringTable[i]) > symbolLengthLimit {
   352  				prof.StringTable[i] = prof.StringTable[i][len(prof.StringTable[i])-symbolLengthLimit:]
   353  			}
   354  		}
   355  	}
   356  	for _, location := range prof.Location {
   357  		if location.Id == 0 {
   358  			return ValidatedProfile{}, NewErrorf(MalformedProfile, "location id is 0")
   359  		}
   360  	}
   361  	for _, function := range prof.Function {
   362  		if function.Id == 0 {
   363  			return ValidatedProfile{}, NewErrorf(MalformedProfile, "function id is 0")
   364  		}
   365  	}
   366  	for _, s := range prof.Sample {
   367  		if s == nil {
   368  			return ValidatedProfile{}, NewErrorf(MalformedProfile, "nil sample")
   369  		}
   370  		if len(s.Value) != len(prof.SampleType) {
   371  			return ValidatedProfile{}, NewErrorf(MalformedProfile, "sample value length mismatch")
   372  		}
   373  	}
   374  
   375  	if err := validateStringTableAccess(prof); err != nil {
   376  		return ValidatedProfile{}, err
   377  	}
   378  	for _, valueType := range prof.SampleType {
   379  		stt := prof.StringTable[valueType.Type]
   380  		if strings.Contains(stt, "-") {
   381  			return ValidatedProfile{}, NewErrorf(MalformedProfile, "sample type contains -")
   382  		}
   383  		// todo check if sample type is valid from the promql parser perspective
   384  	}
   385  
   386  	for _, s := range prof.StringTable {
   387  		if !utf8.ValidString(s) {
   388  			return ValidatedProfile{}, NewErrorf(MalformedProfile, "invalid utf8 string hex: %s", hex.EncodeToString([]byte(s)))
   389  		}
   390  	}
   391  	return ValidatedProfile{Profile: prof}, nil
   392  }
   393  
   394  func validateStringTableAccess(prof *pprof.Profile) error {
   395  	if len(prof.StringTable) == 0 || prof.StringTable[0] != "" {
   396  		return NewErrorf(MalformedProfile, "string 0 should be empty string")
   397  	}
   398  	for _, valueType := range prof.SampleType {
   399  		if int(valueType.Type) >= len(prof.StringTable) {
   400  			return NewErrorf(MalformedProfile, "sample type type string index out of range")
   401  		}
   402  	}
   403  	for _, sample := range prof.Sample {
   404  		for _, lbl := range sample.Label {
   405  			if int(lbl.Str) >= len(prof.StringTable) || int(lbl.Key) >= len(prof.StringTable) {
   406  				return NewErrorf(MalformedProfile, "sample label string index out of range")
   407  			}
   408  		}
   409  	}
   410  	for _, function := range prof.Function {
   411  		if int(function.Name) >= len(prof.StringTable) {
   412  			return NewErrorf(MalformedProfile, "function name string index out of range")
   413  		}
   414  		if int(function.SystemName) >= len(prof.StringTable) {
   415  			return NewErrorf(MalformedProfile, "function system name index string out of range")
   416  		}
   417  		if int(function.Filename) >= len(prof.StringTable) {
   418  			return NewErrorf(MalformedProfile, "function file name string index out of range")
   419  		}
   420  	}
   421  	for _, mapping := range prof.Mapping {
   422  		if int(mapping.Filename) >= len(prof.StringTable) {
   423  			return NewErrorf(MalformedProfile, "mapping file name string index out of range")
   424  		}
   425  		if int(mapping.BuildId) >= len(prof.StringTable) {
   426  			return NewErrorf(MalformedProfile, "mapping build id string index out of range")
   427  		}
   428  	}
   429  	return nil
   430  }
   431  
   432  func isValidServiceName(serviceNameValue string) bool {
   433  	return serviceNameValue != ""
   434  }
   435  
   436  type Error struct {
   437  	Reason Reason
   438  	msg    string
   439  }
   440  
   441  func (e *Error) Error() string {
   442  	return e.msg
   443  }
   444  
   445  func NewErrorf(reason Reason, msg string, args ...interface{}) *Error {
   446  	return &Error{
   447  		Reason: reason,
   448  		msg:    fmt.Sprintf(msg, args...),
   449  	}
   450  }
   451  
   452  func ReasonOf(err error) Reason {
   453  	var validationErr *Error
   454  	ok := errors.As(err, &validationErr)
   455  	if !ok {
   456  		return Unknown
   457  	}
   458  	return validationErr.Reason
   459  }
   460  
   461  type RangeRequestLimits interface {
   462  	MaxQueryLength(tenantID string) time.Duration
   463  	MaxQueryLookback(tenantID string) time.Duration
   464  }
   465  
   466  type ValidatedRangeRequest struct {
   467  	model.Interval
   468  	IsEmpty bool
   469  }
   470  
   471  func ValidateRangeRequest(limits RangeRequestLimits, tenantIDs []string, req model.Interval, now model.Time) (ValidatedRangeRequest, error) {
   472  	if req.Start == 0 || req.End == 0 {
   473  		return ValidatedRangeRequest{}, NewErrorf(QueryMissingTimeRange, QueryMissingTimeRangeErrorMsg)
   474  	}
   475  
   476  	if req.Start > req.End {
   477  		return ValidatedRangeRequest{}, NewErrorf(QueryInvalidTimeRange, QueryStartAfterEndErrorMsg)
   478  	}
   479  
   480  	if maxQueryLookback := validation.SmallestPositiveNonZeroDurationPerTenant(tenantIDs, limits.MaxQueryLookback); maxQueryLookback > 0 {
   481  		minStartTime := now.Add(-maxQueryLookback)
   482  
   483  		if req.End < minStartTime {
   484  			// The request is fully outside the allowed range, so we can return an
   485  			// empty response.
   486  			level.Debug(util.Logger).Log(
   487  				"msg", "skipping the execution of the query because its time range is before the 'max query lookback' setting",
   488  				"reqStart", util.FormatTimeMillis(int64(req.Start)),
   489  				"redEnd", util.FormatTimeMillis(int64(req.End)),
   490  				"maxQueryLookback", maxQueryLookback)
   491  
   492  			return ValidatedRangeRequest{IsEmpty: true, Interval: req}, nil
   493  		}
   494  
   495  		if req.Start < minStartTime {
   496  			// Replace the start time in the request.
   497  			level.Debug(util.Logger).Log(
   498  				"msg", "the start time of the query has been manipulated because of the 'max query lookback' setting",
   499  				"original", util.FormatTimeMillis(int64(req.Start)),
   500  				"updated", util.FormatTimeMillis(int64(minStartTime)))
   501  
   502  			req.Start = minStartTime
   503  		}
   504  	}
   505  
   506  	// Enforce the max query length.
   507  	if maxQueryLength := validation.SmallestPositiveNonZeroDurationPerTenant(tenantIDs, limits.MaxQueryLength); maxQueryLength > 0 {
   508  		queryLen := req.End.Sub(req.Start)
   509  		if queryLen > maxQueryLength {
   510  			return ValidatedRangeRequest{}, NewErrorf(QueryLimit, QueryTooLongErrorMsg, queryLen, model.Duration(maxQueryLength))
   511  		}
   512  	}
   513  
   514  	return ValidatedRangeRequest{Interval: req}, nil
   515  }
   516  
   517  func SanitizeTimeRange(limits RangeRequestLimits, tenant []string, start, end *int64) (empty bool, err error) {
   518  	var interval model.Interval
   519  	if start != nil {
   520  		interval.Start = model.Time(*start)
   521  	}
   522  	if end != nil {
   523  		interval.End = model.Time(*end)
   524  	}
   525  	validated, err := ValidateRangeRequest(limits, tenant, interval, model.Now())
   526  	if err != nil {
   527  		return false, err
   528  	}
   529  	if validated.IsEmpty {
   530  		return true, nil
   531  	}
   532  	*start = int64(validated.Start)
   533  	*end = int64(validated.End)
   534  	return false, nil
   535  }
   536  
   537  type FlameGraphLimits interface {
   538  	MaxFlameGraphNodesDefault(string) int
   539  	MaxFlameGraphNodesMax(string) int
   540  	MaxFlameGraphNodesOnSelectMergeProfile(string) bool
   541  }
   542  
   543  func ValidateMaxNodes(l FlameGraphLimits, tenantIDs []string, n int64) (int64, error) {
   544  	if n == 0 {
   545  		return int64(validation.SmallestPositiveNonZeroIntPerTenant(tenantIDs, l.MaxFlameGraphNodesDefault)), nil
   546  	}
   547  	maxNodes := int64(validation.SmallestPositiveNonZeroIntPerTenant(tenantIDs, l.MaxFlameGraphNodesMax))
   548  	if maxNodes != 0 {
   549  		if n > maxNodes {
   550  			return 0, NewErrorf(FlameGraphLimit, MaxFlameGraphNodesErrorMsg, n, maxNodes)
   551  		}
   552  		if n < 0 {
   553  			return 0, NewErrorf(FlameGraphLimit, MaxFlameGraphNodesUnlimitedErrorMsg, maxNodes)
   554  		}
   555  	}
   556  	return n, nil
   557  }