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 }