diff --git a/cmd/pyroscope/help-all.txt.tmpl b/cmd/pyroscope/help-all.txt.tmpl index fba3fba141..f307f3cc49 100644 --- a/cmd/pyroscope/help-all.txt.tmpl +++ b/cmd/pyroscope/help-all.txt.tmpl @@ -587,6 +587,8 @@ Usage of ./pyroscope: Limit how far back in profiling data can be queried, up until lookback duration ago. This limit is enforced in the query frontend. If the requested time range is outside the allowed range, the request will not fail, but will be modified to only query data within the allowed time range. 0 to disable, default to 7d. (default 1w) -querier.max-query-parallelism int Maximum number of queries that will be scheduled in parallel by the frontend. + -querier.min-step-duration duration + The minimum step duration for range queries. (default 15s) -querier.query-analysis-enabled Whether query analysis is enabled in the query frontend. If disabled, the /AnalyzeQuery endpoint will return an empty response. (default true) -querier.query-analysis-series-enabled diff --git a/cmd/pyroscope/help.txt.tmpl b/cmd/pyroscope/help.txt.tmpl index 34c557bb45..96842f95b0 100644 --- a/cmd/pyroscope/help.txt.tmpl +++ b/cmd/pyroscope/help.txt.tmpl @@ -167,6 +167,8 @@ Usage of ./pyroscope: Limit how far back in profiling data can be queried, up until lookback duration ago. This limit is enforced in the query frontend. If the requested time range is outside the allowed range, the request will not fail, but will be modified to only query data within the allowed time range. 0 to disable, default to 7d. (default 1w) -querier.max-query-parallelism int Maximum number of queries that will be scheduled in parallel by the frontend. + -querier.min-step-duration duration + The minimum step duration for range queries. (default 15s) -querier.query-analysis-enabled Whether query analysis is enabled in the query frontend. If disabled, the /AnalyzeQuery endpoint will return an empty response. (default true) -querier.query-analysis-series-enabled diff --git a/docs/sources/configure-server/reference-configuration-parameters/index.md b/docs/sources/configure-server/reference-configuration-parameters/index.md index c46dfb9e5c..e2f4f42560 100644 --- a/docs/sources/configure-server/reference-configuration-parameters/index.md +++ b/docs/sources/configure-server/reference-configuration-parameters/index.md @@ -2293,6 +2293,10 @@ distributor_usage_groups: # CLI flag: -querier.query-analysis-series-enabled [query_analysis_series_enabled: | default = false] +# The minimum step duration for range queries. +# CLI flag: -querier.min-step-duration +[min_step_duration: | default = 15s] + # Maximum number of flame graph nodes by default. 0 to disable. # CLI flag: -querier.max-flamegraph-nodes-default [max_flamegraph_nodes_default: | default = 8192] diff --git a/pkg/api/api.go b/pkg/api/api.go index b739c73a37..cf250a9c7b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -232,8 +232,8 @@ func (a *API) RegisterFeatureFlagsServiceHandler(svc capabilitiesv1connect.Featu capabilitiesv1connect.RegisterFeatureFlagsServiceHandler(a.server.HTTP, svc, a.connectOptionsAuthLogRecovery()...) } -func (a *API) RegisterPyroscopeHandlers(client querierv1connect.QuerierServiceClient) { - handlers := querier.NewHTTPHandlers(client) +func (a *API) RegisterPyroscopeHandlers(client querierv1connect.QuerierServiceClient, limits querier.Limits) { + handlers := querier.NewHTTPHandlers(client, limits) a.RegisterRoute("/pyroscope/render", http.HandlerFunc(handlers.Render), a.registerOptionsReadPath()...) a.RegisterRoute("/pyroscope/render-diff", http.HandlerFunc(handlers.RenderDiff), a.registerOptionsReadPath()...) a.RegisterRoute("/pyroscope/label-values", http.HandlerFunc(handlers.LabelValues), a.registerOptionsReadPath()...) diff --git a/pkg/pyroscope/modules.go b/pkg/pyroscope/modules.go index 91f9563259..d0262fd217 100644 --- a/pkg/pyroscope/modules.go +++ b/pkg/pyroscope/modules.go @@ -262,7 +262,7 @@ func (f *Pyroscope) initQuerier() (services.Service, error) { } if !f.isModuleActive(QueryFrontend) { - f.API.RegisterPyroscopeHandlers(querierSvc) + f.API.RegisterPyroscopeHandlers(querierSvc, f.Overrides) f.API.RegisterQuerierServiceHandler(querierSvc) } diff --git a/pkg/pyroscope/modules_experimental.go b/pkg/pyroscope/modules_experimental.go index 044a27093e..a86ff5498d 100644 --- a/pkg/pyroscope/modules_experimental.go +++ b/pkg/pyroscope/modules_experimental.go @@ -82,7 +82,7 @@ func (f *Pyroscope) initQueryFrontendV1() (services.Service, error) { } f.API.RegisterFrontendForQuerierHandler(f.frontend) f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger)) - f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger)) + f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger), f.Overrides) f.API.RegisterVCSServiceHandler(f.frontend) return f.frontend, nil } @@ -104,7 +104,7 @@ func (f *Pyroscope) initQueryFrontendV2() (services.Service, error) { ) f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger)) - f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger)) + f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger), f.Overrides) f.API.RegisterVCSServiceHandler(vcsService) // New query frontend does not have any state. @@ -148,7 +148,7 @@ func (f *Pyroscope) initQueryFrontendV12() (services.Service, error) { f.API.RegisterFrontendForQuerierHandler(f.frontend) f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger)) - f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger)) + f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger), f.Overrides) f.API.RegisterVCSServiceHandler(vcsService) return f.frontend, nil diff --git a/pkg/querier/http.go b/pkg/querier/http.go index 53e54a2855..1254e4fa85 100644 --- a/pkg/querier/http.go +++ b/pkg/querier/http.go @@ -12,6 +12,7 @@ import ( "connectrpc.com/connect" "github.com/gogo/status" "github.com/google/pprof/profile" + "github.com/grafana/dskit/tenant" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" @@ -31,12 +32,16 @@ import ( httputil "github.com/grafana/pyroscope/pkg/util/http" ) -func NewHTTPHandlers(client querierv1connect.QuerierServiceClient) *QueryHandlers { - return &QueryHandlers{client} +func NewHTTPHandlers(client querierv1connect.QuerierServiceClient, limits Limits) *QueryHandlers { + return &QueryHandlers{ + client: client, + limits: limits, + } } type QueryHandlers struct { client querierv1connect.QuerierServiceClient + limits Limits } // LabelValues only returns the label values for the given label name. @@ -186,7 +191,13 @@ func (q *QueryHandlers) Render(w http.ResponseWriter, req *http.Request) { return err }) - timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End) + // Get tenant-specific min step duration from limits + tenantID, err := tenant.TenantID(req.Context()) + if err != nil { + tenantID = "" // Use default limits if tenant ID cannot be extracted + } + minStepDuration := q.limits.MinStepDuration(tenantID) + timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End, minStepDuration) var resSeries *connect.Response[querierv1.SelectSeriesResponse] g.Go(func() error { var err error diff --git a/pkg/querier/querier.go b/pkg/querier/querier.go index c62c88adbb..c6c2887e4e 100644 --- a/pkg/querier/querier.go +++ b/pkg/querier/querier.go @@ -57,6 +57,7 @@ func (cfg *Config) RegisterFlags(fs *flag.FlagSet) { type Limits interface { QueryAnalysisSeriesEnabled(string) bool + MinStepDuration(string) time.Duration } type Querier struct { diff --git a/pkg/querier/timeline/calculator.go b/pkg/querier/timeline/calculator.go index 5e7b32536f..5fef8ef3ff 100644 --- a/pkg/querier/timeline/calculator.go +++ b/pkg/querier/timeline/calculator.go @@ -7,23 +7,22 @@ import ( ) var ( - DefaultRes int64 = 1500 - DefaultMinInterval = time.Second * 15 + DefaultRes int64 = 1500 ) // CalcPointInterval calculates the appropriate interval between each point (aka step) // Note that its main usage is with SelectSeries, therefore its // * inputs are in ms // * output is in seconds -func CalcPointInterval(fromMs int64, untilMs int64) float64 { +func CalcPointInterval(fromMs int64, untilMs int64, minStepDuration time.Duration) float64 { resolution := DefaultRes fromNano := fromMs * 1000000 untilNano := untilMs * 1000000 calculatedIntervalNano := time.Duration((untilNano - fromNano) / resolution) - if calculatedIntervalNano < DefaultMinInterval { - return DefaultMinInterval.Seconds() + if calculatedIntervalNano < minStepDuration { + return minStepDuration.Seconds() } return roundInterval(calculatedIntervalNano).Seconds() @@ -31,7 +30,7 @@ func CalcPointInterval(fromMs int64, untilMs int64) float64 { //nolint:gocyclo func roundInterval(interval time.Duration) time.Duration { - // Notice that interval may be smaller than DefaultMinInterval, and therefore some branches may never be reached + // Some branches may never be reached depending on the minimum interval configured // These branches are left in case the invariant changes switch { // 0.01s diff --git a/pkg/querier/timeline/calculator_test.go b/pkg/querier/timeline/calculator_test.go index 509f114d87..230904e67a 100644 --- a/pkg/querier/timeline/calculator_test.go +++ b/pkg/querier/timeline/calculator_test.go @@ -11,6 +11,7 @@ import ( func Test_CalcPointInterval(t *testing.T) { TestDate := time.Date(2023, time.April, 18, 1, 2, 3, 4, time.UTC) + defaultMinStepDuration := 15 * time.Second testCases := []struct { name string @@ -31,10 +32,94 @@ func Test_CalcPointInterval(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli()) + got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli(), defaultMinStepDuration) assert.Equal(t, float64(tc.want), got) }) } } + +func Test_CalcPointInterval_WithCustomMinStepDuration(t *testing.T) { + TestDate := time.Date(2023, time.April, 18, 1, 2, 3, 4, time.UTC) + + testCases := []struct { + name string + start time.Time + end time.Time + minStepDuration time.Duration + want float64 + }{ + { + name: "1 second with 5s min step duration", + start: TestDate, + end: TestDate.Add(1 * time.Second), + minStepDuration: 5 * time.Second, + want: 5.0, + }, + { + name: "1 second with 30s min step duration", + start: TestDate, + end: TestDate.Add(1 * time.Second), + minStepDuration: 30 * time.Second, + want: 30.0, + }, + { + name: "1 hour with 5s min step duration", + start: TestDate, + end: TestDate.Add(1 * time.Hour), + minStepDuration: 5 * time.Second, + want: 5.0, + }, + { + name: "1 hour with 1m min step duration", + start: TestDate, + end: TestDate.Add(1 * time.Hour), + minStepDuration: 1 * time.Minute, + want: 60.0, + }, + { + name: "7 days with 1m min step duration", + start: TestDate, + end: TestDate.Add(7 * 24 * time.Hour), + minStepDuration: 1 * time.Minute, + want: 300.0, // calculated interval is 5m, which is > 1m min + }, + { + name: "7 days with 10m min step duration", + start: TestDate, + end: TestDate.Add(7 * 24 * time.Hour), + minStepDuration: 10 * time.Minute, + want: 600.0, // min step duration enforced (10m) + }, + { + name: "30 days with default min step duration", + start: TestDate, + end: TestDate.Add(30 * 24 * time.Hour), + minStepDuration: 15 * time.Second, + want: 1800.0, // calculated interval is 30m + }, + { + name: "30 days with 1h min step duration", + start: TestDate, + end: TestDate.Add(30 * 24 * time.Hour), + minStepDuration: 1 * time.Hour, + want: 3600.0, // min step duration enforced (1h) + }, + { + name: "1 year with 5m min step duration", + start: TestDate, + end: TestDate.Add(365 * 24 * time.Hour), + minStepDuration: 5 * time.Minute, + want: 21600.0, // calculated interval is 6h + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli(), tc.minStepDuration) + + assert.Equal(t, tc.want, got, "expected %v seconds, got %v seconds", tc.want, got) + }) + } +} diff --git a/pkg/validation/limits.go b/pkg/validation/limits.go index 7360575079..2dc6cbaf56 100644 --- a/pkg/validation/limits.go +++ b/pkg/validation/limits.go @@ -85,6 +85,7 @@ type Limits struct { MaxQueryParallelism int `yaml:"max_query_parallelism" json:"max_query_parallelism"` QueryAnalysisEnabled bool `yaml:"query_analysis_enabled" json:"query_analysis_enabled"` QueryAnalysisSeriesEnabled bool `yaml:"query_analysis_series_enabled" json:"query_analysis_series_enabled"` + MinStepDuration model.Duration `yaml:"min_step_duration" json:"min_step_duration"` // Flame graph enforced limits. MaxFlameGraphNodesDefault int `yaml:"max_flamegraph_nodes_default" json:"max_flamegraph_nodes_default"` @@ -178,6 +179,9 @@ func (l *Limits) RegisterFlags(f *flag.FlagSet) { f.BoolVar(&l.QueryAnalysisEnabled, "querier.query-analysis-enabled", true, "Whether query analysis is enabled in the query frontend. If disabled, the /AnalyzeQuery endpoint will return an empty response.") f.BoolVar(&l.QueryAnalysisSeriesEnabled, "querier.query-analysis-series-enabled", false, "Whether the series portion of query analysis is enabled. If disabled, no series data (e.g., series count) will be calculated by the /AnalyzeQuery endpoint.") + _ = l.MinStepDuration.Set("15s") + f.Var(&l.MinStepDuration, "querier.min-step-duration", "The minimum step duration for range queries.") + f.IntVar(&l.MaxProfileSizeBytes, "validation.max-profile-size-bytes", 4*1024*1024, "Maximum size of a profile in bytes. This is based off the uncompressed size. 0 to disable.") f.IntVar(&l.MaxProfileStacktraceSamples, "validation.max-profile-stacktrace-samples", 16000, "Maximum number of samples in a profile. 0 to disable.") f.IntVar(&l.MaxProfileStacktraceSampleLabels, "validation.max-profile-stacktrace-sample-labels", 100, "Maximum number of labels in a profile sample. 0 to disable.") @@ -560,6 +564,11 @@ func (o *Overrides) QueryAnalysisSeriesEnabled(tenantID string) bool { return o.getOverridesForTenant(tenantID).QueryAnalysisSeriesEnabled } +// MinStepDuration returns the minimum step duration for range queries. +func (o *Overrides) MinStepDuration(tenantID string) time.Duration { + return time.Duration(o.getOverridesForTenant(tenantID).MinStepDuration) +} + func (o *Overrides) WritePathOverrides(tenantID string) writepath.Config { return o.getOverridesForTenant(tenantID).WritePathOverrides }